summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/yaml.gitlab-ci.yml1
-rw-r--r--.gitlab/issue_templates/Feature proposal.md17
-rw-r--r--.gitlab/merge_request_templates/Documentation.md1
-rw-r--r--.markdownlint.json1
-rw-r--r--Dangerfile23
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock45
-rw-r--r--app/assets/javascripts/admin/statistics_panel/components/app.vue45
-rw-r--r--app/assets/javascripts/admin/statistics_panel/constants.js14
-rw-r--r--app/assets/javascripts/admin/statistics_panel/index.js22
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/actions.js28
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/getters.js17
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/index.js16
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/mutations.js16
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/state.js5
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js8
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue1
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue20
-rw-r--r--app/assets/javascripts/error_tracking/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js11
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue38
-rw-r--r--app/assets/javascripts/pages/admin/index.js7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue3
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_component_mixin.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue49
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/settings.scss10
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/controllers/admin/dashboard_controller.rb3
-rw-r--r--app/controllers/clusters/base_controller.rb4
-rw-r--r--app/controllers/clusters/clusters_controller.rb13
-rw-r--r--app/controllers/projects/services_controller.rb27
-rw-r--r--app/controllers/projects/settings/operations_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb1
-rw-r--r--app/finders/issuable_finder.rb23
-rw-r--r--app/graphql/types/permission_types/project.rb2
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/external_link_helper.rb9
-rw-r--r--app/helpers/projects/error_tracking_helper.rb3
-rw-r--r--app/helpers/releases_helper.rb38
-rw-r--r--app/helpers/user_callouts_helper.rb6
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb18
-rw-r--r--app/models/ci/pipeline_enums.rb3
-rw-r--r--app/models/clusters/applications/jupyter.rb7
-rw-r--r--app/models/clusters/cluster.rb6
-rw-r--r--app/models/concerns/ci/metadatable.rb9
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/routable.rb12
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/external_pull_request.rb96
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/protected_branch.rb3
-rw-r--r--app/models/service.rb7
-rw-r--r--app/models/user.rb2
-rw-r--r--app/policies/clusters/instance_policy.rb1
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/event_presenter.rb20
-rw-r--r--app/presenters/instance_clusterable_presenter.rb4
-rw-r--r--app/serializers/merge_request_noteable_entity.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb25
-rw-r--r--app/services/external_pull_requests/create_pipeline_service.rb29
-rw-r--r--app/services/git/base_hooks_service.rb28
-rw-r--r--app/services/issuable_base_service.rb15
-rw-r--r--app/services/labels/available_labels_service.rb6
-rw-r--r--app/services/labels/find_or_create_service.rb8
-rw-r--r--app/services/merge_requests/build_service.rb12
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb10
-rw-r--r--app/services/protected_branches/create_service.rb7
-rw-r--r--app/validators/addressable_url_validator.rb8
-rw-r--r--app/validators/certificate_key_validator.rb4
-rw-r--r--app/validators/named_ecdsa_key_validator.rb34
-rw-r--r--app/views/admin/dashboard/index.html.haml36
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml8
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml11
-rw-r--r--app/views/clusters/clusters/eks/_index.html.haml1
-rw-r--r--app/views/clusters/clusters/new.html.haml44
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/_event_scope.html.haml5
-rw-r--r--app/views/events/event/_common.html.haml4
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml350
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml6
-rw-r--r--app/views/projects/buttons/_fork.html.haml2
-rw-r--r--app/views/projects/error_tracking/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml17
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml1
-rw-r--r--app/views/projects/pages/_access.html.haml6
-rw-r--r--app/views/projects/pages/_list.html.haml4
-rw-r--r--app/views/projects/pages_domains/show.html.haml4
-rw-r--r--app/views/projects/releases/index.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml1
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/update_external_pull_requests_worker.rb25
-rw-r--r--changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml5
-rw-r--r--changelogs/unreleased/46686-bump-kubeclient-version-qa.yml5
-rw-r--r--changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml5
-rw-r--r--changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml5
-rw-r--r--changelogs/unreleased/60724-watch-button.yml5
-rw-r--r--changelogs/unreleased/66067-pages-domain-doesnt-set-target-blank.yml5
-rw-r--r--changelogs/unreleased/66150-remove-dynamically-constructed-feature-flags-starting-with-promethe.yml5
-rw-r--r--changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml5
-rw-r--r--changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml5
-rw-r--r--changelogs/unreleased/67037-user-content-gitlab-static-net-brings-back-404-only.yml5
-rw-r--r--changelogs/unreleased/ab-unconfirmed-email-index.yml5
-rw-r--r--changelogs/unreleased/ac-accelerate-wiki-attachments.yml5
-rw-r--r--changelogs/unreleased/add-label-push-opts.yml5
-rw-r--r--changelogs/unreleased/api_settings.yml5
-rw-r--r--changelogs/unreleased/ecdsa_pages_certificates.yml5
-rw-r--r--changelogs/unreleased/fj-11777-lower-search-count-limits.yml5
-rw-r--r--changelogs/unreleased/fj-remove-dns-protection-when-validating.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.62.0.yml5
-rw-r--r--changelogs/unreleased/group_level_jupyterhub.yml5
-rw-r--r--changelogs/unreleased/id-autosave-for-new-mr.yml5
-rw-r--r--changelogs/unreleased/instance-group-level-knative.yml5
-rw-r--r--changelogs/unreleased/issue-32741.yml5
-rw-r--r--changelogs/unreleased/issue-67127.yml5
-rw-r--r--changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml5
-rw-r--r--changelogs/unreleased/job-rules-e2e.yml5
-rw-r--r--changelogs/unreleased/kamil-improve-import-export.yml5
-rw-r--r--changelogs/unreleased/keyboard-shortcuts-2.yml5
-rw-r--r--changelogs/unreleased/pl-project-service-json.yml5
-rw-r--r--changelogs/unreleased/quote-branch-names-in-instructions.yml5
-rw-r--r--changelogs/unreleased/sh-add-margin-member-list.yml5
-rw-r--r--changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml5
-rw-r--r--changelogs/unreleased/update-rouge.yml5
-rw-r--r--changelogs/unreleased/use_default_external_auth_label_empty.yml6
-rw-r--r--config/gitlab.yml.example5
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/chronic_duration.rb4
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/repository.rb2
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--danger/database/Dangerfile18
-rw-r--r--danger/documentation/Dangerfile22
-rw-r--r--danger/duplicate_yarn_dependencies/Dangerfile24
-rw-r--r--danger/eslint/Dangerfile22
-rw-r--r--danger/frozen_string/Dangerfile12
-rw-r--r--danger/gemfile/Dangerfile19
-rw-r--r--danger/prettier/Dangerfile26
-rw-r--r--db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb31
-rw-r--r--db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb21
-rw-r--r--db/migrate/20190826100605_add_group_column_to_events.rb14
-rw-r--r--db/migrate/20190829131130_create_external_pull_requests.rb25
-rw-r--r--db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb15
-rw-r--r--db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb17
-rw-r--r--db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb17
-rw-r--r--db/migrate/20190904173203_add_index_on_users_unconfirmed_email.rb17
-rw-r--r--db/migrate/20190905223800_add_interruptible_to_builds_metadata.rb9
-rw-r--r--db/migrate/20190905223900_add_concurrent_index_to_builds_metadata.rb19
-rw-r--r--db/schema.rb28
-rw-r--r--doc/administration/database_load_balancing.md35
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/integration/plantuml.md17
-rw-r--r--doc/administration/logs.md4
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md33
-rw-r--r--doc/administration/repository_storage_types.md2
-rw-r--r--doc/administration/smime_signing_email.md51
-rw-r--r--doc/api/api_resources.md1
-rw-r--r--doc/api/merge_request_approvals.md71
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/statistics.md35
-rw-r--r--doc/api/tags.md9
-rw-r--r--doc/api/v3_to_v4.md4
-rw-r--r--doc/ci/docker/using_docker_build.md7
-rw-r--r--doc/ci/examples/README.md2
-rw-r--r--doc/ci/examples/browser_performance.md157
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md14
-rw-r--r--doc/ci/multi_project_pipelines.md2
-rw-r--r--doc/ci/variables/predefined_variables.md1
-rw-r--r--doc/ci/yaml/README.md43
-rw-r--r--doc/development/api_graphql_styleguide.md45
-rw-r--r--doc/development/architecture.md4
-rw-r--r--doc/development/background_migrations.md8
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/code_review.md2
-rw-r--r--doc/development/dangerbot.md2
-rw-r--r--doc/development/distributed_tracing.md12
-rw-r--r--doc/development/documentation/index.md90
-rw-r--r--doc/development/documentation/site_architecture/global_nav.md87
-rw-r--r--doc/development/documentation/site_architecture/index.md8
-rw-r--r--doc/development/documentation/styleguide.md58
-rw-r--r--doc/development/fe_guide/development_process.md18
-rw-r--r--doc/development/fe_guide/vuex.md2
-rw-r--r--doc/development/instrumentation.md4
-rw-r--r--doc/development/integrations/jira_connect.md6
-rw-r--r--doc/development/performance.md26
-rw-r--r--doc/development/testing_guide/end_to_end/page_objects.md12
-rw-r--r--doc/development/testing_guide/frontend_testing.md15
-rw-r--r--doc/development/what_requires_downtime.md4
-rw-r--r--doc/downgrade_ee_to_ce/README.md16
-rw-r--r--doc/install/aws/index.md6
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/omniauth.md30
-rw-r--r--doc/migrate_ci_to_ce/README.md3
-rw-r--r--doc/raketasks/backup_restore.md49
-rw-r--r--doc/raketasks/import.md10
-rw-r--r--doc/security/reset_root_password.md2
-rw-r--r--doc/update/mysql_to_postgresql.md10
-rw-r--r--doc/user/admin_area/diff_limits.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md89
-rw-r--r--doc/user/application_security/index.md25
-rw-r--r--doc/user/application_security/sast/index.md20
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.pngbin60530 -> 61667 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.pngbin43250 -> 52247 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard.pngbin73425 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.pngbin0 -> 48767 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md7
-rw-r--r--doc/user/clusters/applications.md51
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/profile/account/create_accounts.md36
-rw-r--r--doc/user/profile/account/img/admin_user_button.pngbin0 -> 84870 bytes
-rw-r--r--doc/user/profile/account/img/admin_user_form.pngbin0 -> 196691 bytes
-rw-r--r--doc/user/profile/account/img/register_tab.pngbin0 -> 205561 bytes
-rw-r--r--doc/user/profile/index.md4
-rw-r--r--doc/user/project/canary_deployments.md2
-rw-r--r--doc/user/project/description_templates.md2
-rw-r--r--doc/user/project/import/github.md29
-rw-r--r--doc/user/project/import/img/import_projects_from_repo_url.pngbin142329 -> 142284 bytes
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md173
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff_v12_2.pngbin0 -> 136144 bytes
-rw-r--r--doc/user/project/merge_requests/index.md55
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md6
-rw-r--r--doc/user/project/pages/getting_started_part_four.md6
-rw-r--r--doc/user/project/protected_branches.md2
-rw-r--r--doc/user/project/web_ide/index.md2
-rw-r--r--doc/workflow/img/repository_mirroring_push_settings.pngbin50526 -> 72515 bytes
-rw-r--r--doc/workflow/lfs/lfs_administration.md6
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md3
-rw-r--r--doc/workflow/repository_mirroring.md14
-rw-r--r--doc/workflow/shortcuts.md191
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/entities.rb53
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/helpers/internal_helpers.rb12
-rw-r--r--lib/api/helpers/runner.rb2
-rw-r--r--lib/api/internal/base.rb35
-rw-r--r--lib/api/internal/pages.rb27
-rw-r--r--lib/api/settings.rb8
-rw-r--r--lib/api/statistics.rb18
-rw-r--r--lib/api/support/git_access_actor.rb42
-rw-r--r--lib/api/validations/types/workhorse_file.rb20
-rw-r--r--lib/api/wikis.rb22
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/banzai/filter/asset_proxy_filter.rb6
-rw-r--r--lib/banzai/filter/color_filter.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb4
-rw-r--r--lib/banzai/filter/footnote_filter.rb4
-rw-r--r--lib/banzai/filter/math_filter.rb8
-rw-r--r--lib/banzai/filter/suggestion_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb4
-rw-r--r--lib/banzai/issuable_extractor.rb4
-rw-r--r--lib/bitbucket/connection.rb4
-rw-r--r--lib/gitlab.rb4
-rw-r--r--lib/gitlab/auth/unique_ips_limiter.rb2
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb2
-rw-r--r--lib/gitlab/background_migration/merge_request_assignees_migration_progress_check.rb2
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb8
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb2
-rw-r--r--lib/gitlab/checks/lfs_check.rb4
-rw-r--r--lib/gitlab/checks/project_created.rb2
-rw-r--r--lib/gitlab/checks/project_moved.rb2
-rw-r--r--lib/gitlab/ci/build/port.rb4
-rw-r--r--lib/gitlab/ci/build/rules.rb9
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb4
-rw-r--r--lib/gitlab/ci/build/step.rb6
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb19
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/template.rb2
-rw-r--r--lib/gitlab/ci/cron_parser.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb1
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb2
-rw-r--r--lib/gitlab/ci/reports/test_case.rb8
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml22
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--lib/gitlab/cleanup/project_upload_file_finder.rb6
-rw-r--r--lib/gitlab/danger/helper.rb20
-rw-r--r--lib/gitlab/danger/teammate.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb81
-rw-r--r--lib/gitlab/database/sha_attribute.rb2
-rw-r--r--lib/gitlab/downtime_check/message.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb5
-rw-r--r--lib/gitlab/etag_caching/store.rb2
-rw-r--r--lib/gitlab/git.rb6
-rw-r--r--lib/gitlab/git/lfs_pointer_file.rb4
-rw-r--r--lib/gitlab/git/repository.rb4
-rw-r--r--lib/gitlab/git/util.rb2
-rw-r--r--lib/gitlab/github_import/issuable_finder.rb2
-rw-r--r--lib/gitlab/github_import/label_finder.rb2
-rw-r--r--lib/gitlab/github_import/milestone_finder.rb2
-rw-r--r--lib/gitlab/github_import/page_counter.rb2
-rw-r--r--lib/gitlab/github_import/parallel_scheduling.rb2
-rw-r--r--lib/gitlab/github_import/user_finder.rb6
-rw-r--r--lib/gitlab/health_checks/gitaly_check.rb2
-rw-r--r--lib/gitlab/i18n/po_linter.rb8
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb2
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb6
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb65
-rw-r--r--lib/gitlab/import_export/config.rb81
-rw-r--r--lib/gitlab/import_export/fast_hash_serializer.rb108
-rw-r--r--lib/gitlab/import_export/import_export.yml178
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb117
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb85
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb27
-rw-r--r--lib/gitlab/import_export/reader.rb30
-rw-r--r--lib/gitlab/incoming_email.rb6
-rw-r--r--lib/gitlab/job_waiter.rb2
-rw-r--r--lib/gitlab/kubernetes/helm.rb12
-rw-r--r--lib/gitlab/kubernetes/pod.rb10
-rw-r--r--lib/gitlab/logger.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb2
-rw-r--r--lib/gitlab/metrics/transaction.rb7
-rw-r--r--lib/gitlab/metrics/web_transaction.rb4
-rw-r--r--lib/gitlab/middleware/multipart.rb20
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb4
-rw-r--r--lib/gitlab/pages.rb17
-rw-r--r--lib/gitlab/patch/chronic_duration.rb35
-rw-r--r--lib/gitlab/path_regex.rb6
-rw-r--r--lib/gitlab/performance_bar.rb2
-rw-r--r--lib/gitlab/polling_interval.rb2
-rw-r--r--lib/gitlab/private_commit_email.rb2
-rw-r--r--lib/gitlab/profiler.rb2
-rw-r--r--lib/gitlab/prometheus/additional_metrics_parser.rb2
-rw-r--r--lib/gitlab/push_options.rb27
-rw-r--r--lib/gitlab/query_limiting/middleware.rb4
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb5
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb2
-rw-r--r--lib/gitlab/redis/cache.rb6
-rw-r--r--lib/gitlab/redis/queues.rb8
-rw-r--r--lib/gitlab/redis/shared_state.rb12
-rw-r--r--lib/gitlab/redis/wrapper.rb4
-rw-r--r--lib/gitlab/request_profiler.rb2
-rw-r--r--lib/gitlab/search_results.rb5
-rw-r--r--lib/gitlab/shard_health_cache.rb2
-rw-r--r--lib/gitlab/sidekiq_monitor.rb2
-rw-r--r--lib/gitlab/sidekiq_status.rb2
-rw-r--r--lib/gitlab/url_helpers.rb2
-rw-r--r--lib/gitlab/workhorse.rb12
-rw-r--r--lib/gitlab_danger.rb54
-rw-r--r--lib/google_api/cloud_platform/client.rb4
-rw-r--r--lib/quality/test_level.rb2
-rw-r--r--lib/system_check/app/init_script_up_to_date_check.rb2
-rw-r--r--lib/system_check/app/redis_version_check.rb2
-rw-r--r--lib/tasks/gitlab/graphql.rake4
-rw-r--r--lib/tasks/gitlab/uploads/legacy.rake2
-rw-r--r--lib/tasks/gitlab_danger.rake17
-rw-r--r--locale/gitlab.pot190
-rw-r--r--package.json5
-rw-r--r--plugins/.gitignore5
-rw-r--r--qa/README.md4
-rw-r--r--qa/qa/page/main/menu.rb4
-rw-r--r--qa/qa/page/merge_request/new.rb2
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/page/project/milestone/index.rb2
-rw-r--r--qa/qa/resource/group.rb6
-rw-r--r--qa/qa/resource/issue.rb3
-rw-r--r--qa/qa/resource/merge_request.rb19
-rw-r--r--qa/qa/resource/project.rb9
-rw-r--r--qa/qa/resource/user.rb1
-rw-r--r--shared/.gitignore4
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb19
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb19
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb23
-rw-r--r--spec/controllers/projects/services_controller_spec.rb130
-rw-r--r--spec/controllers/registrations_controller_spec.rb20
-rw-r--r--spec/factories/external_pull_requests.rb17
-rw-r--r--spec/factories/pages_domains.rb83
-rw-r--r--spec/features/admin/clusters/applications_spec.rb21
-rw-r--r--spec/features/admin/dashboard_spec.rb2
-rw-r--r--spec/features/clusters/installing_applications_shared_examples.rb228
-rw-r--r--spec/features/groups/clusters/applications_spec.rb23
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb11
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/projects/clusters/applications_spec.rb229
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb3
-rw-r--r--spec/features/projects/clusters_spec.rb24
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb2
-rw-r--r--spec/finders/issues_finder_spec.rb50
-rw-r--r--spec/finders/merge_requests_finder_spec.rb20
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_noteable.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/tag.json3
-rw-r--r--spec/fixtures/api/schemas/statistics.json29
-rw-r--r--spec/fixtures/valid.po5
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js73
-rw-r--r--spec/frontend/admin/statistics_panel/mock_data.js15
-rw-r--r--spec/frontend/admin/statistics_panel/store/actions_spec.js115
-rw-r--r--spec/frontend/admin/statistics_panel/store/getters_spec.js48
-rw-r--r--spec/frontend/admin/statistics_panel/store/mutations_spec.js41
-rw-r--r--spec/frontend/clusters/components/applications_spec.js41
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js32
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js318
-rw-r--r--spec/frontend/lib/utils/axios_utils_spec.js45
-rw-r--r--spec/frontend/mocks/ce/lib/utils/axios_utils.js62
-rw-r--r--spec/frontend/notes/old_notes_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js115
-rw-r--r--spec/helpers/events_helper_spec.rb2
-rw-r--r--spec/helpers/external_link_helper_spec.rb17
-rw-r--r--spec/helpers/icons_helper_spec.rb7
-rw-r--r--spec/helpers/projects/error_tracking_helper_spec.rb28
-rw-r--r--spec/helpers/releases_helper_spec.rb46
-rw-r--r--spec/helpers/version_check_helper_spec.rb4
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js26
-rw-r--r--spec/javascripts/boards/mock_data.js2
-rw-r--r--spec/javascripts/ide/components/preview/clientside_spec.js363
-rw-r--r--spec/javascripts/monitoring/components/graph_group_spec.js47
-rw-r--r--spec/lib/api/support/git_access_actor_spec.rb128
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb11
-rw-r--r--spec/lib/gitlab/checks/diff_check_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb64
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb58
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb12
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb20
-rw-r--r--spec/lib/gitlab/favicon_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml6
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attributes_finder_spec.rb230
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb284
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb272
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb53
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb105
-rw-r--r--spec/lib/gitlab/import_export/relation_rename_service_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml16
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/pages_spec.rb29
-rw-r--r--spec/lib/gitlab/patch/chronic_duration_spec.rb27
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/query_limiting/transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/query_limiting_spec.rb9
-rw-r--r--spec/lib/gitlab/search_results_spec.rb17
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/time_tracking_formatter_spec.rb8
-rw-r--r--spec/lib/gitlab_danger_spec.rb76
-rw-r--r--spec/lib/gitlab_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb20
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb48
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb4
-rw-r--r--spec/models/concerns/has_status_spec.rb12
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb4
-rw-r--r--spec/models/external_pull_request_spec.rb220
-rw-r--r--spec/models/internal_id_spec.rb3
-rw-r--r--spec/models/pages_domain_spec.rb18
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/presenters/event_presenter_spec.rb41
-rw-r--r--spec/requests/api/internal/pages_spec.rb54
-rw-r--r--spec/requests/api/settings_spec.rb38
-rw-r--r--spec/requests/api/statistics_spec.rb91
-rw-r--r--spec/requests/api/wikis_spec.rb13
-rw-r--r--spec/requests/projects/uploads_spec.rb38
-rw-r--r--spec/requests/rack_attack_global_spec.rb222
-rw-r--r--spec/routing/project_routing_spec.rb70
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb5
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb691
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb13
-rw-r--r--spec/services/external_pull_requests/create_pipeline_service_spec.rb72
-rw-r--r--spec/services/git/branch_push_service_spec.rb14
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb145
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb13
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/helpers/search_helpers.rb4
-rw-r--r--spec/support/helpers/workhorse_helpers.rb31
-rw-r--r--spec/support/import_export/import_export.yml25
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb221
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb4
-rw-r--r--spec/validators/addressable_url_validator_spec.rb37
-rw-r--r--spec/validators/named_ecdsa_key_validator_spec.rb54
-rw-r--r--spec/workers/update_external_pull_requests_worker_spec.rb54
-rw-r--r--tmp/.gitignore5
-rw-r--r--yarn.lock21
513 files changed, 9000 insertions, 3101 deletions
diff --git a/.gitignore b/.gitignore
index 3ffe4263c4f..7310c04d117 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,19 +61,17 @@ eslint-report.html
/shared/artifacts/
/rails_best_practices_output.html
/tags
-/tmp/*
/vendor/bundle/*
/vendor/gitaly-ruby
/builds*
-/shared/*
/.gitlab_workhorse_secret
+/.gitlab_pages_shared_secret
/webpack-report/
/knapsack/
/rspec_flaky/
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
-/plugins/*
/.gitlab_pages_secret
/.gitlab_smime_key
/.gitlab_smime_cert
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5b5527284d3..b3b6beb1068 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,6 +5,7 @@ stages:
- prepare
- quick-test
- test
+ - review-prepare
- review
- qa
- post-test
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 0720ea3e056..a20215694c0 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -13,7 +13,9 @@
- .default-before_script
- .except-docs
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
services:
- docker:19.03.0-dind
variables:
@@ -42,10 +44,6 @@
- time scripts/build_assets_image
- scripts/clean-old-cached-assets
- rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
- # Play dependent manual jobs
- - install_api_client_dependencies_with_apt
- - play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
- - play_job "schedule:review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
only:
- /.+/@gitlab-org/gitlab-ce
- /.+/@gitlab-org/gitlab-ee
@@ -186,7 +184,8 @@ jest:
- .default-retry
- .default-cache
- .except-docs
- dependencies: ["setup-test-env"]
+ dependencies: []
+ stage: test
variables:
SETUP_DB: "false"
before_script:
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 9c021b23db6..ca89c4e2c89 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -28,7 +28,8 @@ package-and-qa-manual:master:
extends: .package-and-qa-base
only:
refs:
- - master
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
when: manual
needs: ["build-qa-image", "gitlab:assets:compile"]
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 2e8b197829b..878be25c39b 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -165,7 +165,9 @@ downtime_check:
- /^[\d-]+-stable(-ee)?$/
- /(^docs[\/-].+|.+-docs$)/
- /(^qa[\/-].*|.*-qa$)/
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
ee_compat_check:
extends: .rake-exec
@@ -195,7 +197,9 @@ db:migrate:reset:
- .default-before_script
- .use-pg
- .except-docs-qa
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
script:
- bundle exec rake db:migrate:reset
@@ -207,7 +211,9 @@ db:check-schema:
- .default-before_script
- .use-pg
- .except-docs-qa
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
script:
- source scripts/schema_changed.sh
@@ -219,7 +225,9 @@ db:migrate-from-v11.11.0:
- .default-before_script
- .use-pg
- .except-docs-qa
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
variables:
SETUP_DB: "false"
script:
@@ -248,7 +256,9 @@ db:rollback:
- .default-before_script
- .use-pg
- .except-docs-qa
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
script:
- bundle exec rake db:migrate VERSION=20180101160629
- bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
@@ -261,7 +271,9 @@ gitlab:setup:
- .default-before_script
- .use-pg
- .except-docs-qa
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
variables:
SETUP_DB: "false"
script:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 6695404653c..6f1505b5c0d 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -53,8 +53,7 @@ build-qa-image:
.review-build-cng-base:
image: ruby:2.6-alpine
- stage: test
- when: manual
+ stage: review-prepare
before_script:
- source scripts/utils.sh
- install_api_client_dependencies_with_apk
@@ -67,17 +66,20 @@ review-build-cng:
extends:
- .review-build-cng-base
- .review-only
+ needs: ["gitlab:assets:compile pull-cache"]
schedule:review-build-cng:
extends:
- .review-build-cng-base
- .review-schedules-only
+ needs: ["gitlab:assets:compile"]
review-deploy:
extends: .review-base
allow_failure: true
retry: 1
stage: review
+ needs: ["review-build-cng"]
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index d9384780356..cd7ca4d30bf 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -7,7 +7,9 @@ cache gems:
- .default-cache
- .default-before_script
- .except-docs
+ stage: test
dependencies: ["setup-test-env"]
+ needs: ["setup-test-env"]
variables:
SETUP_DB: "false"
script:
diff --git a/.gitlab/ci/yaml.gitlab-ci.yml b/.gitlab/ci/yaml.gitlab-ci.yml
index 3e107b475c9..dd61cb3f035 100644
--- a/.gitlab/ci/yaml.gitlab-ci.yml
+++ b/.gitlab/ci/yaml.gitlab-ci.yml
@@ -4,6 +4,7 @@ lint-ci-gitlab:
extends:
- .default-tags
- .default-retry
+ - .except-docs
image: sdesbure/yamllint:latest
dependencies: []
script:
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index 2ba6b68a53e..68f60cb52d4 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -5,7 +5,17 @@
### Intended users
<!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
-Personas can be found at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
+
+* [Parker (Product Manager)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#parker-product-manager)
+* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#delaney-development-team-lead)
+* [Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
+* [Presley (Product Designer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#presley-product-designer)
+* [Devon (DevOps Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#devon-devops-engineer)
+* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sidney-systems-administrator)
+* [Sam (Security Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sam-security-analyst)
+* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
+
+Personas are described at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
### Further details
@@ -17,12 +27,13 @@ Personas can be found at https://about.gitlab.com/handbook/marketing/product-mar
### Permissions and Security
-<!-- What permissions are required to perform the described actions? Are they consistent with the existing permissions as documented for users, groups, and projects as appropriate? Is the proposed behavior consistent between the UI, API, and other access methods (e.g. email replies)? -->
+<!-- What permissions are required to perform the described actions? Are they consistent with the existing permissions as documented for users, groups, and projects as appropriate? Is the proposed behavior consistent between the UI, API, and other access methods (e.g. email replies)?-->
### Documentation
<!-- See the Feature Change Documentation Workflow https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html
-Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#documentation-requirements -->
+Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#documentation-requirements
+If this feature requires changing permissions, this document https://docs.gitlab.com/ee/user/permissions.html must be updated accordingly. -->
### Testing
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index ba9624aeeab..e502614b5ca 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -15,6 +15,7 @@
## Author's checklist
- [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
+- [ ] If applicable, update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html).
- [ ] Link docs to and from the higher-level index page, plus other related docs where helpful.
- [ ] Apply the ~Documentation label.
diff --git a/.markdownlint.json b/.markdownlint.json
index 2c40c0859f0..f06c0766c38 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -8,7 +8,6 @@
"style": "dash"
},
"line-length": false,
- "commands-show-output": false,
"no-duplicate-header": {
"allow_different_nesting": true
},
diff --git a/Dangerfile b/Dangerfile
index 094d55e8652..228190cd530 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,23 +1,12 @@
# frozen_string_literal: true
+
+require_relative 'lib/gitlab_danger'
+
danger.import_plugin('danger/plugins/helper.rb')
danger.import_plugin('danger/plugins/roulette.rb')
unless helper.release_automation?
- danger.import_dangerfile(path: 'danger/metadata')
- danger.import_dangerfile(path: 'danger/changes_size')
- danger.import_dangerfile(path: 'danger/changelog')
- danger.import_dangerfile(path: 'danger/specs')
- danger.import_dangerfile(path: 'danger/gemfile')
- danger.import_dangerfile(path: 'danger/database')
- danger.import_dangerfile(path: 'danger/documentation')
- danger.import_dangerfile(path: 'danger/frozen_string')
- danger.import_dangerfile(path: 'danger/commit_messages')
- danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
- danger.import_dangerfile(path: 'danger/prettier')
- danger.import_dangerfile(path: 'danger/eslint')
- danger.import_dangerfile(path: 'danger/roulette')
- danger.import_dangerfile(path: 'danger/single_codebase')
- danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
- danger.import_dangerfile(path: 'danger/ce_ee_vue_templates')
- danger.import_dangerfile(path: 'danger/only_documentation')
+ GitlabDanger.new(helper.gitlab_helper).rule_names.each do |file|
+ danger.import_dangerfile(path: File.join('danger', file))
+ end
end
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 91951fd8ad7..76d05362056 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.61.0
+1.62.0
diff --git a/Gemfile b/Gemfile
index 3e3a8c5dde2..ac848cce5e8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -106,7 +106,7 @@ gem 'fog-aws', '~> 3.5'
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
# Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0'
-gem 'fog-google', '~> 1.8'
+gem 'fog-google', '~> 1.9'
gem 'fog-local', '~> 0.6'
gem 'fog-openstack', '~> 1.0'
gem 'fog-rackspace', '~> 0.1.1'
@@ -135,7 +135,7 @@ gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 2.0.10'
gem 'asciidoctor-include-ext', '~> 0.3.1', require: false
gem 'asciidoctor-plantuml', '0.0.9'
-gem 'rouge', '~> 3.7'
+gem 'rouge', '~> 3.10'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.4'
@@ -234,7 +234,7 @@ gem 'asana', '~> 0.8.1'
gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
-gem 'kubeclient', '~> 4.2.2'
+gem 'kubeclient', '~> 4.4.0'
# Sanitize user input
gem 'sanitize', '~> 4.6'
@@ -318,6 +318,7 @@ end
group :development do
gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false
+ gem 'danger', '~> 6.0', require: false
gem 'letter_opener_web', '~> 1.3.4'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 6add217bc32..48053e5740e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -140,9 +140,15 @@ GEM
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
+ claide (1.0.3)
+ claide-plugins (0.9.2)
+ cork
+ nap
+ open4 (~> 1.3)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
+ colored2 (3.1.2)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concord (0.1.5)
@@ -151,6 +157,8 @@ GEM
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
contracts (0.11.0)
+ cork (0.3.0)
+ colored2 (~> 3.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
@@ -158,6 +166,19 @@ GEM
css_parser (1.5.0)
addressable
daemons (1.2.6)
+ danger (6.0.9)
+ claide (~> 1.0)
+ claide-plugins (>= 0.9.2)
+ colored2 (~> 3.1)
+ cork (~> 0.1)
+ faraday (~> 0.9)
+ faraday-http-cache (~> 2.0)
+ git (~> 1.5)
+ kramdown (~> 2.0)
+ kramdown-parser-gfm (~> 1.0)
+ no_proxy_fix
+ octokit (~> 4.7)
+ terminal-table (~> 1)
database_cleaner (1.7.0)
debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
@@ -227,6 +248,8 @@ GEM
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
+ faraday-http-cache (2.0.0)
+ faraday (~> 0.8)
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
@@ -261,7 +284,7 @@ GEM
excon (~> 0.58)
formatador (~> 0.2)
mime-types
- fog-google (1.8.2)
+ fog-google (1.9.1)
fog-core (<= 2.1.0)
fog-json (~> 1.2)
fog-xml (~> 0.1.0)
@@ -308,6 +331,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
+ git (1.5.0)
gitaly (1.58.0)
grpc (~> 1.0)
github-markup (1.7.0)
@@ -478,7 +502,10 @@ GEM
kgio (2.11.2)
knapsack (1.17.0)
rake
- kubeclient (4.2.2)
+ kramdown (2.1.0)
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ kubeclient (4.4.0)
http (~> 3.0)
recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0)
@@ -535,10 +562,12 @@ GEM
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
nakayoshi_fork (0.0.4)
+ nap (1.1.0)
net-ldap (0.16.0)
net-ssh (5.2.0)
netrc (0.11.0)
nio4r (2.3.1)
+ no_proxy_fix (0.1.2)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
nokogumbo (1.5.0)
@@ -615,6 +644,7 @@ GEM
addressable (~> 2.5)
omniauth (~> 1.3)
openid_connect (~> 1.1)
+ open4 (1.3.4)
openid_connect (1.1.6)
activemodel
attr_required (>= 1.0.0)
@@ -769,7 +799,7 @@ GEM
retriable (3.1.2)
rinku (2.0.0)
rotp (2.1.2)
- rouge (3.7.0)
+ rouge (3.10.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -926,6 +956,8 @@ GEM
ffi
sysexits (1.2.0)
temple (0.8.0)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
test-prof (0.2.5)
text (1.3.1)
thin (1.7.2)
@@ -1055,6 +1087,7 @@ DEPENDENCIES
concurrent-ruby (~> 1.1)
connection_pool (~> 2.0)
creole (~> 0.5.0)
+ danger (~> 6.0)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.2.0)
default_value_for (~> 3.2.0)
@@ -1081,7 +1114,7 @@ DEPENDENCIES
fog-aliyun (~> 0.3)
fog-aws (~> 3.5)
fog-core (= 2.1.0)
- fog-google (~> 1.8)
+ fog-google (~> 1.9)
fog-local (~> 0.6)
fog-openstack (~> 1.0)
fog-rackspace (~> 0.1.1)
@@ -1131,7 +1164,7 @@ DEPENDENCIES
jwt (~> 2.1.0)
kaminari (~> 1.0)
knapsack (~> 1.17)
- kubeclient (~> 4.2.2)
+ kubeclient (~> 4.4.0)
letter_opener_web (~> 1.3.4)
license_finder (~> 5.4)
licensee (~> 8.9)
@@ -1196,7 +1229,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 3.7)
+ rouge (~> 3.10)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.8.0)
diff --git a/app/assets/javascripts/admin/statistics_panel/components/app.vue b/app/assets/javascripts/admin/statistics_panel/components/app.vue
new file mode 100644
index 00000000000..29077d926cf
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/components/app.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import statisticsLabels from '../constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ data() {
+ return {
+ statisticsLabels,
+ };
+ },
+ computed: {
+ ...mapState(['isLoading', 'statistics']),
+ ...mapGetters(['getStatistics']),
+ },
+ mounted() {
+ this.fetchStatistics();
+ },
+ methods: {
+ ...mapActions(['fetchStatistics']),
+ },
+};
+</script>
+
+<template>
+ <div class="info-well">
+ <div class="well-segment admin-well admin-well-statistics">
+ <h4>{{ __('Statistics') }}</h4>
+ <gl-loading-icon v-if="isLoading" size="md" class="my-3" />
+ <template v-else>
+ <p
+ v-for="statistic in getStatistics(statisticsLabels)"
+ :key="statistic.key"
+ class="js-stats"
+ >
+ {{ statistic.label }}
+ <span class="light float-right">{{ statistic.value }}</span>
+ </p>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/statistics_panel/constants.js b/app/assets/javascripts/admin/statistics_panel/constants.js
new file mode 100644
index 00000000000..2dce19a3894
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/constants.js
@@ -0,0 +1,14 @@
+import { s__ } from '~/locale';
+
+const statisticsLabels = {
+ forks: s__('AdminStatistics|Forks'),
+ issues: s__('AdminStatistics|Issues'),
+ mergeRequests: s__('AdminStatistics|Merge Requests'),
+ notes: s__('AdminStatistics|Notes'),
+ snippets: s__('AdminStatistics|Snippets'),
+ sshKeys: s__('AdminStatistics|SSH Keys'),
+ milestones: s__('AdminStatistics|Milestones'),
+ activeUsers: s__('AdminStatistics|Active Users'),
+};
+
+export default statisticsLabels;
diff --git a/app/assets/javascripts/admin/statistics_panel/index.js b/app/assets/javascripts/admin/statistics_panel/index.js
new file mode 100644
index 00000000000..39112e3ddc0
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import StatisticsPanelApp from './components/app.vue';
+import createStore from './store';
+
+export default function(el) {
+ if (!el) {
+ return false;
+ }
+
+ const store = createStore();
+
+ return new Vue({
+ el,
+ store,
+ components: {
+ StatisticsPanelApp,
+ },
+ render(h) {
+ return h(StatisticsPanelApp);
+ },
+ });
+}
diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js
new file mode 100644
index 00000000000..537025f524c
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js
@@ -0,0 +1,28 @@
+import Api from '~/api';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
+
+export const requestStatistics = ({ commit }) => commit(types.REQUEST_STATISTICS);
+
+export const fetchStatistics = ({ dispatch }) => {
+ dispatch('requestStatistics');
+
+ Api.adminStatistics()
+ .then(({ data }) => {
+ dispatch('receiveStatisticsSuccess', convertObjectPropsToCamelCase(data, { deep: true }));
+ })
+ .catch(error => dispatch('receiveStatisticsError', error));
+};
+
+export const receiveStatisticsSuccess = ({ commit }, statistics) =>
+ commit(types.RECEIVE_STATISTICS_SUCCESS, statistics);
+
+export const receiveStatisticsError = ({ commit }, error) => {
+ commit(types.RECEIVE_STATISTICS_ERROR, error);
+ createFlash(s__('AdminDashboard|Error loading the statistics. Please try again'));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js
new file mode 100644
index 00000000000..24437bc76bf
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js
@@ -0,0 +1,17 @@
+/**
+ * Merges the statisticsLabels with the state's data
+ * and returns an array of the following form:
+ * [{ key: "forks", label: "Forks", value: 50 }]
+ */
+export const getStatistics = state => labels =>
+ Object.keys(labels).map(key => {
+ const result = {
+ key,
+ label: labels[key],
+ value: state.statistics && state.statistics[key] ? state.statistics[key] : null,
+ };
+ return result;
+ });
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/index.js b/app/assets/javascripts/admin/statistics_panel/store/index.js
new file mode 100644
index 00000000000..ece9e6419dd
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/admin/statistics_panel/store/mutation_types.js b/app/assets/javascripts/admin/statistics_panel/store/mutation_types.js
new file mode 100644
index 00000000000..4e0ca4ed3cd
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const REQUEST_STATISTICS = 'REQUEST_STATISTICS';
+export const RECEIVE_STATISTICS_SUCCESS = 'RECEIVE_STATISTICS_SUCCESS';
+export const RECEIVE_STATISTICS_ERROR = 'RECEIVE_STATISTICS_ERROR';
diff --git a/app/assets/javascripts/admin/statistics_panel/store/mutations.js b/app/assets/javascripts/admin/statistics_panel/store/mutations.js
new file mode 100644
index 00000000000..d0fac5cfbab
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/mutations.js
@@ -0,0 +1,16 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_STATISTICS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_STATISTICS_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.error = null;
+ state.statistics = data;
+ },
+ [types.RECEIVE_STATISTICS_ERROR](state, error) {
+ state.isLoading = false;
+ state.error = error;
+ },
+};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/state.js b/app/assets/javascripts/admin/statistics_panel/store/state.js
new file mode 100644
index 00000000000..f2f2dc0a4d2
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ error: null,
+ isLoading: false,
+ statistics: null,
+});
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 136ffdf8b9d..1d97ad5ec11 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
+ adminStatisticsPath: 'api/:version/application/statistics',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -376,6 +377,11 @@ const Api = {
return axios.get(url);
},
+ adminStatistics() {
+ const url = Api.buildUrl(this.adminStatisticsPath);
+ return axios.get(url);
+ },
+
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 7e3515b1f4b..66cb9fd7672 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -46,7 +46,6 @@ export default class Shortcuts {
$(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) {
$(this).remove();
- $('.hidden-shortcut').show();
e.preventDefault();
});
}
@@ -104,7 +103,6 @@ export default class Shortcuts {
return results;
}
- $('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
});
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index c8eb96a625c..f7b327b2af1 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -6,7 +6,7 @@ import { CopyAsGFM } from '../markdown/copy_as_gfm';
import { getSelectedFragment } from '~/lib/utils/common_utils';
export default class ShortcutsIssuable extends Shortcuts {
- constructor(isMergeRequest) {
+ constructor() {
super();
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
@@ -14,12 +14,6 @@ export default class ShortcutsIssuable extends Shortcuts {
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
-
- if (isMergeRequest) {
- this.enabledHelp.push('.hidden-shortcut.merge_requests');
- } else {
- this.enabledHelp.push('.hidden-shortcut.issues');
- }
}
static replyWithSelectedText() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index bef1553703b..b46b4132ba8 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -23,7 +23,5 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
-
- this.enabledHelp.push('.hidden-shortcut.project');
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
index a88c280fa3b..3e791e4673a 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
@@ -11,7 +11,5 @@ export default class ShortcutsNetwork extends ShortcutsNavigation {
Mousetrap.bind(['down', 'j'], graph.scrollDown);
Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
-
- this.enabledHelp.push('.hidden-shortcut.network');
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 208c91a1f08..8b7e6a56d25 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -6,8 +6,6 @@ export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
super();
Mousetrap.bind('e', ShortcutsWiki.editWiki);
-
- this.enabledHelp.push('.hidden-shortcut.wiki');
}
static editWiki() {
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index f9284266b72..f9a08f151c5 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -2,9 +2,9 @@
import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
-import ListIssue from '../models/issue';
import boardsStore from '../stores/boards_store';
export default {
@@ -54,6 +54,9 @@ export default {
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
+ const { weightFeatureAvailable } = boardsStore;
+ const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {};
+
const issue = new ListIssue({
title: this.title,
labels,
@@ -61,6 +64,7 @@ export default {
assignees,
milestone,
project_id: this.selectedProject.id,
+ weight,
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 7296426549a..ebb2f5b23e4 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -245,6 +245,7 @@ export default {
<div
v-if="!loading"
ref="content"
+ data-qa-selector="boards_dropdown_content"
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 970f5a7b297..27959898fb7 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -397,7 +397,6 @@ export default {
</div>
</application-row>
<application-row
- v-if="isProjectCluster"
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
@@ -459,7 +458,6 @@ export default {
</div>
</application-row>
<application-row
- v-if="isProjectCluster"
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 43ae54133af..b1d568532a6 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -38,6 +38,10 @@ export default {
type: String,
required: true,
},
+ userCanEnableErrorTracking: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapState(['errors', 'externalUrl', 'loading']),
@@ -111,14 +115,26 @@ export default {
</gl-table>
</div>
</div>
- <div v-else>
+ <div v-else-if="userCanEnableErrorTracking">
<gl-empty-state
:title="__('Get started with error tracking')"
- :description="__('Monitor your errors by integrating with Sentry')"
+ :description="__('Monitor your errors by integrating with Sentry.')"
:primary-button-text="__('Enable error tracking')"
:primary-button-link="enableErrorTrackingLink"
:svg-path="illustrationPath"
/>
</div>
+ <div v-else>
+ <gl-empty-state :title="__('Get started with error tracking')" :svg-path="illustrationPath">
+ <template #description>
+ <div>
+ <span>{{ __('Monitor your errors by integrating with Sentry.') }}</span>
+ <a href="/help/user/project/operations/error_tracking.html">
+ {{ __('More information') }}
+ </a>
+ </div>
+ </template>
+ </gl-empty-state>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js
index 3d609448efe..073e2c8f1c7 100644
--- a/app/assets/javascripts/error_tracking/index.js
+++ b/app/assets/javascripts/error_tracking/index.js
@@ -14,9 +14,10 @@ export default () => {
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset;
- let { errorTrackingEnabled } = domEl.dataset;
+ let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset;
errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
+ userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking);
return createElement('error-tracking-list', {
props: {
@@ -24,6 +25,7 @@ export default () => {
enableErrorTrackingLink,
errorTrackingEnabled,
illustrationPath,
+ userCanEnableErrorTracking,
},
});
},
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 69159e2d741..37721cd030c 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -10,21 +10,18 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use(config => {
window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1;
-
return config;
});
// Remove the global counter
axios.interceptors.response.use(
- config => {
+ response => {
window.activeVueResources -= 1;
-
- return config;
+ return response;
},
- e => {
+ err => {
window.activeVueResources -= 1;
-
- return Promise.reject(e);
+ return Promise.reject(err);
},
);
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 0f5c5b3d60f..72ddd8d4fcf 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -1,5 +1,10 @@
<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
export default {
+ components: {
+ Icon,
+ },
props: {
name: {
type: String,
@@ -15,15 +20,42 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ showGroup: true,
+ };
+ },
+ computed: {
+ caretIcon() {
+ return this.collapseGroup && this.showGroup ? 'angle-down' : 'angle-right';
+ },
+ },
+ created() {
+ this.showGroup = this.collapseGroup;
+ },
+ methods: {
+ collapse() {
+ this.showGroup = !this.showGroup;
+ },
+ },
};
</script>
<template>
<div v-if="showPanels" class="card prometheus-panel">
- <div class="card-header">
- <h4>{{ name }}</h4>
+ <div class="card-header d-flex align-items-center">
+ <h4 class="flex-grow-1">{{ name }}</h4>
+ <a role="button" @click="collapse">
+ <icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" />
+ </a>
+ </div>
+ <div
+ v-if="collapseGroup"
+ v-show="collapseGroup && showGroup"
+ class="card-body prometheus-graph-group"
+ >
+ <slot></slot>
</div>
- <div v-if="collapseGroup" class="card-body prometheus-graph-group"><slot></slot></div>
</div>
<div v-else class="prometheus-graph-group"><slot></slot></div>
</template>
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index 8a32556f06c..74f2eead755 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,3 +1,8 @@
import initAdmin from './admin';
+import initAdminStatisticsPanel from '../../admin/statistics_panel/index';
-document.addEventListener('DOMContentLoaded', initAdmin());
+document.addEventListener('DOMContentLoaded', () => {
+ const statisticsPanelContainer = document.getElementById('js-admin-statistics-container');
+ initAdmin();
+ initAdminStatisticsPanel(statisticsPanelContainer);
+});
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ba0dea626dc..27c1b639889 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -20,6 +20,9 @@ export default {
<stage-column-component
v-for="(stage, index) in graph"
:key="stage.name"
+ :class="{
+ 'append-right-48': shouldAddRightMargin(index),
+ }"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
index 66e9476dadf..f383a4b3368 100644
--- a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
@@ -40,5 +40,15 @@ export default {
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
+ /**
+ * CSS class is applied:
+ * - if pipeline graph contains only one stage column component
+ *
+ * @param {number} index
+ * @returns {boolean}
+ */
+ shouldAddRightMargin(index) {
+ return !(index === this.graph.length - 1);
+ },
},
};
diff --git a/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue
new file mode 100644
index 00000000000..b649dac029a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlToggle } from '@gitlab/ui';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+export default {
+ name: 'GlToggleVuex',
+ components: {
+ GlToggle,
+ },
+ props: {
+ stateProperty: {
+ type: String,
+ required: true,
+ },
+ storeModule: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ setAction: {
+ type: String,
+ required: false,
+ default() {
+ return `set${capitalizeFirstCharacter(this.stateProperty)}`;
+ },
+ },
+ },
+ computed: {
+ value: {
+ get() {
+ const { state } = this.$store;
+ const { stateProperty, storeModule } = this;
+ return storeModule ? state[storeModule][stateProperty] : state[stateProperty];
+ },
+ set(value) {
+ const { stateProperty, storeModule, setAction } = this;
+ const action = storeModule ? `${storeModule}/${setAction}` : setAction;
+ this.$store.dispatch(action, { key: stateProperty, value });
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-toggle v-model="value">
+ <slot v-bind="{ value }"></slot>
+ </gl-toggle>
+</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index e9218dcec67..b95978b6966 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -395,6 +395,7 @@ img.emoji {
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
.prepend-left-32 { margin-left: 32px; }
+.prepend-left-64 { margin-left: 64px; }
.append-right-4 { margin-right: 4px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
@@ -402,6 +403,8 @@ img.emoji {
.append-right-15 { margin-right: 15px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
+.append-right-32 { margin-right: 32px; }
+.append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-4 { margin-bottom: $gl-padding-4; }
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index fd9a75bc5b6..9c924559135 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -2,6 +2,12 @@
max-width: 98%;
}
+.modal-1040 {
+ @include media-breakpoint-up(xl) {
+ max-width: 1040px;
+ }
+}
+
.modal-header {
background-color: $modal-body-bg;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 5f4db37c317..d4bd5b1b7dc 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -476,10 +476,6 @@
display: inline-block;
vertical-align: top;
- &:not(:last-child) {
- margin-right: 44px;
- }
-
&.left-margin {
&:not(:first-child) {
margin-left: 44px;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 79de1d78a6e..416537ef763 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -24,12 +24,16 @@
.settings {
// border-top for each item except the top one
- + .settings {
- border-top: 1px solid $border-color;
- }
+ border-top: 1px solid $border-color;
&:first-of-type {
margin-top: 10px;
+ border: 0;
+ }
+
+ + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid $border-color;
}
&.animating {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 99411641874..f2f72bea5b4 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -85,7 +85,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
+ # TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204)
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
+ params.delete(:domain_blacklist_raw) if params[:domain_blacklist]
+ params.delete(:domain_whitelist_raw) if params[:domain_whitelist]
params.require(:application_setting).permit(
visible_application_setting_attributes
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 23cc9ee247a..64b959e2431 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -3,8 +3,7 @@
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
- COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
- MergeRequest, Note, Snippet, Key, Milestone].freeze
+ COUNTED_ITEMS = [Project, User, Group].freeze
# rubocop: disable CodeReuse/ActiveRecord
def index
diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb
index ef42f7c4074..188805c6106 100644
--- a/app/controllers/clusters/base_controller.rb
+++ b/app/controllers/clusters/base_controller.rb
@@ -31,6 +31,10 @@ class Clusters::BaseController < ApplicationController
access_denied! unless can?(current_user, :create_cluster, clusterable)
end
+ def authorize_read_prometheus!
+ access_denied! unless can?(current_user, :read_prometheus, clusterable)
+ end
+
def clusterable
raise NotImplementedError
end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index ec8077d18e3..bcd771dafcf 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -35,6 +35,12 @@ class Clusters::ClustersController < Clusters::BaseController
end
def new
+ return unless Feature.enabled?(:create_eks_clusters)
+
+ @gke_selected = params[:provider] == 'gke'
+ @eks_selected = params[:provider] == 'eks'
+
+ return redirect_to @authorize_url if @gke_selected && @authorize_url && !@valid_gcp_token
end
# Overridding ActionController::Metal#status is NOT a good idea
@@ -99,7 +105,7 @@ class Clusters::ClustersController < Clusters::BaseController
validate_gcp_token
user_cluster
- render :new, locals: { active_tab: 'gcp' }
+ render :new, locals: { active_tab: 'create' }
end
end
@@ -116,7 +122,7 @@ class Clusters::ClustersController < Clusters::BaseController
validate_gcp_token
gcp_cluster
- render :new, locals: { active_tab: 'user' }
+ render :new, locals: { active_tab: 'add' }
end
end
@@ -189,7 +195,8 @@ class Clusters::ClustersController < Clusters::BaseController
end
def generate_gcp_authorize_url
- state = generate_session_key_redirect(clusterable.new_path.to_s)
+ params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {}
+ state = generate_session_key_redirect(clusterable.new_path(params).to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index e0df51590ae..c9f680a4696 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -18,10 +18,23 @@ class Projects::ServicesController < Projects::ApplicationController
def update
@service.attributes = service_params[:service]
- if @service.save(context: :manual_change)
- redirect_to(project_settings_integrations_path(@project), notice: success_message)
- else
- render 'edit'
+ saved = @service.save(context: :manual_change)
+
+ respond_to do |format|
+ format.html do
+ if saved
+ redirect_to project_settings_integrations_path(@project),
+ notice: success_message
+ else
+ render 'edit'
+ end
+ end
+
+ format.json do
+ status = saved ? :ok : :unprocessable_entity
+
+ render json: serialize_as_json, status: status
+ end
end
end
@@ -67,4 +80,10 @@ class Projects::ServicesController < Projects::ApplicationController
def ensure_service_enabled
render_404 unless service
end
+
+ def serialize_as_json
+ @service
+ .as_json(only: @service.json_fields)
+ .merge(errors: @service.errors.as_json)
+ end
end
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 5cfb0ac307d..ec89bb89edc 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -3,7 +3,7 @@
module Projects
module Settings
class OperationsController < Projects::ApplicationController
- before_action :authorize_update_environment!
+ before_action :authorize_admin_operations!
helper_method :error_tracking_setting
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index e773ec09924..fb631f09f10 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -20,6 +20,7 @@ class RegistrationsController < Devise::RegistrationsController
super do |new_user|
persist_accepted_terms_if_required(new_user)
+ yield new_user if block_given?
end
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b735f9ff3b8..8ed6ff56e2b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -193,15 +193,30 @@ class IssuableFinder
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects(min_access_level)
- elsif group
- find_group_projects
else
- Project.public_or_visible_to_user(current_user, min_access_level)
+ projects_public_or_visible_to_user
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
end
+ def projects_public_or_visible_to_user
+ projects =
+ if group
+ if params[:projects]
+ find_group_projects.id_in(params[:projects])
+ else
+ find_group_projects
+ end
+ elsif params[:projects]
+ Project.id_in(params[:projects])
+ else
+ Project
+ end
+
+ projects.public_or_visible_to_user(current_user, min_access_level)
+ end
+
def find_group_projects
return Project.none unless group
@@ -209,7 +224,7 @@ class IssuableFinder
Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
else
group.projects
- end.public_or_visible_to_user(current_user, min_access_level)
+ end
end
def search
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index e9a4ea9157b..993d33c4fc2 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -16,7 +16,7 @@ module Types
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
- :create_pages, :destroy_pages, :read_pages_content
+ :create_pages, :destroy_pages, :read_pages_content, :admin_operations
end
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 84021d0da56..b1a6e988a1d 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -180,8 +180,12 @@ module ApplicationSettingsHelper
:default_projects_limit,
:default_snippet_visibility,
:disabled_oauth_sign_in_sources,
+ :domain_blacklist,
:domain_blacklist_enabled,
+ # TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204)
:domain_blacklist_raw,
+ :domain_whitelist,
+ # TODO Remove domain_whitelist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204)
:domain_whitelist_raw,
:outbound_local_requests_whitelist_raw,
:dsa_key_restriction,
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index e990e425cb6..09866ca75ff 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -103,7 +103,7 @@ module EventsHelper
words << "at"
end
- words << event.project_name
+ words << event.resource_parent_name
words.join(" ")
end
@@ -223,3 +223,5 @@ module EventsHelper
end
end
end
+
+EventsHelper.prepend_if_ee('EE::EventsHelper')
diff --git a/app/helpers/external_link_helper.rb b/app/helpers/external_link_helper.rb
new file mode 100644
index 00000000000..9dbad1f5032
--- /dev/null
+++ b/app/helpers/external_link_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ExternalLinkHelper
+ def external_link(body, url, options = {})
+ link_to url, { target: '_blank', rel: 'noopener noreferrer' }.merge(options) do
+ "#{body} #{icon('external-link')}".html_safe
+ end
+ end
+end
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
index 6daf2e21ca2..fd1222a1dfb 100644
--- a/app/helpers/projects/error_tracking_helper.rb
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -1,12 +1,13 @@
# frozen_string_literal: true
module Projects::ErrorTrackingHelper
- def error_tracking_data(project)
+ def error_tracking_data(current_user, project)
error_tracking_enabled = !!project.error_tracking_setting&.enabled?
{
'index-path' => project_error_tracking_index_path(project,
format: :json),
+ 'user-can-enable-error-tracking' => can?(current_user, :admin_operations, project).to_s,
'enable-error-tracking-link' => project_settings_operations_path(project),
'error-tracking-enabled' => error_tracking_enabled.to_s,
'illustration-path' => image_path('illustrations/cluster_popover.svg')
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
new file mode 100644
index 00000000000..4d9fe345edf
--- /dev/null
+++ b/app/helpers/releases_helper.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ReleasesHelper
+ IMAGE_PATH = 'illustrations/releases.svg'
+ DOCUMENTATION_PATH = 'user/project/releases/index'
+
+ def illustration
+ image_path(IMAGE_PATH)
+ end
+
+ def help_page
+ help_page_path(DOCUMENTATION_PATH)
+ end
+
+ def url_for_merge_requests
+ project_merge_requests_url(@project, params_for_issue_and_mr_paths)
+ end
+
+ def url_for_issues
+ project_issues_url(@project, params_for_issue_and_mr_paths)
+ end
+
+ def data_for_releases_page
+ {
+ project_id: @project.id,
+ illustration_path: illustration,
+ documentation_path: help_page,
+ merge_requests_url: url_for_merge_requests,
+ issues_url: url_for_issues
+ }
+ end
+
+ private
+
+ def params_for_issue_and_mr_paths
+ { scope: 'all', state: 'opened' }
+ end
+end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index d5e459311f7..f10fadfdf49 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
module UserCalloutsHelper
- GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
- GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
- SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'.freeze
+ GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'
+ SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d558f66154e..72782827906 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -88,6 +88,7 @@ module Ci
validates :coverage, numericality: true, allow_blank: true
validates :ref, presence: true
+ scope :not_interruptible, -> { joins(:metadata).where(ci_builds_metadata: { interruptible: false }) }
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2b6f10ef79f..d2271c1335c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -23,6 +23,7 @@ module Ci
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
+ belongs_to :external_pull_request
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
@@ -64,6 +65,11 @@ module Ci
validates :merge_request, presence: { if: :merge_request_event? }
validates :merge_request, absence: { unless: :merge_request_event? }
validates :tag, inclusion: { in: [false], if: :merge_request_event? }
+
+ validates :external_pull_request, presence: { if: :external_pull_request_event? }
+ validates :external_pull_request, absence: { unless: :external_pull_request_event? }
+ validates :tag, inclusion: { in: [false], if: :external_pull_request_event? }
+
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
@@ -225,6 +231,14 @@ module Ci
where('EXISTS (?)', ::Ci::Build.latest.with_reports(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1))
end
+ scope :without_interruptible_builds, -> do
+ where('NOT EXISTS (?)',
+ Ci::Build.where('ci_builds.commit_id = ci_pipelines.id')
+ .with_status(:running, :success, :failed)
+ .not_interruptible
+ )
+ end
+
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
@@ -675,6 +689,10 @@ module Ci
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
variables.concat(merge_request.predefined_variables)
end
+
+ if external_pull_request_event? && external_pull_request
+ variables.concat(external_pull_request.predefined_variables)
+ end
end
end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 571c4271475..0c2bd0aa8eb 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -23,7 +23,8 @@ module Ci
api: 5,
external: 6,
chat: 8,
- merge_request_event: 10
+ merge_request_event: 10,
+ external_pull_request_event: 11
}
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index fb74d96efe3..ec65482a846 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -85,7 +85,8 @@ module Clusters
"clientId" => oauth_application.uid,
"clientSecret" => oauth_application.secret,
"callbackUrl" => callback_url,
- "gitlabProjectIdWhitelist" => [project_id]
+ "gitlabProjectIdWhitelist" => cluster.projects.ids,
+ "gitlabGroupWhitelist" => cluster.groups.map(&:to_param)
}
},
"singleuser" => {
@@ -101,10 +102,6 @@ module Clusters
@crypto_key ||= SecureRandom.hex(32)
end
- def project_id
- cluster&.project&.id
- end
-
def gitlab_url
Gitlab.config.gitlab.url
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 444e1a82c97..a976093ac0c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -10,15 +10,15 @@ module Clusters
self.table_name = 'clusters'
PROJECT_ONLY_APPLICATIONS = {
- Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative
}.freeze
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
Applications::CertManager.application_name => Applications::CertManager,
+ Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
- Applications::Prometheus.application_name => Applications::Prometheus
+ Applications::Jupyter.application_name => Applications::Jupyter,
+ Applications::Knative.application_name => Applications::Knative
}.merge(PROJECT_ONLY_APPLICATIONS).freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 304cc71e9dc..a0ca8a34c6d 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -15,6 +15,7 @@ module Ci
autosave: true
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
before_create :ensure_metadata
end
@@ -50,6 +51,14 @@ module Ci
write_metadata_attribute(:yaml_variables, :config_variables, value)
end
+ def interruptible
+ metadata&.interruptible
+ end
+
+ def interruptible=(value)
+ ensure_metadata.interruptible = value
+ end
+
private
def read_metadata_attribute(legacy_key, metadata_key, default_value = nil)
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index cf88076ac74..bcbbb27a9a8 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -102,6 +102,7 @@ module HasStatus
scope :manual, -> { with_status(:manual) }
scope :scheduled, -> { with_status(:scheduled) }
scope :alive, -> { with_status(:created, :preparing, :pending, :running) }
+ scope :alive_or_scheduled, -> { with_status(:created, :preparing, :pending, :running, :scheduled) }
scope :created_or_pending, -> { with_status(:created, :pending) }
scope :running_or_pending, -> { with_status(:running, :pending) }
scope :finished, -> { with_status(:success, :failed, :canceled) }
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 07d22641a0a..8b011bca72c 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -33,7 +33,7 @@ module Routable
#
# Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false)
- increment_counter(:routable_find_by_full_path, 'Number of calls to Routable.find_by_full_path')
+ routable_calls_counter.increment(method: 'find_by_full_path')
if Feature.enabled?(:routable_two_step_lookup)
# Case sensitive match first (it's cheaper and the usual case)
@@ -61,7 +61,7 @@ module Routable
def where_full_path_in(paths)
return none if paths.empty?
- increment_counter(:routable_where_full_path_in, 'Number of calls to Routable.where_full_path_in')
+ routable_calls_counter.increment(method: 'where_full_path_in')
wheres = paths.map do |path|
"(LOWER(routes.path) = LOWER(#{connection.quote(path)}))"
@@ -71,12 +71,8 @@ module Routable
end
# Temporary instrumentation of method calls
- def increment_counter(counter, description)
- @counters[counter] ||= Gitlab::Metrics.counter(counter, description)
-
- @counters[counter].increment
- rescue
- # ignore the error
+ def routable_calls_counter
+ @routable_calls_counter ||= Gitlab::Metrics.counter(:gitlab_routable_calls_total, 'Number of calls to Routable by method')
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 52d54be39a9..580bb770599 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -3,6 +3,8 @@
class Event < ApplicationRecord
include Sortable
include FromUnion
+ include Presentable
+
default_scope { reorder(nil) }
CREATED = 1
@@ -135,6 +137,10 @@ class Event < ApplicationRecord
end
end
+ def present
+ super(presenter_class: ::EventPresenter)
+ end
+
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def visible_to_user?(user = nil)
@@ -161,12 +167,8 @@ class Event < ApplicationRecord
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
- def project_name
- if project
- project.full_name
- else
- "(deleted project)"
- end
+ def resource_parent
+ project || group
end
def target_title
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
new file mode 100644
index 00000000000..65ae8d95500
--- /dev/null
+++ b/app/models/external_pull_request.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+# This model stores pull requests coming from external providers, such as
+# GitHub, when GitLab project is set as CI/CD only and remote mirror.
+#
+# When setting up a remote mirror with GitHub we subscribe to push and
+# pull_request webhook events. When a pull request is opened on GitHub,
+# a webhook is sent out, we create or update the status of the pull
+# request locally.
+#
+# When the mirror is updated and changes are pushed to branches we check
+# if there are open pull requests for the source and target branch.
+# If so, we create pipelines for external pull requests.
+class ExternalPullRequest < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+ include ShaAttribute
+
+ belongs_to :project
+
+ sha_attribute :source_sha
+ sha_attribute :target_sha
+
+ validates :source_branch, presence: true
+ validates :target_branch, presence: true
+ validates :source_sha, presence: true
+ validates :target_sha, presence: true
+ validates :source_repository, presence: true
+ validates :target_repository, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ open: 1,
+ closed: 2
+ }
+
+ # We currently don't support pull requests from fork, so
+ # we are going to return an error to the webhook
+ validate :not_from_fork
+
+ scope :by_source_branch, ->(branch) { where(source_branch: branch) }
+ scope :by_source_repository, -> (repository) { where(source_repository: repository) }
+
+ def self.create_or_update_from_params(params)
+ find_params = params.slice(:project_id, :source_branch, :target_branch)
+
+ safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
+ yield(pull_request) if block_given?
+ end
+ end
+
+ def actual_branch_head?
+ actual_source_branch_sha == source_sha
+ end
+
+ def from_fork?
+ source_repository != target_repository
+ end
+
+ def source_ref
+ Gitlab::Git::BRANCH_REF_PREFIX + source_branch
+ end
+
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
+ end
+ end
+
+ private
+
+ def actual_source_branch_sha
+ project.commit(source_ref)&.sha
+ end
+
+ def not_from_fork
+ if from_fork?
+ errors.add(:base, 'Pull requests from fork are not supported')
+ end
+ end
+
+ def self.safe_find_or_initialize_and_update(find:, update:)
+ safe_ensure_unique(retries: 1) do
+ model = find_or_initialize_by(find)
+
+ if model.update(update)
+ yield(model) if block_given?
+ end
+
+ model
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 75d4fc8c1c5..7c5a139ab55 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -31,7 +31,7 @@ class Issue < ApplicationRecord
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 28e450f9b30..901ebcf249f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -54,7 +54,7 @@ class MergeRequest < ApplicationRecord
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
@@ -174,6 +174,7 @@ class MergeRequest < ApplicationRecord
scope :from_project, ->(project) { where(source_project_id: project.id) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
+ scope :open_and_closed, -> { with_states(:opened, :closed) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
scope :by_commit_sha, ->(sha) do
where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
@@ -187,6 +188,11 @@ class MergeRequest < ApplicationRecord
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
}
+ scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
+ where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
+ end
+ scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
+ scope :preload_source_project, -> { preload(:source_project) }
after_save :keep_around_commit
@@ -1232,9 +1238,9 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::CompareTestReportsService)
end
- def compare_reports(service_class)
+ def compare_reports(service_class, current_user = nil)
with_reactive_cache(service_class.name) do |data|
- unless service_class.new(project)
+ unless service_class.new(project, current_user)
.latest?(base_pipeline, actual_head_pipeline, data)
raise InvalidateReactiveCache
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 915978d37b8..7f46e5faf1a 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -37,7 +37,7 @@ class Milestone < ApplicationRecord
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
diff --git a/app/models/note.rb b/app/models/note.rb
index 0d024b0a25c..5bd3a7f969a 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -78,7 +78,7 @@ class Note < ApplicationRecord
# suggestions.delete_all calls
has_many :suggestions, -> { order(:relative_order) },
inverse_of: :note, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 12ce717efd7..a2a471074a9 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -17,7 +17,7 @@ class PagesDomain < ApplicationRecord
validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? }
validates :key, presence: { message: 'must be present if HTTPS-only is enabled' },
if: :certificate_should_be_present?
- validates :key, certificate_key: true, if: ->(domain) { domain.key.present? }
+ validates :key, certificate_key: true, named_ecdsa_key: true, if: ->(domain) { domain.key.present? }
validates :verification_code, presence: true, allow_blank: false
validate :validate_pages_domain
@@ -247,7 +247,7 @@ class PagesDomain < ApplicationRecord
def pkey
return unless key
- @pkey ||= OpenSSL::PKey::RSA.new(key)
+ @pkey ||= OpenSSL::PKey.read(key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 17b52d0578e..d948410e397 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -291,6 +291,8 @@ class Project < ApplicationRecord
has_many :remote_mirrors, inverse_of: :project
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
+ has_many :external_pull_requests, inverse_of: :project
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index ee0c94c20af..9fd929371f8 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -3,6 +3,9 @@
class ProtectedBranch < ApplicationRecord
include ProtectedRef
+ scope :requiring_code_owner_approval,
+ -> { where(code_owner_approval_required: true) }
+
protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
diff --git a/app/models/service.rb b/app/models/service.rb
index 431c5881460..d866a51c42e 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -107,6 +107,13 @@ class Service < ApplicationRecord
[]
end
+ # Expose a list of fields in the JSON endpoint.
+ #
+ # This list is used in `Service#as_json(only: json_fields)`.
+ def json_fields
+ %w(active)
+ end
+
def test_data(project, user)
Gitlab::DataBuilder::Push.build_sample(project, user)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 67d730e2fa3..5f109feb96a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -131,7 +131,7 @@ class User < ApplicationRecord
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
- has_many :events, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, dependent: :delete_all, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :releases, dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb
index bd7ff413afe..c8e6c973bf5 100644
--- a/app/policies/clusters/instance_policy.rb
+++ b/app/policies/clusters/instance_policy.rb
@@ -8,6 +8,7 @@ module Clusters
enable :create_cluster
enable :update_cluster
enable :admin_cluster
+ enable :read_prometheus
end
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index e2634692dc7..5c36b59f07b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -294,6 +294,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_release
enable :destroy_artifacts
enable :daily_statistics
+ enable :admin_operations
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index d1bf0344b66..49c64b31fc7 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -25,8 +25,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters])
end
- def new_path
- new_polymorphic_path([clusterable, :cluster])
+ def new_path(options = {})
+ new_polymorphic_path([clusterable, :cluster], options)
end
def create_user_clusters_path
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
new file mode 100644
index 00000000000..f31d362d5fa
--- /dev/null
+++ b/app/presenters/event_presenter.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class EventPresenter < Gitlab::View::Presenter::Delegated
+ presents :event
+
+ def resource_parent_name
+ resource_parent&.full_name || ''
+ end
+
+ def target_link_options
+ case resource_parent
+ when Group
+ [event.group, event.target]
+ when Project
+ [event.project.namespace.becomes(Namespace), event.project, event.target]
+ else
+ ''
+ end
+ end
+end
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index f8bbe5216f1..cce400ad2a1 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -18,8 +18,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
end
override :new_path
- def new_path
- new_admin_cluster_path
+ def new_path(options = {})
+ new_admin_cluster_path(options)
end
override :cluster_status_cluster_path
diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb
index e22be6880bb..9504fdd8eac 100644
--- a/app/serializers/merge_request_noteable_entity.rb
+++ b/app/serializers/merge_request_noteable_entity.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class MergeRequestNoteableEntity < Grape::Entity
+class MergeRequestNoteableEntity < IssuableEntity
include RequestAwareEntity
# Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 29317f1176e..4a7f62de9e1 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -18,7 +18,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
- def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block)
+ # rubocop: disable Metrics/ParameterLists
+ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
@@ -32,6 +33,7 @@ module Ci
trigger_request: trigger_request,
schedule: schedule,
merge_request: merge_request,
+ external_pull_request: external_pull_request,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
@@ -62,6 +64,7 @@ module Ci
pipeline
end
+ # rubocop: enable Metrics/ParameterLists
def execute!(*args, &block)
execute(*args, &block).tap do |pipeline|
@@ -91,11 +94,21 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def auto_cancelable_pipelines
- project.ci_pipelines
- .where(ref: pipeline.ref)
- .where.not(id: pipeline.id)
- .where.not(sha: project.commit(pipeline.ref).try(:id))
- .created_or_pending
+ # TODO: Introduced by https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23464
+ if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
+ project.ci_pipelines
+ .where(ref: pipeline.ref)
+ .where.not(id: pipeline.id)
+ .where.not(sha: project.commit(pipeline.ref).try(:id))
+ .alive_or_scheduled
+ .without_interruptible_builds
+ else
+ project.ci_pipelines
+ .where(ref: pipeline.ref)
+ .where.not(id: pipeline.id)
+ .where.not(sha: project.commit(pipeline.ref).try(:id))
+ .created_or_pending
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/external_pull_requests/create_pipeline_service.rb b/app/services/external_pull_requests/create_pipeline_service.rb
new file mode 100644
index 00000000000..36411465ff1
--- /dev/null
+++ b/app/services/external_pull_requests/create_pipeline_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# This service is responsible for creating a pipeline for a given
+# ExternalPullRequest coming from other providers such as GitHub.
+
+module ExternalPullRequests
+ class CreatePipelineService < BaseService
+ def execute(pull_request)
+ return unless pull_request.open? && pull_request.actual_branch_head?
+
+ create_pipeline_for(pull_request)
+ end
+
+ private
+
+ def create_pipeline_for(pull_request)
+ Ci::CreatePipelineService.new(project, current_user, create_params(pull_request))
+ .execute(:external_pull_request_event, external_pull_request: pull_request)
+ end
+
+ def create_params(pull_request)
+ {
+ ref: pull_request.source_ref,
+ source_sha: pull_request.source_sha,
+ target_sha: pull_request.target_sha
+ }
+ end
+ end
+end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 47c308c8280..35a4d2015fa 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -57,7 +57,9 @@ module Git
Ci::CreatePipelineService
.new(project, current_user, pipeline_params)
- .execute(:push, pipeline_options)
+ .execute!(:push, pipeline_options)
+ rescue Ci::CreatePipelineService::CreateError => ex
+ log_pipeline_errors(ex)
end
def execute_project_hooks
@@ -125,5 +127,29 @@ module Git
project.mark_stuck_remote_mirrors_as_failed!
project.update_remote_mirrors
end
+
+ def log_pipeline_errors(exception)
+ data = {
+ class: self.class.name,
+ correlation_id: Labkit::Correlation::CorrelationId.current_id.to_s,
+ project_id: project.id,
+ project_path: project.full_path,
+ message: "Error creating pipeline",
+ errors: exception.to_s,
+ pipeline_params: pipeline_params
+ }
+
+ logger.warn(data)
+ end
+
+ def logger
+ if Sidekiq.server?
+ Sidekiq.logger
+ else
+ # This service runs in Sidekiq, so this shouldn't ever be
+ # called, but this is included just in case.
+ Gitlab::ProjectServiceLogger
+ end
+ end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3555864f834..900e5063621 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -76,13 +76,16 @@ class IssuableBaseService < BaseService
end
def filter_labels
- params[:add_label_ids] = labels_service.filter_labels_ids_in_param(:add_label_ids) if params[:add_label_ids]
- params[:remove_label_ids] = labels_service.filter_labels_ids_in_param(:remove_label_ids) if params[:remove_label_ids]
+ label_ids_to_filter(:add_label_ids, :add_labels, false)
+ label_ids_to_filter(:remove_label_ids, :remove_labels, true)
+ label_ids_to_filter(:label_ids, :labels, false)
+ end
- if params[:label_ids]
- params[:label_ids] = labels_service.filter_labels_ids_in_param(:label_ids)
- elsif params[:labels]
- params[:label_ids] = labels_service.find_or_create_by_titles.map(&:id)
+ def label_ids_to_filter(label_id_key, label_key, find_only)
+ if params[label_id_key]
+ params[label_id_key] = labels_service.filter_labels_ids_in_param(label_id_key)
+ elsif params[label_key]
+ params[label_id_key] = labels_service.find_or_create_by_titles(label_key, find_only: find_only).map(&:id)
end
end
diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb
index fe477d96970..8886e58d6ef 100644
--- a/app/services/labels/available_labels_service.rb
+++ b/app/services/labels/available_labels_service.rb
@@ -9,8 +9,8 @@ module Labels
@params = params
end
- def find_or_create_by_titles
- labels = params.delete(:labels)
+ def find_or_create_by_titles(key = :labels, find_only: false)
+ labels = params.delete(key)
return [] unless labels
@@ -23,7 +23,7 @@ module Labels
include_ancestor_groups: true,
title: label_name.strip,
available_labels: available_labels
- ).execute
+ ).execute(find_only: find_only)
label
end.compact
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index 628873519d7..a47dd42aea0 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -9,9 +9,9 @@ module Labels
@params = params.dup.with_indifferent_access
end
- def execute(skip_authorization: false)
+ def execute(skip_authorization: false, find_only: false)
@skip_authorization = skip_authorization
- find_or_create_label
+ find_or_create_label(find_only: find_only)
end
private
@@ -30,9 +30,11 @@ module Labels
# Only creates the label if current_user can do so, if the label does not exist
# and the user can not create the label, nil is returned
# rubocop: disable CodeReuse/ActiveRecord
- def find_or_create_label
+ def find_or_create_label(find_only: false)
new_label = available_labels.find_by(title: title)
+ return new_label if find_only
+
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
create_params = params.except(:include_ancestor_groups)
new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index a1937d24294..88ed0c3ef4c 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -18,6 +18,18 @@ module MergeRequests
merge_request.target_project = find_target_project
filter_params(merge_request)
+
+ # merge_request.assign_attributes(...) below is a Rails
+ # method that only work if all the params it is passed have
+ # corresponding fields in the database. As there are no fields
+ # in the database for :add_label_ids and :remove_label_ids, we
+ # need to remove them from the params before the call to
+ # merge_request.assign_attributes(...)
+ #
+ # IssuableBaseService#process_label_ids takes care
+ # of the removal.
+ params[:label_ids] = process_label_ids(params, extra_label_ids: merge_request.label_ids.to_a)
+
merge_request.assign_attributes(params.to_h.compact)
merge_request.compare_commits = []
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index b210004e6e1..0168b31005e 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -100,7 +100,8 @@ module MergeRequests
merge_request = ::MergeRequests::CreateService.new(
project,
current_user,
- merge_request.attributes.merge(assignees: merge_request.assignees)
+ merge_request.attributes.merge(assignees: merge_request.assignees,
+ label_ids: merge_request.label_ids)
).execute
end
@@ -122,7 +123,9 @@ module MergeRequests
title: push_options[:title],
description: push_options[:description],
target_branch: push_options[:target],
- force_remove_source_branch: push_options[:remove_source_branch]
+ force_remove_source_branch: push_options[:remove_source_branch],
+ label: push_options[:label],
+ unlabel: push_options[:unlabel]
}
params.compact!
@@ -134,6 +137,9 @@ module MergeRequests
)
end
+ params[:add_labels] = params.delete(:label).keys if params.has_key?(:label)
+ params[:remove_labels] = params.delete(:unlabel).keys if params.has_key?(:unlabel)
+
params
end
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 87aaf4672a4..6b2836bba39 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -5,7 +5,8 @@ module ProtectedBranches
def execute(skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized?
- protected_branch.save
+ save_protected_branch
+
protected_branch
end
@@ -15,6 +16,10 @@ module ProtectedBranches
private
+ def save_protected_branch
+ protected_branch.save
+ end
+
def protected_branch
@protected_branch ||= project.protected_branches.new(params)
end
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index bb445499cee..f292730441c 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -42,6 +42,11 @@
class AddressableUrlValidator < ActiveModel::EachValidator
attr_reader :record
+ # By default, we avoid checking the dns rebinding protection
+ # when saving/updating a record. Sometimes, the url
+ # is not resolvable at that point, and some automated
+ # tasks that uses that url won't work.
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/66723
BLOCKER_VALIDATE_OPTIONS = {
schemes: %w(http https),
ports: [],
@@ -49,7 +54,8 @@ class AddressableUrlValidator < ActiveModel::EachValidator
allow_local_network: true,
ascii_only: false,
enforce_user: false,
- enforce_sanitization: false
+ enforce_sanitization: false,
+ dns_rebind_protection: false
}.freeze
DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 5b2bbffc066..b9d54d9636e 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# UrlValidator
+# CertificateKeyValidator
#
# Custom validator for private keys.
#
@@ -20,7 +20,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator
def valid_private_key_pem?(value)
return false unless value
- pkey = OpenSSL::PKey::RSA.new(value)
+ pkey = OpenSSL::PKey.read(value)
pkey.private?
rescue OpenSSL::PKey::PKeyError
false
diff --git a/app/validators/named_ecdsa_key_validator.rb b/app/validators/named_ecdsa_key_validator.rb
new file mode 100644
index 00000000000..42ee02b6ad4
--- /dev/null
+++ b/app/validators/named_ecdsa_key_validator.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# NamedEcdsaKeyValidator
+#
+# Custom validator for ecdsa private keys.
+# Golang currently doesn't support explicit curves for ECDSA certificates
+# This validator checks if curve is set by name, not by parameters
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, named_ecdsa_key: true
+# end
+#
+class NamedEcdsaKeyValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if explicit_ec?(value)
+ record.errors.add(attribute, "ECDSA keys with explicit curves are not supported")
+ end
+ end
+
+ private
+
+ UNNAMED_CURVE = "UNDEF"
+
+ def explicit_ec?(value)
+ return false unless value
+
+ pkey = OpenSSL::PKey.read(value)
+ return false unless pkey.is_a?(OpenSSL::PKey::EC)
+
+ pkey.group.curve_name == UNNAMED_CURVE
+ rescue OpenSSL::PKey::PKeyError
+ false
+ end
+end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 8aca61efe7b..8fad42436ca 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -35,41 +35,7 @@
= link_to 'New group', new_admin_group_path, class: "btn btn-success"
.row
.col-md-4
- .info-well
- .well-segment.admin-well.admin-well-statistics
- %h4 Statistics
- %p
- Forks
- %span.light.float-right
- = approximate_fork_count_with_delimiters(@counts)
- %p
- Issues
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Issue)
- %p
- Merge Requests
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, MergeRequest)
- %p
- Notes
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Note)
- %p
- Snippets
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Snippet)
- %p
- SSH Keys
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Key)
- %p
- Milestones
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Milestone)
- %p
- Active Users
- %span.light.float-right
- = number_with_delimiter(User.active.count)
+ #js-admin-statistics-container
.col-md-4
.info-well
.well-segment.admin-well.admin-well-features
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
new file mode 100644
index 00000000000..f707c6585ec
--- /dev/null
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
@@ -0,0 +1,8 @@
+- provider = local_assigns.fetch(:provider)
+- logo_path = local_assigns.fetch(:logo_path)
+- label = local_assigns.fetch(:label)
+
+= link_to clusterable.new_path(provider: provider), class: 'btn gl-button btn-outline flex-fill d-inline-flex flex-column mr-3 justify-content-center align-items-center' do
+ = image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13'
+ %span
+ = label
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
new file mode 100644
index 00000000000..24506205243
--- /dev/null
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
@@ -0,0 +1,11 @@
+- gke_label = s_('ClusterIntegration|Google GKE')
+- eks_label = s_('ClusterIntegration|Amazon EKS')
+- create_cluster_label = s_('ClusterIntegration|Create cluster on')
+.d-flex.flex-column
+ %h5
+ = create_cluster_label
+ .d-flex
+ = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
+ locals: { provider: 'gke', label: gke_label, logo_path: '' }
+ = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
+ locals: { provider: 'eks', label: eks_label, logo_path: '' }
diff --git a/app/views/clusters/clusters/eks/_index.html.haml b/app/views/clusters/clusters/eks/_index.html.haml
new file mode 100644
index 00000000000..ca8e9ba527a
--- /dev/null
+++ b/app/views/clusters/clusters/eks/_index.html.haml
@@ -0,0 +1 @@
+.js-create-eks-cluster-form-container
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index 6a8af23e5e8..fb182d99ff0 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title _('Kubernetes')
- page_title _('Kubernetes Cluster')
-- active_tab = local_assigns.fetch(:active_tab, 'gcp')
+- create_eks_enabled = Feature.enabled?(:create_eks_clusters)
+- active_tab = local_assigns.fetch(:active_tab, 'create')
+- link_end = '<a/>'.html_safe
= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
@@ -11,26 +13,36 @@
.col-md-9.js-toggle-container
%ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#create-gcp-cluster-pane', id: 'create-gcp-cluster-tab', class: active_when(active_tab == 'gcp'), data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' }
%span Create new Cluster on GKE
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' }
%span Add existing cluster
.tab-content.gitlab-tab-content
- .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
- = render 'clusters/clusters/gcp/header'
- - if @valid_gcp_token
- = render 'clusters/clusters/gcp/form'
- - elsif @authorize_url
- .signin-with-google
- = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
- = _('or')
- = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- - else
- - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
- = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
+ - if create_eks_enabled
+ .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
+ - if @gke_selected && @valid_gcp_token
+ = render 'clusters/clusters/gcp/header'
+ = render 'clusters/clusters/gcp/form'
+ - elsif @eks_selected
+ = render 'clusters/clusters/eks/index'
+ - else
+ = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
+ - else
+ .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
+ = render 'clusters/clusters/gcp/header'
+ - if @valid_gcp_token
+ = render 'clusters/clusters/gcp/form'
+ - elsif @authorize_url
+ .signin-with-google
+ - create_account_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral' }
+ = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px', alt: _('Sign in with Google')), @authorize_url)
+ = s_('or %{link_start}create a new Google account%{link_end}').html_safe % { link_start: create_account_link, link_end: link_end }
+ - else
+ - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
+ = s_('Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }
- .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
+ .tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
= render 'clusters/clusters/user/header'
= render 'clusters/clusters/user/form'
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index d56234e6c1a..406e8a93194 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -1,5 +1,7 @@
return unless event.visible_to_user?(current_user)
+event = event.present
+
xml.entry do
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_feed_url(event)
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 222175c818a..647f0597adb 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,3 +1,5 @@
+- event = event.present
+
- if event.visible_to_user?(current_user)
.event-item
.event-item-timestamp
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 98941722434..67e4c538b4a 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -2,6 +2,5 @@
= event_preposition(event)
- if event.project
= link_to_project(event.project)
- - else
- = event.project_name
-
+ - elsif event.group
+ = link_to event.resource_parent_name, group_path(event.group)
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index b02fdb4b638..50c5885c648 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -8,7 +8,7 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
%span.event-target-type.append-right-4= event.target_type.titleize.downcase
- = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
+ = link_to event.target_link_options, class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
%span.event-target-title.append-right-4{ dir: "auto" }
@@ -17,4 +17,4 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event)
- = render "events/event_scope", event: event
+ = render "events/event_scope", event: event if event.resource_parent.present?
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 2f156603414..606b0febb57 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -10,4 +10,4 @@
- if event.project
= link_to_project(event.project)
- else
- = event.project_name
+ = event.resource_parent_name
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index a996c86a256..f1ba804f920 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,5 +1,5 @@
#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog.modal-lg
+ .modal-dialog.modal-lg.modal-1040
.modal-content
.modal-header
%h4.modal-title
@@ -11,104 +11,100 @@
.modal-body
.row
.col-lg-4
- %table.shortcut-mappings
+ %table.shortcut-mappings.text-2
%tbody
%tr
%th
%th= _('Global Shortcuts')
%tr
%td.shortcut
- %kbd s
- %td= _('Focus Search')
+ %kbd ?
+ %td= _('Toggle this dialog')
%tr
%td.shortcut
- %kbd f
- %td= _('Focus Filter')
+ %kbd shift p
+ %td= _('Go to your projects')
%tr
%td.shortcut
- %kbd p
- %kbd b
- %td= _('Toggle the Performance Bar')
+ %kbd shift g
+ %td= _('Go to your groups')
%tr
%td.shortcut
- %kbd ?
- %td= _('Show/hide this dialog')
+ %kbd shift a
+ %td= _('Go to the activity feed')
%tr
%td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; shift p
- - else
- %kbd ctrl shift p
- %td= _('Toggle Markdown preview')
+ %kbd shift l
+ %td= _('Go to the milestone list')
%tr
%td.shortcut
- %kbd
- %i.fa.fa-arrow-up
- %td= _('Edit last comment (when focused on an empty textarea)')
+ %kbd shift s
+ %td= _('Go to your snippets')
%tr
%td.shortcut
- %kbd shift t
- %td
- = _('Go to todos')
+ %kbd s
+ %td= _('Start search')
%tr
%td.shortcut
- %kbd shift a
- %td
- = _('Go to the activity feed')
+ %kbd shift i
+ %td= _('Go to your issues')
%tr
%td.shortcut
- %kbd shift p
- %td
- = _('Go to projects')
+ %kbd shift m
+ %td= _('Go to your merge requests')
%tr
%td.shortcut
- %kbd shift i
- %td
- = _('Go to issues')
+ %kbd shift t
+ %td= _('Go to your To-Do list')
%tr
%td.shortcut
- %kbd shift m
- %td
- = _('Go to merge requests')
+ %kbd p
+ %kbd b
+ %td= _('Toggle the Performance Bar')
+ %tbody
%tr
- %td.shortcut
- %kbd shift g
- %td
- = _('Go to groups')
+ %th
+ %th= _('Web IDE')
%tr
%td.shortcut
- %kbd shift l
- %td
- = _('Go to milestones')
+ - if browser.platform.mac?
+ %kbd &#8984; p
+ - else
+ %kbd ctrl p
+ %td= _('Go to file')
%tr
%td.shortcut
- %kbd shift s
- %td
- = _('Go to snippets')
+ - if browser.platform.mac?
+ %kbd &#8984; enter
+ - else
+ %kbd ctrl enter
+ %td= _('Commit (when editing commit message)')
%tbody
%tr
%th
- %th= _('Finding Project File')
+ %th= _('Wiki pages')
%tr
%td.shortcut
- %kbd
- %i.fa.fa-arrow-up
- %td= _('Move selection up')
+ %kbd e
+ %td= _('Edit wiki page')
+ %tbody
%tr
- %td.shortcut
- %kbd
- %i.fa.fa-arrow-down
- %td= _('Move selection down')
+ %th
+ %th= _('Editing')
%tr
%td.shortcut
- %kbd enter
- %td= _('Open Selection')
+ - if browser.platform.mac?
+ %kbd &#8984; shift p
+ - else
+ %kbd ctrl shift p
+ %td= _('Toggle Markdown preview')
%tr
%td.shortcut
- %kbd esc
- %td= _('Go back')
+ %kbd
+ %i.fa.fa-arrow-up
+ %td= _('Edit your most recent comment in a thread (from an empty textarea)')
.col-lg-4
- %table.shortcut-mappings
+ %table.shortcut-mappings.text-2
%tbody
%tr
%th
@@ -117,105 +113,94 @@
%td.shortcut
%kbd g
%kbd p
- %td
- = _('Go to the project\'s overview page')
+ %td= _('Go to the project\'s overview page')
%tr
%td.shortcut
%kbd g
%kbd v
- %td
- = _('Go to the project\'s activity feed')
+ %td= _('Go to the project\'s activity feed')
%tr
%td.shortcut
%kbd g
- %kbd f
- %td
- = _('Go to files')
+ %kbd r
+ %td= _('Go to releases')
%tr
%td.shortcut
%kbd g
- %kbd c
- %td
- = _('Go to commits')
+ %kbd f
+ %td= _('Go to files')
+ %tr
+ %td.shortcut
+ %kbd t
+ %td= _('Go to find file')
%tr
%td.shortcut
%kbd g
- %kbd j
- %td
- = _('Go to jobs')
+ %kbd c
+ %td= _('Go to commits')
%tr
%td.shortcut
%kbd g
%kbd n
- %td
- = _('Go to network graph')
+ %td= _('Go to repository graph')
%tr
%td.shortcut
%kbd g
%kbd d
- %td
- = _('Go to repository charts')
+ %td= _('Go to repository charts')
%tr
%td.shortcut
%kbd g
%kbd i
- %td
- = _('Go to issues')
+ %td= _('Go to issues')
+ %tr
+ %td.shortcut
+ %kbd i
+ %td= _('New issue')
%tr
%td.shortcut
%kbd g
%kbd b
- %td
- = _('Go to issue boards')
+ %td= _('Go to issue boards')
%tr
%td.shortcut
%kbd g
%kbd m
- %td
- = _('Go to merge requests')
+ %td= _('Go to merge requests')
%tr
%td.shortcut
%kbd g
- %kbd e
- %td
- = _('Go to environments')
+ %kbd j
+ %td= _('Go to jobs')
%tr
%td.shortcut
%kbd g
%kbd l
- %td
- = _('Go to metrics')
+ %td= _('Go to metrics')
+ %tr
+ %td.shortcut
+ %kbd g
+ %kbd e
+ %td= _('Go to environments')
%tr
%td.shortcut
%kbd g
%kbd k
- %td
- = _('Go to kubernetes')
+ %td= _('Go to kubernetes')
%tr
%td.shortcut
%kbd g
%kbd s
- %td
- = _('Go to snippets')
+ %td= _('Go to snippets')
%tr
%td.shortcut
%kbd g
%kbd w
- %td
- = _('Go to wiki')
- %tr
- %td.shortcut
- %kbd t
- %td= _('Go to finding file')
- %tr
- %td.shortcut
- %kbd i
- %td= _('New issue')
-
+ %td= _('Go to wiki')
%tbody
%tr
%th
- %th= _('Project Files browsing')
+ %th= _('Project Files')
%tr
%td.shortcut
%kbd
@@ -230,38 +215,87 @@
%td.shortcut
%kbd enter
%td= _('Open Selection')
- %tbody
%tr
- %th
- %th= _('Project File')
+ %td.shortcut
+ %kbd esc
+ %td= _('Go back (while searching for files')
%tr
%td.shortcut
%kbd y
- %td= _('Go to file permalink')
+ %td= _('Go to file permalink (while viewing a file)')
+ .col-lg-4
+ %table.shortcut-mappings.text-2
%tbody
%tr
%th
- %th= _('Web IDE')
+ %th= _('Issues / Merge Requests')
+ %tr
+ %td.shortcut
+ %kbd a
+ %td= _('Change assignee')
+ %tr
+ %td.shortcut
+ %kbd m
+ %td= _('Change milestone')
+ %tr
+ %td.shortcut
+ %kbd r
+ %td= _('Comment/Reply (quoting selected text)')
+ %tr
+ %td.shortcut
+ %kbd e
+ %td= _('Edit description')
+ %tr
+ %td.shortcut
+ %kbd l
+ %td= _('Change label')
+ %tr
+ %td.shortcut
+ %kbd ]
+ \/
+ %kbd j
+ %td= _('Next file in diff (MRs only)')
+ %tr
+ %td.shortcut
+ %kbd [
+ \/
+ %kbd k
+ %td= _('Previous file in diff (MRs only)')
%tr
%td.shortcut
- if browser.platform.mac?
%kbd &#8984; p
- else
%kbd ctrl p
- %td= _('Go to file')
+ %td= _('Go to file (MRs only)')
%tr
%td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; enter
- - else
- %kbd ctrl enter
- %td= _('Commit (when editing commit message)')
- .col-lg-4
- %table.shortcut-mappings
- %tbody.hidden-shortcut.network{ style: 'display:none' }
+ %kbd n
+ %td= _('Next unresolved discussion (MRs only)')
+ %tr
+ %td.shortcut
+ %kbd p
+ %td= _('Previous unresolved discussion (MRs only)')
+ %tbody
%tr
%th
- %th= _('Network Graph')
+ %th= _('Epics (Ultimate / Gold license only)')
+ %tr
+ %td.shortcut
+ %kbd r
+ %td= _('Comment/Reply (quoting selected text)')
+ %tr
+ %td.shortcut
+ %kbd e
+ %td= _('Edit epic description')
+ %tr
+ %td.shortcut
+ %kbd l
+ %td= _('Change label')
+ %tbody
+ %tr
+ %th
+ %th= _('Repository Graph')
%tr
%td.shortcut
%kbd
@@ -295,92 +329,12 @@
%kbd
shift
%i.fa.fa-arrow-up
- \/
- %kbd
- shift k
+ \/ k
%td= _('Scroll to top')
%tr
%td.shortcut
%kbd
shift
%i.fa.fa-arrow-down
- \/
- %kbd
- shift j
+ \/ j
%td= _('Scroll to bottom')
- %tbody.hidden-shortcut.issues{ style: 'display:none' }
- %tr
- %th
- %th= _('Issues')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
- %tr
- %td.shortcut
- %kbd r
- %td= _('Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit issue')
- %tr
- %td.shortcut
- %kbd l
- %td= _('Change Label')
- %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
- %tr
- %th
- %th= _('Merge Requests')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
- %tr
- %td.shortcut
- %kbd r
- %td= _('Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit merge request')
- %tr
- %td.shortcut
- %kbd l
- %td= _('Change Label')
- %tr
- %td.shortcut
- %kbd ]
- \/
- %kbd j
- %td= _('Move to next file')
- %tr
- %td.shortcut
- %kbd [
- \/
- %kbd k
- %td= _('Move to previous file')
- %tr
- %td.shortcut
- %kbd n
- %td= _('Move to next unresolved discussion')
- %tr
- %td.shortcut
- %kbd p
- %td= _('Move to previous unresolved discussion')
- %tbody.hidden-shortcut.wiki{ style: 'display:none' }
- %tr
- %th
- %th= _('Wiki pages')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit wiki page')
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index c1f4b3adfec..7cc7d1783c4 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -48,14 +48,14 @@
- if group_sidebar_link?(:issues)
= nav_link(path: group_issues_sub_menu_items) do
- = link_to issues_group_path(@group) do
+ = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' } do
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
= _('Issues')
%span.badge.badge-pill.count= number_with_delimiter(issues_count)
- %ul.sidebar-sub-level-items
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
@@ -70,7 +70,7 @@
- if group_sidebar_link?(:boards)
= nav_link(path: ['boards#index', 'boards#show']) do
- = link_to group_boards_path(@group), title: boards_link_text do
+ = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
%span
= boards_link_text
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index bc0a89bea62..4b82eb2c5ef 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -8,7 +8,7 @@
- else
- can_create_fork = current_user.can?(:create_fork)
= link_to new_project_fork_path(@project),
- class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}",
+ class: "btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}",
title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do
= sprite_icon('fork', { css_class: 'icon' })
%span= s_('ProjectOverview|Fork')
diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml
index bc02c5f0e5a..96f61584a99 100644
--- a/app/views/projects/error_tracking/index.html.haml
+++ b/app/views/projects/error_tracking/index.html.haml
@@ -1,3 +1,3 @@
- page_title _('Errors')
-#js-error_tracking{ data: error_tracking_data(@project) }
+#js-error_tracking{ data: error_tracking_data(@current_user, @project) }
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 15499c89ffb..928b54ea28f 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -13,12 +13,13 @@
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
- git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch}
- git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
+ -# All repo/branch refs have been quoted to allow support for special characters (such as #my-branch)
+ git fetch "#{h default_url_to_repo(@merge_request.source_project)}" "#{h @merge_request.source_branch}"
+ git checkout -b "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}" FETCH_HEAD
- else
:preserve
git fetch origin
- git checkout -b #{h @merge_request.source_branch} origin/#{h @merge_request.source_branch}
+ git checkout -b "#{h @merge_request.source_branch}" "origin/#{h @merge_request.source_branch}"
%p
%strong Step 2.
Review the changes locally
@@ -31,20 +32,20 @@
- if @merge_request.for_fork?
:preserve
git fetch origin
- git checkout origin/#{h @merge_request.target_branch}
- git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch}
+ git checkout "origin/#{h @merge_request.target_branch}"
+ git merge --no-ff "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}"
- else
:preserve
git fetch origin
- git checkout origin/#{h @merge_request.target_branch}
- git merge --no-ff #{h @merge_request.source_branch}
+ git checkout "origin/#{h @merge_request.target_branch}"
+ git merge --no-ff "#{h @merge_request.source_branch}"
%p
%strong Step 4.
Push the result of the merge to GitLab
= clipboard_button(target: "pre#merge-info-4", title: "Copy commands to clipboard")
%pre.dark#merge-info-4
:preserve
- git push origin #{h @merge_request.target_branch}
+ git push origin "#{h @merge_request.target_branch}"
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 1a163cc4a54..7ff6c0a2019 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -3,6 +3,7 @@
%li
= _('The repository must be accessible over <code>http://</code>,
<code>https://</code>, <code>ssh://</code> or <code>git://</code>.').html_safe
+ %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe
%li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 539f223ca9b..7b6d46964a2 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -7,9 +7,11 @@
%strong
= _("Your pages are served under:")
- %p= link_to @project.pages_url, @project.pages_url
+ %p
+ = external_link(@project.pages_url, @project.pages_url)
- @project.pages_domains.each do |domain|
- %p= link_to domain.url, domain.url
+ %p
+ = external_link(domain.url, domain.url)
.card-footer.alert-primary
= _("It may take up to 30 minutes before the site is available after the first deployment.")
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 2427b4d7611..c4285e7f3d2 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -12,9 +12,7 @@
.domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip }
= sprite_icon("status_#{status}", size: 16 )
.domain-name
- = link_to domain.url do
- = domain.url
- = icon('external-link')
+ = external_link(domain.url, domain.url)
- if domain.subject
%div
%span.badge.badge-gray Certificate: #{domain.subject}
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index d0b54946f7e..33837e21c8d 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -21,9 +21,7 @@
%td
= _("Domain")
%td
- = link_to @domain.url do
- = @domain.url
- = icon('external-link')
+ = external_link(@domain.url, @domain.url)
%tr
%td
= _("DNS")
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
index 326b83c856e..4d5b8cc80f7 100644
--- a/app/views/projects/releases/index.html.haml
+++ b/app/views/projects/releases/index.html.haml
@@ -1,3 +1,3 @@
- page_title _('Releases')
-#js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases/index') } }
+#js-releases-page{ data: data_for_releases_page }
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index d0f9374e832..b2ea45d6f1a 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -27,6 +27,7 @@
%ul
%li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
+ %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe
%li
= _('If your HTTP repository is not publicly accessible, add your credentials.')
%li
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 837707707a9..3b26b8df8a1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -41,7 +41,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present?
- = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
+ = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' }
- else
%span.no-value
= _('None')
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
index feca109dade..5f3d49adff7 100644
--- a/app/views/shared/members/_sort_dropdown.html.haml
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -1,4 +1,4 @@
-= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold pr-2'
+= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold px-2'
.dropdown.inline.qa-user-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index b4266937a4e..441abd57334 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -17,14 +17,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 3191eaa1e2c..7516dfe1602 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -27,7 +27,7 @@
- if event.project
= link_to_project(event.project)
- else
- = event.project_name
+ = event.resource_parent_name
- else
made a private contribution
- else
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 991a177018e..a33afd436b0 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -160,6 +160,7 @@
- repository_import
- repository_remove_remote
- system_hook_push
+- update_external_pull_requests
- update_merge_requests
- update_project_statistics
- upload_checksum
diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb
new file mode 100644
index 00000000000..c5acfa82ada
--- /dev/null
+++ b/app/workers/update_external_pull_requests_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class UpdateExternalPullRequestsWorker
+ include ApplicationWorker
+
+ def perform(project_id, user_id, ref)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ user = User.find_by_id(user_id)
+ return unless user
+
+ branch = Gitlab::Git.branch_name(ref)
+ return unless branch
+
+ external_pull_requests = project.external_pull_requests
+ .by_source_repository(project.import_source)
+ .by_source_branch(branch)
+
+ external_pull_requests.find_each do |pull_request|
+ ExternalPullRequests::CreatePipelineService.new(project, user)
+ .execute(pull_request)
+ end
+ end
+end
diff --git a/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml b/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml
new file mode 100644
index 00000000000..9f34a912dc5
--- /dev/null
+++ b/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml
@@ -0,0 +1,5 @@
+---
+title: Only show /copy_metadata quick action when usable
+merge_request: 31735
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/46686-bump-kubeclient-version-qa.yml b/changelogs/unreleased/46686-bump-kubeclient-version-qa.yml
new file mode 100644
index 00000000000..b4adbe4dcea
--- /dev/null
+++ b/changelogs/unreleased/46686-bump-kubeclient-version-qa.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Kubeclient to 4.4.0
+merge_request: 32811
+author:
+type: other
diff --git a/changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml b/changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml
new file mode 100644
index 00000000000..9862137c80c
--- /dev/null
+++ b/changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml
@@ -0,0 +1,5 @@
+---
+title: 'Admin dashboard: Fetch and render statistics async'
+merge_request: 32449
+author:
+type: other
diff --git a/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml b/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml
new file mode 100644
index 00000000000..1962a662179
--- /dev/null
+++ b/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml
@@ -0,0 +1,5 @@
+---
+title: Fix parsing of months in time tracking commands
+merge_request: 32165
+author:
+type: fixed
diff --git a/changelogs/unreleased/60724-watch-button.yml b/changelogs/unreleased/60724-watch-button.yml
new file mode 100644
index 00000000000..f22f7708ed2
--- /dev/null
+++ b/changelogs/unreleased/60724-watch-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fix watch button styling and notifications buttons consistency
+merge_request: 32827
+author:
+type: fixed
diff --git a/changelogs/unreleased/66067-pages-domain-doesnt-set-target-blank.yml b/changelogs/unreleased/66067-pages-domain-doesnt-set-target-blank.yml
new file mode 100644
index 00000000000..726d4b163d2
--- /dev/null
+++ b/changelogs/unreleased/66067-pages-domain-doesnt-set-target-blank.yml
@@ -0,0 +1,5 @@
+---
+title: Makes custom Pages domain open as external link in new tab
+merge_request: 32130
+author: jakeburden
+type: fixed
diff --git a/changelogs/unreleased/66150-remove-dynamically-constructed-feature-flags-starting-with-promethe.yml b/changelogs/unreleased/66150-remove-dynamically-constructed-feature-flags-starting-with-promethe.yml
new file mode 100644
index 00000000000..e6dc1f1bec6
--- /dev/null
+++ b/changelogs/unreleased/66150-remove-dynamically-constructed-feature-flags-starting-with-promethe.yml
@@ -0,0 +1,5 @@
+---
+title: Remove dynamically constructed feature flags starting with prometheus_transaction_
+merge_request: 32395
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml b/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml
new file mode 100644
index 00000000000..b152943942f
--- /dev/null
+++ b/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Display `more information` docs link on error tracking page when users do not have permissions to enable that feature
+merge_request: 32365
+author: Romain Maneschi
+type: fixed
diff --git a/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml b/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml
new file mode 100644
index 00000000000..54bec43815c
--- /dev/null
+++ b/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml
@@ -0,0 +1,5 @@
+---
+title: Update merge train documentation
+merge_request: 32218
+author:
+type: changed
diff --git a/changelogs/unreleased/67037-user-content-gitlab-static-net-brings-back-404-only.yml b/changelogs/unreleased/67037-user-content-gitlab-static-net-brings-back-404-only.yml
new file mode 100644
index 00000000000..8d4f816af2e
--- /dev/null
+++ b/changelogs/unreleased/67037-user-content-gitlab-static-net-brings-back-404-only.yml
@@ -0,0 +1,5 @@
+---
+title: Default the asset proxy whitelist to the installation domain
+merge_request: 32703
+author:
+type: fixed
diff --git a/changelogs/unreleased/ab-unconfirmed-email-index.yml b/changelogs/unreleased/ab-unconfirmed-email-index.yml
new file mode 100644
index 00000000000..3887cd87e41
--- /dev/null
+++ b/changelogs/unreleased/ab-unconfirmed-email-index.yml
@@ -0,0 +1,5 @@
+---
+title: Create index for users.unconfirmed_email
+merge_request: 32664
+author:
+type: performance
diff --git a/changelogs/unreleased/ac-accelerate-wiki-attachments.yml b/changelogs/unreleased/ac-accelerate-wiki-attachments.yml
new file mode 100644
index 00000000000..347a570488e
--- /dev/null
+++ b/changelogs/unreleased/ac-accelerate-wiki-attachments.yml
@@ -0,0 +1,5 @@
+---
+title: Preprocess wiki attachments with GitLab-Workhorse
+merge_request: 32663
+author:
+type: performance
diff --git a/changelogs/unreleased/add-label-push-opts.yml b/changelogs/unreleased/add-label-push-opts.yml
new file mode 100644
index 00000000000..1289020e4e5
--- /dev/null
+++ b/changelogs/unreleased/add-label-push-opts.yml
@@ -0,0 +1,5 @@
+---
+title: Support adding and removing labels w/ push opts
+merge_request: 31831
+author:
+type: added
diff --git a/changelogs/unreleased/api_settings.yml b/changelogs/unreleased/api_settings.yml
new file mode 100644
index 00000000000..58830a5ab97
--- /dev/null
+++ b/changelogs/unreleased/api_settings.yml
@@ -0,0 +1,5 @@
+---
+title: Improve application settings API
+merge_request: 31149
+author: Mathieu Parent
+type: fixed
diff --git a/changelogs/unreleased/ecdsa_pages_certificates.yml b/changelogs/unreleased/ecdsa_pages_certificates.yml
new file mode 100644
index 00000000000..059cb434b62
--- /dev/null
+++ b/changelogs/unreleased/ecdsa_pages_certificates.yml
@@ -0,0 +1,5 @@
+---
+title: Allow ECDSA certificates for pages domains
+merge_request: 32393
+author:
+type: added
diff --git a/changelogs/unreleased/fj-11777-lower-search-count-limits.yml b/changelogs/unreleased/fj-11777-lower-search-count-limits.yml
new file mode 100644
index 00000000000..c284bc49bfc
--- /dev/null
+++ b/changelogs/unreleased/fj-11777-lower-search-count-limits.yml
@@ -0,0 +1,5 @@
+---
+title: Lower search counters
+merge_request: 11777
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-remove-dns-protection-when-validating.yml b/changelogs/unreleased/fj-remove-dns-protection-when-validating.yml
new file mode 100644
index 00000000000..9c74f8d69c7
--- /dev/null
+++ b/changelogs/unreleased/fj-remove-dns-protection-when-validating.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid checking dns rebind protection when validating
+merge_request: 32577
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.62.0.yml b/changelogs/unreleased/gitaly-version-v1.62.0.yml
new file mode 100644
index 00000000000..6ae200bd7f4
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.62.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.62.0
+merge_request: 32608
+author:
+type: changed
diff --git a/changelogs/unreleased/group_level_jupyterhub.yml b/changelogs/unreleased/group_level_jupyterhub.yml
new file mode 100644
index 00000000000..81fc7600e0e
--- /dev/null
+++ b/changelogs/unreleased/group_level_jupyterhub.yml
@@ -0,0 +1,5 @@
+---
+title: Group level JupyterHub
+merge_request: 32512
+author:
+type: added
diff --git a/changelogs/unreleased/id-autosave-for-new-mr.yml b/changelogs/unreleased/id-autosave-for-new-mr.yml
new file mode 100644
index 00000000000..8f269094715
--- /dev/null
+++ b/changelogs/unreleased/id-autosave-for-new-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix sharing localStorage with all MRs
+merge_request: 32699
+author:
+type: fixed
diff --git a/changelogs/unreleased/instance-group-level-knative.yml b/changelogs/unreleased/instance-group-level-knative.yml
new file mode 100644
index 00000000000..5108334a3ea
--- /dev/null
+++ b/changelogs/unreleased/instance-group-level-knative.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Knative to be installed on group and instance level clusters
+merge_request: 32128
+author:
+type: added
diff --git a/changelogs/unreleased/issue-32741.yml b/changelogs/unreleased/issue-32741.yml
new file mode 100644
index 00000000000..5d0a3ee2cc7
--- /dev/null
+++ b/changelogs/unreleased/issue-32741.yml
@@ -0,0 +1,5 @@
+---
+title: New interruptible attribute for CI/CD jobs
+merge_request: 23464
+author: Cédric Tabin
+type: added
diff --git a/changelogs/unreleased/issue-67127.yml b/changelogs/unreleased/issue-67127.yml
new file mode 100644
index 00000000000..70848dc6ff9
--- /dev/null
+++ b/changelogs/unreleased/issue-67127.yml
@@ -0,0 +1,5 @@
+---
+title: Expose 'protected' field for Tag API endpoint.
+merge_request: 32790
+author: Andrea Leone
+type: added
diff --git a/changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml b/changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml
new file mode 100644
index 00000000000..148e306c3b8
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Add caret icons to the monitoring dashboard
+merge_request: 32239
+author:
+type: changed
diff --git a/changelogs/unreleased/job-rules-e2e.yml b/changelogs/unreleased/job-rules-e2e.yml
new file mode 100644
index 00000000000..2c55dfcec49
--- /dev/null
+++ b/changelogs/unreleased/job-rules-e2e.yml
@@ -0,0 +1,5 @@
+---
+title: Passing job rules downstream and E2E specs for job:rules configuration
+merge_request: 32609
+author:
+type: fixed
diff --git a/changelogs/unreleased/kamil-improve-import-export.yml b/changelogs/unreleased/kamil-improve-import-export.yml
new file mode 100644
index 00000000000..9d485e0df2d
--- /dev/null
+++ b/changelogs/unreleased/kamil-improve-import-export.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce N+1 when doing project export
+merge_request: 32423
+author:
+type: performance
diff --git a/changelogs/unreleased/keyboard-shortcuts-2.yml b/changelogs/unreleased/keyboard-shortcuts-2.yml
new file mode 100644
index 00000000000..a6a2266b20a
--- /dev/null
+++ b/changelogs/unreleased/keyboard-shortcuts-2.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up keyboard shortcuts help modal, removing and adding as needed
+merge_request: 31642
+author:
+type: other
diff --git a/changelogs/unreleased/pl-project-service-json.yml b/changelogs/unreleased/pl-project-service-json.yml
new file mode 100644
index 00000000000..a273289d871
--- /dev/null
+++ b/changelogs/unreleased/pl-project-service-json.yml
@@ -0,0 +1,5 @@
+---
+title: Expose update project service endpoint JSON
+merge_request: 32759
+author:
+type: added
diff --git a/changelogs/unreleased/quote-branch-names-in-instructions.yml b/changelogs/unreleased/quote-branch-names-in-instructions.yml
new file mode 100644
index 00000000000..fe5964c8918
--- /dev/null
+++ b/changelogs/unreleased/quote-branch-names-in-instructions.yml
@@ -0,0 +1,5 @@
+---
+title: Quote branch names in how to merge instructions
+merge_request: 32639
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/sh-add-margin-member-list.yml b/changelogs/unreleased/sh-add-margin-member-list.yml
new file mode 100644
index 00000000000..35d35aad8bf
--- /dev/null
+++ b/changelogs/unreleased/sh-add-margin-member-list.yml
@@ -0,0 +1,5 @@
+---
+title: Add padding to left of "Sort by" in members dropdown
+merge_request: 32602
+author:
+type: other
diff --git a/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml b/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml
new file mode 100644
index 00000000000..b334355cab6
--- /dev/null
+++ b/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Log errors for failed pipeline creation in PostReceive
+merge_request: 32633
+author:
+type: other
diff --git a/changelogs/unreleased/update-rouge.yml b/changelogs/unreleased/update-rouge.yml
new file mode 100644
index 00000000000..6f44de02d76
--- /dev/null
+++ b/changelogs/unreleased/update-rouge.yml
@@ -0,0 +1,5 @@
+---
+title: Update rouge to v3.10.0
+merge_request: 32745
+author:
+type: other
diff --git a/changelogs/unreleased/use_default_external_auth_label_empty.yml b/changelogs/unreleased/use_default_external_auth_label_empty.yml
new file mode 100644
index 00000000000..9c1039b0875
--- /dev/null
+++ b/changelogs/unreleased/use_default_external_auth_label_empty.yml
@@ -0,0 +1,6 @@
+---
+title: Prevent empty external authorization classification labels from overriding
+ the default label
+merge_request: 32517
+author: Will Chandler
+type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index e3693f612e3..87159b695f9 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -321,6 +321,9 @@ production: &base
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
admin:
address: unix:/home/git/gitlab/tmp/sockets/private/pages-admin.socket # TCP connections are supported too (e.g. tcp://host:port)
+ # File that contains the shared secret key for verifying access for gitlab-pages.
+ # Default is '.gitlab_pages_shared_secret' relative to Rails.root (i.e. root of the GitLab app).
+ # secret_file: /home/git/gitlab/.gitlab_pages_shared_secret
## Mattermost
## For enabling Add to Mattermost button
@@ -339,7 +342,7 @@ production: &base
## Sidekiq
sidekiq:
- log_format: default # (json is also supported)
+ log_format: json # (default is the original format)
## Auxiliary jobs
# Periodically executed jobs, to self-heal GitLab, do external synchronizations, etc.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4160f488a7a..dbbb7ba1b60 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -292,6 +292,7 @@ Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pa
Settings.pages['admin'] ||= Settingslogic.new({})
Settings.pages.admin['certificate'] ||= ''
+Settings.pages['secret_file'] ||= Rails.root.join('.gitlab_pages_shared_secret')
#
# Geo
diff --git a/config/initializers/chronic_duration.rb b/config/initializers/chronic_duration.rb
index b65b06c813a..aa43eef434c 100644
--- a/config/initializers/chronic_duration.rb
+++ b/config/initializers/chronic_duration.rb
@@ -1 +1,5 @@
+# frozen_string_literal: true
+
ChronicDuration.raise_exceptions = true
+
+ChronicDuration.prepend Gitlab::Patch::ChronicDuration
diff --git a/config/routes.rb b/config/routes.rb
index 02a405a91f8..a622ce268da 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -107,6 +107,7 @@ Rails.application.routes.draw do
draw :instance_statistics
Gitlab.ee do
+ draw :security
draw :smartcard
draw :jira_connect
draw :username
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index b89e1c7f9af..093b86f3259 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -65,7 +65,7 @@ scope format: false do
resources :protected_tags, only: [:index, :show, :create, :update, :destroy]
end
- scope constraints: { id: /.+/ } do
+ scope constraints: { id: /[^\0]+/ } do
scope controller: :blob do
get '/new/*id', action: :new, as: :new_blob
post '/create/*id', action: :create, as: :create_blob
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7edec576f9a..f37cd518d48 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -115,3 +115,5 @@
- [export_csv, 1]
- [incident_management, 2]
- [jira_connect, 1]
+ - [update_external_pull_requests, 3]
+ - [refresh_license_compliance_checks, 2]
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
index 3550cb7eabf..5cdad09db6e 100644
--- a/danger/database/Dangerfile
+++ b/danger/database/Dangerfile
@@ -1,7 +1,13 @@
# frozen_string_literal: true
-SCHEMA_NOT_UPDATED_MESSAGE = <<~MSG
-**New %<migrations>s added but %<schema>s wasn't updated.**
+gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
+
+SCHEMA_NOT_UPDATED_MESSAGE_SHORT = <<~MSG
+New %<migrations>s added but %<schema>s wasn't updated.
+MSG
+
+SCHEMA_NOT_UPDATED_MESSAGE_FULL = <<~MSG
+**#{SCHEMA_NOT_UPDATED_MESSAGE_SHORT}**
Usually, when adding new %<migrations>s, %<schema>s should be
updated too (unless the migration isn't changing the DB schema
@@ -29,14 +35,18 @@ geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb}).emp
non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty?
geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty?
+format_str = gitlab_danger.ci? ? SCHEMA_NOT_UPDATED_MESSAGE_FULL : SCHEMA_NOT_UPDATED_MESSAGE_SHORT
+
if non_geo_migration_created && !non_geo_db_schema_updated
- warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'migrations', schema: gitlab.html_link("db/schema.rb"))
+ warn format(format_str, migrations: 'migrations', schema: gitlab_danger.html_link("db/schema.rb"))
end
if geo_migration_created && !geo_db_schema_updated
- warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'Geo migrations', schema: gitlab.html_link("ee/db/geo/schema.rb"))
+ warn format(format_str, migrations: 'Geo migrations', schema: gitlab_danger.html_link("ee/db/geo/schema.rb"))
end
+return unless gitlab_danger.ci?
+
db_paths_to_review = helper.changes_by_category[:database]
if gitlab.mr_labels.include?('database') || db_paths_to_review.any?
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 96c0d9b7478..ad64c3a6c60 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -6,20 +6,22 @@ unless docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a review ' \
'from the Technical Writing team.'
- markdown(<<~MARKDOWN)
-## Documentation review
+ if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Documentation review
-The following files require a review from a technical writer:
+ The following files require a review from a technical writer:
-* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
-The review does not need to block merging this merge request. See the:
+ The review does not need to block merging this merge request. See the:
-- [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review.
-- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
- MARKDOWN
+ - [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review.
+ - [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
+ MARKDOWN
- unless gitlab.mr_labels.include?('Documentation')
- warn 'This merge request is missing the ~Documentation label.'
+ unless gitlab.mr_labels.include?('Documentation')
+ warn 'This merge request is missing the ~Documentation label.'
+ end
end
end
diff --git a/danger/duplicate_yarn_dependencies/Dangerfile b/danger/duplicate_yarn_dependencies/Dangerfile
index 25f81ec86a4..492b888d00e 100644
--- a/danger/duplicate_yarn_dependencies/Dangerfile
+++ b/danger/duplicate_yarn_dependencies/Dangerfile
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-return unless helper.all_changed_files.include? 'yarn.lock'
+return unless helper.all_changed_files.include?('yarn.lock')
duplicate = `node_modules/.bin/yarn-deduplicate --list --strategy fewer yarn.lock`
.split(/$/)
@@ -11,17 +11,19 @@ return if duplicate.empty?
warn 'This merge request has introduced duplicated yarn dependencies.'
-markdown(<<~MARKDOWN)
- ## Duplicate yarn dependencies
+if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Duplicate yarn dependencies
- The following dependencies should be de-duplicated:
+ The following dependencies should be de-duplicated:
- * #{duplicate.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{duplicate.map { |path| "`#{path}`" }.join("\n* ")}
- Please run the following command and commit the changes to `yarn.lock`:
+ Please run the following command and commit the changes to `yarn.lock`:
- ```
- node_modules/.bin/yarn-deduplicate --strategy fewer yarn.lock \\
- && yarn install
- ```
-MARKDOWN
+ ```
+ node_modules/.bin/yarn-deduplicate --strategy fewer yarn.lock \\
+ && yarn install
+ ```
+ MARKDOWN
+end
diff --git a/danger/eslint/Dangerfile b/danger/eslint/Dangerfile
index 4916cacfd7e..92830bd7706 100644
--- a/danger/eslint/Dangerfile
+++ b/danger/eslint/Dangerfile
@@ -13,17 +13,19 @@ return if eslint_candidates.empty?
warn 'This merge request changed files with disabled eslint rules. Please consider fixing them.'
-markdown(<<~MARKDOWN)
- ## Disabled eslint rules
+if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Disabled eslint rules
- The following files have disabled `eslint` rules. Please consider fixing them:
+ The following files have disabled `eslint` rules. Please consider fixing them:
- * #{eslint_candidates.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{eslint_candidates.map { |path| "`#{path}`" }.join("\n* ")}
- Run the following command for more details
+ Run the following command for more details
- ```
- node_modules/.bin/eslint --report-unused-disable-directives --no-inline-config \\
- #{eslint_candidates.map { |path| " '#{path}'" }.join(" \\\n")}
- ```
-MARKDOWN
+ ```
+ node_modules/.bin/eslint --report-unused-disable-directives --no-inline-config \\
+ #{eslint_candidates.map { |path| " '#{path}'" }.join(" \\\n")}
+ ```
+ MARKDOWN
+end
diff --git a/danger/frozen_string/Dangerfile b/danger/frozen_string/Dangerfile
index b9687ef6b83..8d3ac3dee68 100644
--- a/danger/frozen_string/Dangerfile
+++ b/danger/frozen_string/Dangerfile
@@ -16,11 +16,13 @@ if files_to_fix.any?
warn 'This merge request adds files that do not enforce frozen string literal. ' \
'See https://gitlab.com/gitlab-org/gitlab-ce/issues/47424 for more information.'
- markdown(<<~MARKDOWN)
- ## Enable Frozen String Literal
+ if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Enable Frozen String Literal
- The following files should have `#{MAGIC_COMMENT}` on the first line:
+ The following files should have `#{MAGIC_COMMENT}` on the first line:
- * #{files_to_fix.map { |path| "`#{path}`" }.join("\n* ")}
- MARKDOWN
+ * #{files_to_fix.map { |path| "`#{path}`" }.join("\n* ")}
+ MARKDOWN
+ end
end
diff --git a/danger/gemfile/Dangerfile b/danger/gemfile/Dangerfile
index dfe64f79d7b..07c4c07cfe8 100644
--- a/danger/gemfile/Dangerfile
+++ b/danger/gemfile/Dangerfile
@@ -1,5 +1,9 @@
-GEMFILE_LOCK_NOT_UPDATED_MESSAGE = <<~MSG.freeze
-**%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.**
+GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT = <<~MSG.freeze
+%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.
+MSG
+
+GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL = <<~MSG.freeze
+**#{GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT}**
Usually, when %<gemfile>s is updated, you should run
```
@@ -19,5 +23,14 @@ gemfile_modified = git.modified_files.include?("Gemfile")
gemfile_lock_modified = git.modified_files.include?("Gemfile.lock")
if gemfile_modified && !gemfile_lock_modified
- warn format(GEMFILE_LOCK_NOT_UPDATED_MESSAGE, gemfile: gitlab.html_link("Gemfile"), gemfile_lock: gitlab.html_link("Gemfile.lock"))
+ gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
+
+ format_str = gitlab_danger.ci? ? GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL : GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT
+
+ message = format(format_str,
+ gemfile: gitlab_danger.html_link("Gemfile"),
+ gemfile_lock: gitlab_danger.html_link("Gemfile.lock")
+ )
+
+ warn(message)
end
diff --git a/danger/prettier/Dangerfile b/danger/prettier/Dangerfile
index 37c4b78a213..0be75db8baa 100644
--- a/danger/prettier/Dangerfile
+++ b/danger/prettier/Dangerfile
@@ -19,21 +19,23 @@ return if unpretty.empty?
warn 'This merge request changed frontend files without pretty printing them.'
-markdown(<<~MARKDOWN)
- ## Pretty print Frontend files
+if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Pretty print Frontend files
- The following files should have been pretty printed with `prettier`:
+ The following files should have been pretty printed with `prettier`:
- * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")}
- Please run
+ Please run
- ```
- node_modules/.bin/prettier --write \\
- #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")}
- ```
+ ```
+ node_modules/.bin/prettier --write \\
+ #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")}
+ ```
- Also consider auto-formatting [on-save].
+ Also consider auto-formatting [on-save].
- [on-save]: https://docs.gitlab.com/ee/development/new_fe_guide/style/prettier.html
-MARKDOWN
+ [on-save]: https://docs.gitlab.com/ee/development/new_fe_guide/style/prettier.html
+ MARKDOWN
+end
diff --git a/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb b/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb
new file mode 100644
index 00000000000..098fcff9ace
--- /dev/null
+++ b/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class AddMergeRequestsRequireCodeOwnerApprovalToProtectedBranches < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(
+ :protected_branches,
+ :code_owner_approval_required,
+ :boolean,
+ default: false
+ )
+
+ add_concurrent_index(
+ :protected_branches,
+ [:project_id, :code_owner_approval_required],
+ name: "code_owner_approval_required",
+ where: "code_owner_approval_required = #{Gitlab::Database.true_value}")
+ end
+
+ def down
+ remove_concurrent_index(:protected_branches, name: "code_owner_approval_required")
+
+ remove_column(:protected_branches, :code_owner_approval_required)
+ end
+end
diff --git a/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
index f6db90f6637..3037f2ea106 100644
--- a/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
+++ b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
@@ -3,8 +3,23 @@
class RemoveEpicIssuesDefaultRelativePosition < ActiveRecord::Migration[5.2]
DOWNTIME = false
- def change
- change_column_null :epic_issues, :relative_position, true
- change_column_default :epic_issues, :relative_position, from: 1073741823, to: nil
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ # The column won't exist if someone installed EE, downgraded to CE
+ # before it was added in EE, then tries to upgrade CE.
+ if column_exists?(:epic_issues, :relative_position)
+ change_column_null :epic_issues, :relative_position, true
+ change_column_default :epic_issues, :relative_position, from: 1073741823, to: nil
+ else
+ add_column_with_default(:epic_issues, :relative_position, :integer, default: nil, allow_null: true)
+ end
+ end
+
+ def down
+ change_column_default :epic_issues, :relative_position, from: nil, to: 1073741823
+ change_column_null :epic_issues, :relative_position, false
end
end
diff --git a/db/migrate/20190826100605_add_group_column_to_events.rb b/db/migrate/20190826100605_add_group_column_to_events.rb
index cd7b2b1d96a..dfc9d8cbdf1 100644
--- a/db/migrate/20190826100605_add_group_column_to_events.rb
+++ b/db/migrate/20190826100605_add_group_column_to_events.rb
@@ -1,9 +1,19 @@
# frozen_string_literal: true
class AddGroupColumnToEvents < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
DOWNTIME = false
- def change
- add_reference :events, :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }
+ disable_ddl_transaction!
+
+ def up
+ add_column(:events, :group_id, :bigint) unless column_exists?(:events, :group_id)
+ add_concurrent_index(:events, :group_id)
+ add_concurrent_foreign_key(:events, :namespaces, column: :group_id, on_delete: :cascade)
+ end
+
+ def down
+ remove_column(:events, :group_id) if column_exists?(:events, :group_id)
end
end
diff --git a/db/migrate/20190829131130_create_external_pull_requests.rb b/db/migrate/20190829131130_create_external_pull_requests.rb
new file mode 100644
index 00000000000..0c3168807ec
--- /dev/null
+++ b/db/migrate/20190829131130_create_external_pull_requests.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CreateExternalPullRequests < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX = 'index_external_pull_requests_on_project_and_branches'
+
+ def change
+ create_table :external_pull_requests do |t|
+ t.timestamps_with_timezone null: false
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }, index: false
+ t.integer :pull_request_iid, null: false
+ t.integer :status, null: false, limit: 2
+ t.string :source_branch, null: false, limit: 255
+ t.string :target_branch, null: false, limit: 255
+ t.string :source_repository, null: false, limit: 255
+ t.string :target_repository, null: false, limit: 255
+ t.binary :source_sha, null: false
+ t.binary :target_sha, null: false
+
+ t.index [:project_id, :source_branch, :target_branch], unique: true, name: INDEX
+ end
+ end
+end
diff --git a/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb b/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb
new file mode 100644
index 00000000000..5abf56742b1
--- /dev/null
+++ b/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddExternalPullRequestIdToCiPipelines < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :ci_pipelines, :external_pull_request_id, :bigint
+ end
+
+ def down
+ remove_column :ci_pipelines, :external_pull_request_id
+ end
+end
diff --git a/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb b/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb
new file mode 100644
index 00000000000..d2f5ad7a420
--- /dev/null
+++ b/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_pipelines, :external_pull_request_id, where: 'external_pull_request_id IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :ci_pipelines, :external_pull_request_id
+ end
+end
diff --git a/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb b/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb
new file mode 100644
index 00000000000..b38fda83047
--- /dev/null
+++ b/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddForeignKeyToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :ci_pipelines, :external_pull_requests, column: :external_pull_request_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :ci_pipelines, :external_pull_requests
+ end
+end
diff --git a/db/migrate/20190904173203_add_index_on_users_unconfirmed_email.rb b/db/migrate/20190904173203_add_index_on_users_unconfirmed_email.rb
new file mode 100644
index 00000000000..e78d47f023f
--- /dev/null
+++ b/db/migrate/20190904173203_add_index_on_users_unconfirmed_email.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnUsersUnconfirmedEmail < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :users, :unconfirmed_email, where: 'unconfirmed_email IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :users, :unconfirmed_email, where: 'unconfirmed_email IS NOT NULL'
+ end
+end
diff --git a/db/migrate/20190905223800_add_interruptible_to_builds_metadata.rb b/db/migrate/20190905223800_add_interruptible_to_builds_metadata.rb
new file mode 100644
index 00000000000..a666b11013b
--- /dev/null
+++ b/db/migrate/20190905223800_add_interruptible_to_builds_metadata.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddInterruptibleToBuildsMetadata < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds_metadata, :interruptible, :boolean
+ end
+end
diff --git a/db/migrate/20190905223900_add_concurrent_index_to_builds_metadata.rb b/db/migrate/20190905223900_add_concurrent_index_to_builds_metadata.rb
new file mode 100644
index 00000000000..d394f995673
--- /dev/null
+++ b/db/migrate/20190905223900_add_concurrent_index_to_builds_metadata.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddConcurrentIndexToBuildsMetadata < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds_metadata, [:build_id],
+ where: "interruptible = false",
+ name: "index_ci_builds_metadata_on_build_id_and_interruptible_false"
+ end
+
+ def down
+ remove_concurrent_index_by_name(:ci_builds_metadata, 'index_ci_builds_metadata_on_build_id_and_interruptible_false')
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 79f4551ee26..70f3a42e7f8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_09_02_160015) do
+ActiveRecord::Schema.define(version: 2019_09_05_223900) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -619,9 +619,11 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
t.integer "project_id", null: false
t.integer "timeout"
t.integer "timeout_source", default: 1, null: false
+ t.boolean "interruptible"
t.jsonb "config_options"
t.jsonb "config_variables"
t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true
+ t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_interruptible_false", where: "(interruptible = false)"
t.index ["project_id"], name: "index_ci_builds_metadata_on_project_id"
end
@@ -752,7 +754,9 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
t.integer "merge_request_id"
t.binary "source_sha"
t.binary "target_sha"
+ t.bigint "external_pull_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id"
+ t.index ["external_pull_request_id"], name: "index_ci_pipelines_on_external_pull_request_id", where: "(external_pull_request_id IS NOT NULL)"
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id"
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)"
@@ -1321,6 +1325,21 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
end
+ create_table "external_pull_requests", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "project_id", null: false
+ t.integer "pull_request_iid", null: false
+ t.integer "status", limit: 2, null: false
+ t.string "source_branch", limit: 255, null: false
+ t.string "target_branch", limit: 255, null: false
+ t.string "source_repository", limit: 255, null: false
+ t.string "target_repository", limit: 255, null: false
+ t.binary "source_sha", null: false
+ t.binary "target_sha", null: false
+ t.index ["project_id", "source_branch", "target_branch"], name: "index_external_pull_requests_on_project_and_branches", unique: true
+ end
+
create_table "feature_gates", id: :serial, force: :cascade do |t|
t.string "feature_key", null: false
t.string "key", null: false
@@ -2932,6 +2951,8 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.boolean "code_owner_approval_required", default: false, null: false
+ t.index ["project_id", "code_owner_approval_required"], name: "code_owner_approval_required", where: "(code_owner_approval_required = true)"
t.index ["project_id"], name: "index_protected_branches_on_project_id"
end
@@ -3563,6 +3584,7 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
t.index ["state"], name: "index_users_on_state"
t.index ["state"], name: "index_users_on_state_and_internal", where: "(ghost IS NOT TRUE)"
t.index ["state"], name: "index_users_on_state_and_internal_ee", where: "((ghost IS NOT TRUE) AND (bot_type IS NULL))"
+ t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)"
t.index ["username"], name: "index_users_on_username"
t.index ["username"], name: "index_users_on_username_trigram", opclass: :gin_trgm_ops, using: :gin
end
@@ -3780,6 +3802,7 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
+ add_foreign_key "ci_pipelines", "external_pull_requests", name: "fk_190998ef09", on_delete: :nullify
add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
@@ -3841,9 +3864,10 @@ ActiveRecord::Schema.define(version: 2019_09_02_160015) do
add_foreign_key "epics", "users", column: "assignee_id", name: "fk_dccd3f98fc", on_delete: :nullify
add_foreign_key "epics", "users", column: "author_id", name: "fk_3654b61b03", on_delete: :cascade
add_foreign_key "epics", "users", column: "closed_by_id", name: "fk_aa5798e761", on_delete: :nullify
- add_foreign_key "events", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "events", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
+ add_foreign_key "external_pull_requests", "projects", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
add_foreign_key "fork_network_members", "projects", on_delete: :cascade
diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md
index 64eca0b00f6..6620989983f 100644
--- a/doc/administration/database_load_balancing.md
+++ b/doc/administration/database_load_balancing.md
@@ -146,7 +146,11 @@ The following options can be set:
| `use_tcp` | Lookup DNS resources using TCP instead of UDP | false |
If `record_type` is set to `SRV`, GitLab will continue to use a round-robin algorithm
-and will ignore the `weight` and `priority` in the record.
+and will ignore the `weight` and `priority` in the record. Since SRV records usually
+return hostnames instead of IPs, GitLab will look for the IPs of returned hostnames
+in the additional section of the SRV response. If no IP is found for a hostname, GitLab
+will query the configured `nameserver` for ANY record for each such hostname looking for A or AAAA
+records, eventually dropping this hostname from rotation if it can't resolve its IP.
The `interval` value specifies the _minimum_ time between checks. If the A
record has a TTL greater than this value, then service discovery will honor said
@@ -212,28 +216,25 @@ without it immediately leading to errors being presented to the users.
## Logging
-The load balancer logs various messages, such as:
+The load balancer logs various events in
+[`database_load_balancing.log`](logs.md#database_load_balancinglog-premium-only), such as
- When a host is marked as offline
- When a host comes back online
- When all secondaries are offline
+- When a read is retried on a different host due to a query conflict
-Each log message contains the tag `[DB-LB]` to make searching/filtering of such
-log entries easier. For example:
+The log is structured with each entry a JSON object containing at least:
-```
-[DB-LB] Host 10.123.2.5 came back online
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Host 10.123.2.6 came back online
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Host 10.123.2.7 came back online
-[DB-LB] Host 10.123.2.7 came back online
+- An `event` field useful for filtering.
+- A human-readable `message` field.
+- Some event-specific metadata. For example, `db_host`
+- Contextual information that is always logged. For example, `severity` and `time`.
+
+For example:
+
+```json
+{"severity":"INFO","time":"2019-09-02T12:12:01.728Z","correlation_id":"abcdefg","event":"host_online","message":"Host came back online","db_host":"111.222.333.444","db_port":null,"tag":"rails.database_load_balancing","environment":"production","hostname":"web-example-1","fqdn":"gitlab.example.com","path":null,"params":null}
```
## Handling Stale Reads
diff --git a/doc/administration/index.md b/doc/administration/index.md
index d557068e6c8..b58291b7478 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -104,6 +104,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
## User settings and permissions
+- [Creating users](../user/profile/account/create_accounts.md): Create users manually or through authentication integrations.
- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index df6c554decb..318711fd281 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -54,6 +54,21 @@ http://localhost:8080/plantuml
you can change these defaults by editing the `/etc/tomcat7/server.xml` file.
+### Making local PlantUML accessible using custom GitLab setup
+
+The PlantUML server runs locally on your server, so it is not accessible
+externally. As such, it is necessary to catch external PlantUML calls and
+redirect them to the local server.
+
+The idea is to redirect each call to `https://gitlab.example.com/-/plantuml/`
+to the local PlantUML server `http://localhost:8080/plantuml`.
+
+To enable the redirection, add the following line in `/etc/gitlab/gitlab.rb`:
+
+```ruby
+nginx['custom_gitlab_server_config'] = "location /-/plantuml { \n proxy_cache off; \n proxy_pass http://127.0.0.1:8080; \n}\n"
+```
+
## GitLab
You need to enable PlantUML integration from Settings under Admin Area. To do
@@ -62,7 +77,7 @@ that, login with an Admin account and do following:
- In GitLab, go to **Admin Area > Settings > Integrations**.
- Expand the **PlantUML** section.
- Check **Enable PlantUML** checkbox.
-- Set the PlantUML instance as **PlantUML URL**.
+- Set the PlantUML instance as `https://gitlab.example.com/-/plantuml/`.
## Creating Diagrams
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 4c43a434817..c51b53c596e 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -339,11 +339,11 @@ installations from source.
[Rack Attack]: ../security/rack_attack.md
[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md
-## `database_load_balancing.log`
+## `database_load_balancing.log` **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15442) in GitLab 12.3.
-This log is used for observability of [Database Load Balancing](database_load_balancing.md).
+Contains details of GitLab's [Database Load Balancing](database_load_balancing.md).
It is stored at:
- `/var/log/gitlab/gitlab-rails/database_load_balancing.log` for Omnibus GitLab packages.
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index ca48326c6d0..5c348702ba2 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -7,9 +7,9 @@ installations from source you'll have to configure it yourself.
To enable the GitLab Prometheus metrics:
1. Log into GitLab as an administrator, and go to the Admin area.
-1. Click on the gear, then click on Settings.
-1. Find the `Metrics - Prometheus` section, and click `Enable Prometheus Metrics`
-1. [Restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart) for the changes to take effect
+1. Navigate to GitLab's **Settings > Metrics and profiling**.
+1. Find the **Metrics - Prometheus** section, and click **Enable Prometheus Metrics**.
+1. [Restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart) for the changes to take effect.
## Collecting the metrics
@@ -91,33 +91,6 @@ The following metrics can be controlled by feature flags:
|:---------------------------------------------------------------|:-------------------------------------------------------------------|
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
| `gitlab_transaction_allocated_memory_bytes` | `prometheus_metrics_transaction_allocated_memory` |
-| `gitlab_transaction_event_build_found_total` | `prometheus_transaction_event_build_found_total` |
-| `gitlab_transaction_event_build_invalid_total` | `prometheus_transaction_event_build_invalid_total` |
-| `gitlab_transaction_event_build_not_found_cached_total` | `prometheus_transaction_event_build_not_found_cached_total` |
-| `gitlab_transaction_event_build_not_found_total` | `prometheus_transaction_event_build_not_found_total` |
-| `gitlab_transaction_event_change_default_branch_total` | `prometheus_transaction_event_change_default_branch_total` |
-| `gitlab_transaction_event_create_repository_total` | `prometheus_transaction_event_create_repository_total` |
-| `gitlab_transaction_event_etag_caching_cache_hit_total` | `prometheus_transaction_event_etag_caching_cache_hit_total` |
-| `gitlab_transaction_event_etag_caching_header_missing_total` | `prometheus_transaction_event_etag_caching_header_missing_total` |
-| `gitlab_transaction_event_etag_caching_key_not_found_total` | `prometheus_transaction_event_etag_caching_key_not_found_total` |
-| `gitlab_transaction_event_etag_caching_middleware_used_total` | `prometheus_transaction_event_etag_caching_middleware_used_total` |
-| `gitlab_transaction_event_etag_caching_resource_changed_total` | `prometheus_transaction_event_etag_caching_resource_changed_total` |
-| `gitlab_transaction_event_fork_repository_total` | `prometheus_transaction_event_fork_repository_total` |
-| `gitlab_transaction_event_import_repository_total` | `prometheus_transaction_event_import_repository_total` |
-| `gitlab_transaction_event_push_branch_total` | `prometheus_transaction_event_push_branch_total` |
-| `gitlab_transaction_event_push_commit_total` | `prometheus_transaction_event_push_commit_total` |
-| `gitlab_transaction_event_push_tag_total` | `prometheus_transaction_event_push_tag_total` |
-| `gitlab_transaction_event_rails_exception_total` | `prometheus_transaction_event_rails_exception_total` |
-| `gitlab_transaction_event_receive_email_total` | `prometheus_transaction_event_receive_email_total` |
-| `gitlab_transaction_event_remote_mirrors_failed_total` | `prometheus_transaction_event_remote_mirrors_failed_total` |
-| `gitlab_transaction_event_remote_mirrors_finished_total` | `prometheus_transaction_event_remote_mirrors_finished_total` |
-| `gitlab_transaction_event_remote_mirrors_running_total` | `prometheus_transaction_event_remote_mirrors_running_total` |
-| `gitlab_transaction_event_remove_branch_total` | `prometheus_transaction_event_remove_branch_total` |
-| `gitlab_transaction_event_remove_repository_total` | `prometheus_transaction_event_remove_repository_total` |
-| `gitlab_transaction_event_remove_tag_total` | `prometheus_transaction_event_remove_tag_total` |
-| `gitlab_transaction_event_sidekiq_exception_total` | `prometheus_transaction_event_sidekiq_exception_total` |
-| `gitlab_transaction_event_stuck_import_jobs_total` | `prometheus_transaction_event_stuck_import_jobs_total` |
-| `gitlab_transaction_event_update_build_total` | `prometheus_transaction_event_update_build_total` |
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
## Sidekiq Metrics available for Geo **(PREMIUM)**
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 3ee8673b297..d2d7ebdd634 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -10,7 +10,7 @@ that can be:
- Mounted to the local disk
- Exposed as an NFS shared volume
-- Acessed via [gitaly] on its own machine.
+- Accessed via [gitaly] on its own machine.
In GitLab, this is configured in `/etc/gitlab/gitlab.rb` by the `git_data_dirs({})`
configuration hash. The storage layouts discussed here will apply to any shard
diff --git a/doc/administration/smime_signing_email.md b/doc/administration/smime_signing_email.md
index b2e3bf8487b..530553ec1c4 100644
--- a/doc/administration/smime_signing_email.md
+++ b/doc/administration/smime_signing_email.md
@@ -11,29 +11,56 @@ S/MIME signs and/or encrypts the message itself
## Enable S/MIME signing
This setting must be explicitly enabled and a single pair of key and certificate
-files must be provided in `gitlab.rb` or `gitlab.yml` if you are using Omnibus
-GitLab or installed GitLab from source respectively:
-
-```yaml
-email_smime:
- enabled: true
- key_file: /etc/pki/smime/private/gitlab.key
- cert_file: /etc/pki/smime/certs/gitlab.crt
-```
+files must be provided:
-- Both files must be provided PEM-encoded.
-- The key file must be unencrypted so that Gitlab can read it without user
+- Both files must be PEM-encoded.
+- The key file must be unencrypted so that GitLab can read it without user
intervention.
+- Only RSA keys are supported.
NOTE: **Note:** Be mindful of the access levels for your private keys and visibility to
third parties.
+**For Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and adapt the file paths:
+
+ ```ruby
+ gitlab_rails['gitlab_email_smime_enabled'] = true
+ gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
+ gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
+ ```
+
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+NOTE: **Note:** The key needs to be readable by the GitLab system user (`git` by default).
+
+**For installations from source:**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ email_smime:
+ # Uncomment and set to true if you need to enable email S/MIME signing (default: false)
+ enabled: true
+ # S/MIME private key file in PEM format, unencrypted
+ # Default is '.gitlab_smime_key' relative to Rails.root (i.e. root of the GitLab app).
+ key_file: /etc/pki/smime/private/gitlab.key
+ # S/MIME public certificate key in PEM format, will be attached to signed messages
+ # Default is '.gitlab_smime_cert' relative to Rails.root (i.e. root of the GitLab app).
+ cert_file: /etc/pki/smime/certs/gitlab.crt
+ ```
+
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
+
+NOTE: **Note:** The key needs to be readable by the GitLab system user (`git` by default).
+
### How to convert S/MIME PKCS#12 / PFX format to PEM encoding
Typically S/MIME certificates are handled in binary PKCS#12 format (`.pfx` or `.p12`
extensions), which contain the following in a single encrypted file:
-- Server certificate
+- Public certificate
- Intermediate certificates (if any)
- Private key
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 9af5430f1c8..e2ddc2cbc18 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -126,6 +126,7 @@ The following API resources are available outside of project and group contexts
| [Runners](runners.md) | `/runners` (also available for projects) |
| [Search](search.md) | `/search` (also available for groups and projects) |
| [Settings](settings.md) | `/application/settings` |
+| [Statistics](statistics.md) | `/application/statistics` |
| [Sidekiq metrics](sidekiq_metrics.md) | `/sidekiq` |
| [Suggestions](suggestions.md) | `/suggestions` |
| [System hooks](system_hooks.md) | `/hooks` |
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index b73fe38f53e..d19f11ba1d4 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -525,6 +525,77 @@ PUT /projects/:id/merge_requests/:merge_request_iid/approvers
}
```
+### Get the approval state of merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/13712) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
+
+You can request information about a merge request's approval state by using the following endpoint:
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/approval_state
+```
+
+The `approval_rules_overwritten` will be `true` if the merge request level rules
+are created for the merge request. If there's none, it'll be `false`.
+
+This includes additional information about the users who have already approved
+(`approved_by`) and whether a rule is already approved (`approved`).
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
+
+```json
+{
+ "approval_rules_overwritten": true,
+ "rules": [
+ {
+ "id": 1,
+ "name": "Ruby",
+ "rule_type": "regular",
+ "eligible_approvers": [
+ {
+ "id": 4,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "approvals_required": 2,
+ "users": [
+ {
+ "id": 4,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "groups": [],
+ "contains_hidden_groups": false,
+ "approved_by": [
+ {
+ "id": 4,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "source_rule": null,
+ "approved": true
+ }
+ ]
+}
+```
+
### Get merge request level rules
>**Note:** This API endpoint is only available on 12.3 Starter and above.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index cf28ea84704..3ea1434d5ca 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -929,7 +929,7 @@ POST /projects
| `ci_config_path` | string | no | The path to CI config file |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
-| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
| `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds |
@@ -986,7 +986,7 @@ POST /projects/user/:user_id
| `ci_config_path` | string | no | The path to CI config file |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
-| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
@@ -1043,7 +1043,7 @@ PUT /projects/:id
| `ci_default_git_depth` | integer | no | Default number of revisions for [shallow cloning](../user/project/pipelines/settings.md#git-shallow-clone) |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
-| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge request by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index a14b0d3632a..4ad4ebdacb6 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -210,7 +210,7 @@ are listed in the descriptions of the relevant settings.
| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). |
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |
-| `domain_blacklist` | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
+| `domain_blacklist` | array of strings | no | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
| `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. |
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
diff --git a/doc/api/statistics.md b/doc/api/statistics.md
new file mode 100644
index 00000000000..5078b2f26d4
--- /dev/null
+++ b/doc/api/statistics.md
@@ -0,0 +1,35 @@
+# Application statistics API
+
+## Get current application statistics
+
+List the current statistics of the GitLab instance. You have to be an
+administrator in order to perform this action.
+
+NOTE: **Note:**
+These statistics are approximate.
+
+```
+GET /application/statistics
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/statistics
+```
+
+Example response:
+
+```json
+{
+ "forks": "10",
+ "issues": "76",
+ "merge_requests": "27",
+ "notes": "954",
+ "snippets": "50",
+ "ssh_keys": "10",
+ "milestones": "40",
+ "users": "50",
+ "groups": "10",
+ "projects": "20",
+ "active_users": "50"
+}
+```
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 1d874fea1f8..88f63d6b34b 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -46,7 +46,8 @@ Parameters:
},
"name": "v1.0.0",
"target": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": null
+ "message": null,
+ "protected": true
}
]
```
@@ -94,7 +95,8 @@ Example Response:
"committer_email": "contact@arthurverschaeve.be",
"committed_date": "2015-02-01T21:56:31.000+01:00"
},
- "release": null
+ "release": null,
+ "protected": false
}
```
@@ -138,7 +140,8 @@ Parameters:
},
"name": "v1.0.0",
"target": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": null
+ "message": null,
+ "protected": false
}
```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 5f875528a6c..b6059c71b27 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -23,8 +23,8 @@ Below are the changes made between V3 and V4.
- Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
- - `/licences`
- - `/licences/:key`
+ - `/licenses`
+ - `/licenses/:key`
- `/gitignores`
- `/gitlab_ci_ymls`
- `/dockerfiles`
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index a48da557e09..fc0125fcc18 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -721,6 +721,13 @@ deploy:
- master
```
+NOTE: **Note:**
+This example explicitly calls `docker pull`. If you prefer to implicitly pull the
+built image using `image:`, and use either the [Docker](https://docs.gitlab.com/runner/executors/docker.html)
+or [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html) executor,
+make sure that [`pull_policy`](https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work)
+is set to `always`.
+
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
[2fa]: ../../user/profile/account/two_factor_authentication.md
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 00995f881da..f9612d0c53d 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -22,7 +22,7 @@ The following table lists examples with step-by-step tutorials that are containe
| Use case | Resource |
|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
-| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). |
+| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](../../user/project/merge_requests/browser_performance_testing.md). |
| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
| Deployment with Dpl | [Using `dpl` as deployment tool](deployment/README.md). |
| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index 3266e5dc62e..4a73fe2e62c 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -1,158 +1,5 @@
---
-type: howto
+redirect_to: '../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing'
---
-# Browser Performance Testing with the sitespeed.io container
-
-NOTE: **Note:**
-The job definition shown below is supported on GitLab 11.5 and later versions.
-It also requires the GitLab Runner 11.5 or later.
-For earlier versions, use the [previous job definitions](#previous-job-definitions).
-
-This example shows how to run the
-[sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on
-your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
-using Docker-in-Docker.
-
-First, you need GitLab Runner with
-[docker-in-docker build](../docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
-generates the expected report:
-
-```yaml
-performance:
- stage: performance
- image: docker:git
- variables:
- URL: https://example.com
- services:
- - docker:stable-dind
- script:
- - mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
- - mv sitespeed-results/data/performance.json performance.json
- artifacts:
- paths:
- - sitespeed-results/
- reports:
- performance: performance.json
-```
-
-The above example will create a `performance` job in your CI/CD pipeline and will run
-sitespeed.io against the webpage you defined in `URL` to gather key metrics.
-The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for
-sitespeed.io is downloaded in order to save the report as a
-[Performance report artifact](../yaml/README.md#artifactsreportsperformance-premium)
-that you can later download and analyze.
-Due to implementation limitations we always take the latest Performance artifact available.
-
-The full HTML sitespeed.io report will also be saved as an artifact, and if you have
-[GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed
-directly in your browser.
-
-For further customization options for sitespeed.io, including the ability to
-provide a list of URLs to test, please see the
-[Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) documentation.
-
-TIP: **Tip:**
-For [GitLab Premium](https://about.gitlab.com/pricing/) users, key metrics are automatically
-extracted and shown right in the merge request widget.
-[Learn more on Browser Performance Testing in merge requests](../../user/project/merge_requests/browser_performance_testing.md).
-
-## Performance testing on Review Apps
-
-The above CI YML is great for testing against static environments, and it can
-be extended for dynamic environments. There are a few extra steps to take to
-set this up:
-
-1. The `performance` job should run after the dynamic environment has started.
-1. In the `review` job, persist the hostname and upload it as an artifact so
- it's available to the `performance` job (the same can be done for static
- environments like staging and production to unify the code path). Saving it
- as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
- in your job's `script`.
-1. In the `performance` job, read the previous artifact into an environment
- variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
- URLs.
-1. You can now run the sitespeed.io container against the desired hostname and
- paths.
-
-Your `.gitlab-ci.yml` file would look like:
-
-```yaml
-stages:
- - deploy
- - performance
-
-review:
- stage: deploy
- environment:
- name: review/$CI_COMMIT_REF_SLUG
- url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN
- script:
- - run_deploy_script
- - echo $CI_ENVIRONMENT_URL > environment_url.txt
- artifacts:
- paths:
- - environment_url.txt
- only:
- - branches
- except:
- - master
-
-performance:
- stage: performance
- image: docker:git
- services:
- - docker:stable-dind
- dependencies:
- - review
- script:
- - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
- - mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
- - mv sitespeed-results/data/performance.json performance.json
- artifacts:
- paths:
- - sitespeed-results/
- reports:
- performance: performance.json
-```
-
-A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml).
-
-## Previous job definitions
-
-CAUTION: **Caution:**
-Before GitLab 11.5, Performance job and artifact had to be named specifically
-to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
-and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
-
-For GitLab 11.4 and earlier, the job should look like:
-
-```yaml
-performance:
- stage: performance
- image: docker:git
- variables:
- URL: https://example.com
- services:
- - docker:stable-dind
- script:
- - mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
- - mv sitespeed-results/data/performance.json performance.json
- artifacts:
- paths:
- - performance.json
- - sitespeed-results/
-```
+This document was moved to [another location](../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing).
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
index 126e12e460f..d68fba82f4b 100644
--- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
@@ -61,6 +61,20 @@ CAUTION: **Warning:**
Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](../index.md#configuring-pipelines-for-merge-requests),
otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state.
+## Automatic pipeline cancelation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12996) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+
+GitLab CI can detect the presence of redundant pipelines,
+and will cancel them automatically in order to conserve CI resources.
+
+When a user merges a merge request immediately within an ongoing merge
+train, the train will be reconstructed, as it will recreate the expected
+post-merge commit and pipeline. In this case, the merge train may already
+have pipelines running against the previous expected post-merge commit.
+These pipelines are considered redundant and will be automatically
+canceled.
+
## Troubleshooting
### Pipelines for merged results not created even with new change pushed to merge request
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index 377ae9717b2..9e18d1d883c 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -178,6 +178,8 @@ the ones defined in the upstream project will take precedence.
### Mirroring status from triggered pipeline
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+
You can mirror the pipeline status from the triggered pipeline to the source
bridge job by using `strategy: depend`. For example:
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 409f7d62538..a3c253cc517 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -100,6 +100,7 @@ future GitLab releases.**
| `CI_RUNNER_REVISION` | all | 10.6 | GitLab Runner revision that is executing the current job |
| `CI_RUNNER_TAGS` | 8.10 | 0.5 | The defined runner tags |
| `CI_RUNNER_VERSION` | all | 10.6 | GitLab Runner version that is executing the current job |
+| `CI_RUNNER_SHORT_TOKEN` | all | 12.3 | First eight characters of GitLab Runner's token used to authenticate new job requests. Used as Runner's unique ID |
| `CI_SERVER` | all | all | Mark that job is executed in CI environment |
| `CI_SERVER_HOST` | 12.1 | all | Host component of the GitLab instance URL, without protocol and port (like gitlab.example.com) |
| `CI_SERVER_NAME` | all | all | The name of CI server that is used to coordinate jobs |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index bfba21646b5..8aae0e85c89 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -116,6 +116,7 @@ The following table lists available parameters for jobs:
| [`extends`](#extends) | Configuration entries that this job is going to inherit from. |
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
| [`variables`](#variables) | Define job variables on a job level. |
+| [interruptible](#interruptible) | Defines if a job can be canceled when made redundant by a newer run |
NOTE: **Note:**
Parameters `types` and `type` are [deprecated](#deprecated-parameters).
@@ -2083,6 +2084,48 @@ staging:
branch: stable
```
+### `interruptible`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23464) in GitLab 12.3.
+
+`interruptible` is used to indicate that a job should be canceled if made redundant by a newer run of the same job. Defaults to `true`.
+This value will only be used if the [automatic cancellation of redundant pipelines feature](https://docs.gitlab.com/ee/user/project/pipelines/settings.html#auto-cancel-pending-pipelines)
+is enabled.
+
+When enabled, a pipeline on the same branch will be canceled when:
+
+- It is made redundant by a newer pipeline run.
+- Either all jobs are set as interruptible, or any uninterruptible jobs are not yet pending.
+
+Pending jobs are always considered interruptible.
+
+TIP: **Tip:**
+Set jobs as uninterruptible that should behave atomically and should never be canceled once started.
+
+Here is a simple example:
+
+```yaml
+stages:
+ - stage1
+ - stage2
+
+step-1:
+ stage: stage1
+ script:
+ - echo "Can be canceled"
+
+step-2:
+ stage: stage2
+ script:
+ - echo "Can not be canceled"
+ interruptible: false
+```
+
+In the example above, a new pipeline run will cause an existing running pipeline to be:
+
+- Canceled, if only `step-1` is running or pending.
+- Not canceled, once `step-2` becomes pending.
+
### `include`
> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 7569ccc04c1..c3165dc2e21 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -191,6 +191,51 @@ end
policies at once. The fields for these will all have be non-nullable
booleans with a default description.
+## Enums
+
+GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the
+following rules apply:
+
+- Values must be uppercase.
+- Class names must end with the string `Enum`.
+- The `graphql_name` must not contain the string `Enum`.
+
+For example:
+
+```ruby
+module Types
+ class TrafficLightStateEnum < BaseEnum
+ graphql_name 'TrafficLightState'
+ description 'State of a traffic light'
+
+ value 'RED', description: 'Drivers must stop'
+ value 'YELLOW', description: 'Drivers must stop when it is safe to'
+ value 'GREEN', description: 'Drivers can start or keep driving'
+ end
+end
+```
+
+If the enum will be used for a class property in Ruby that is not an uppercase string,
+you can provide a `value:` option that will adapt the uppercase value.
+
+In the following example:
+
+- GraphQL inputs of `OPENED` will be converted to `'opened'`.
+- Ruby values of `'opened'` will be converted to `"OPENED"` in GraphQL responses.
+
+```ruby
+module Types
+ class EpicStateEnum < BaseEnum
+ graphql_name 'EpicState'
+ description 'State of a GitLab epic'
+
+ value 'OPENED', value: 'opened', description: 'An open Epic'
+ value 'CLOSED', value: 'closed', description: 'An closed Epic'
+ end
+end
+
+```
+
## Authorization
Authorizations can be applied to both types and fields using the same
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index ee5fc553e27..147bd21e6c7 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -61,8 +61,8 @@ graph TB
Unicorn --> PgBouncer[PgBouncer]
Unicorn --> Redis
Unicorn --> Gitaly
- Redis --> Sidekiq
- Sidekiq["Sidekiq (GitLab Rails, ES Indexer)"] --> PgBouncer
+ Sidekiq --> Redis
+ Sidekiq --> PgBouncer
GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis
GitLabWorkhorse --> Gitaly
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index a456bbc781f..606ee431c3e 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -302,18 +302,18 @@ for more details.
## Best practices
-1. Make sure to know how much data you're dealing with
+1. Make sure to know how much data you're dealing with.
1. Make sure that background migration jobs are idempotent.
1. Make sure that tests you write are not false positives.
1. Make sure that if the data being migrated is critical and cannot be lost, the
clean-up migration also checks the final state of the data before completing.
-1. Make sure to know how much time it'll take to run all scheduled migrations
+1. Make sure to know how much time it'll take to run all scheduled migrations.
1. When migrating many columns, make sure it won't generate too many
dead tuples in the process (you may need to directly query the number of dead tuples
- and adjust the scheduling according to this piece of data)
+ and adjust the scheduling according to this piece of data).
1. Make sure to discuss the numbers with a database specialist, the migration may add
more pressure on DB than you expect (measure on staging,
- or ask someone to measure on production)
+ or ask someone to measure on production).
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 814624c7586..5667f58b0c3 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -99,7 +99,7 @@ automatically.
Its simplest usage is to provide the value for `title`:
```text
-$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
+bin/changelog 'Hey DZ, I added a feature to GitLab!'
```
At this point the script would ask you to select the category of the change (mapped to the `type` field in the entry):
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index bcfc0734c06..0afa44a5491 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -72,7 +72,7 @@ from teams other than your own.
1. If your merge request includes adding a new UI/UX paradigm [^1], it must be
**approved by a [UX lead][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must be
- **approved by a [Distribution team member][team]**. See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/dev-backend/distribution/) for more details.
+ **approved by a [Distribution team member][team]**. See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/#how-to-work-with-distribution) for more details.
#### Security requirements
diff --git a/doc/development/dangerbot.md b/doc/development/dangerbot.md
index 5fc5886e3a2..6bf59209d21 100644
--- a/doc/development/dangerbot.md
+++ b/doc/development/dangerbot.md
@@ -77,7 +77,7 @@ complex logic related to that task.
Danger code is just Ruby code. It should adhere to our coding standards, and
needs tests, like any other piece of Ruby in our codebase. However, we aren't
-able to test a `Dangerfile` directly! So, to maximise test coverage, try to
+able to test a `Dangerfile` directly! So, to maximize test coverage, try to
minimize the number of lines of code in `danger/`. A non-trivial `Dangerfile`
should mostly call plugin code with arguments derived from the methods provided
by Danger. The plugin code itself should have unit tests.
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index 4776c8348d4..d2810fe89f0 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -27,7 +27,7 @@ no overhead at all.
To enable `GITLAB_TRACING`, a valid _"configuration-string"_ value should be set, with a URL-like
form:
-```console
+```sh
GITLAB_TRACING=opentracing://<driver>?<param_name>=<param_value>&<param_name_2>=<param_value_2>
```
@@ -90,7 +90,7 @@ documentation](https://www.jaegertracing.io/docs/1.9/getting-started/).
If you have Docker available, the easier approach to running the Jaeger all-in-one is through
Docker, using the following command:
-```console
+```sh
$ docker run \
--rm \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
@@ -121,8 +121,8 @@ appropriate configuration string.
**TL;DR:** If you are running everything on the same host, use the following value:
-```console
-$ export GITLAB_TRACING="opentracing://jaeger?http_endpoint=http%3A%2F%2Flocalhost%3A14268%2Fapi%2Ftraces&sampler=const&sampler_param=1"
+```sh
+export GITLAB_TRACING="opentracing://jaeger?http_endpoint=http%3A%2F%2Flocalhost%3A14268%2Fapi%2Ftraces&sampler=const&sampler_param=1"
```
This configuration string uses the Jaeger driver `opentracing://jaeger` with the following options:
@@ -152,7 +152,7 @@ application.
When `GITLAB_TRACING` is configured properly, the application will log this on startup:
-```console
+```sh
13:41:53 gitlab-workhorse.1 | 2019/02/12 13:41:53 Tracing enabled
...
13:41:54 gitaly.1 | 2019/02/12 13:41:54 Tracing enabled
@@ -161,7 +161,7 @@ When `GITLAB_TRACING` is configured properly, the application will log this on s
If `GITLAB_TRACING` is not configured correctly, this will also be logged:
-```console
+```sh
13:43:45 gitaly.1 | 2019/02/12 13:43:45 skipping tracing configuration step: tracer: unable to load driver mytracer
```
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 719b9aa212a..c267669aa79 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -463,7 +463,7 @@ The current tests are:
to [check locally](#previewing-the-changes-live) before pushing to GitLab by executing the command
`bundle exec nanoc check internal_links` on your local
[`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory. In addition,
- `docs-lint` also runs [markdownlint](styleguide.md#markdown-rules) to ensure the
+ `docs-lint` also runs [`markdownlint`](#markdownlint) to ensure the
markdown is consistent across all documentation.
1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-ee-merge-conflicts-beforehand) (runs on CE only):
When you submit a merge request to GitLab Community Edition (CE),
@@ -488,7 +488,7 @@ help you catch common issues before raising merge requests for review of documen
The following are some suggested linters you can install locally and sample configuration:
- [`proselint`](#proselint)
-- [`markdownlint`](#markdownlint)
+- [`markdownlint`](#markdownlint), which is the same as the test run in [`docs-lint`](#testing)
NOTE: **Note:**
This list does not limit what other linters you can add to your local documentation writing toolchain.
@@ -534,76 +534,56 @@ A file with `proselint` configuration must be placed in a
#### `markdownlint`
-`markdownlint` checks that certain rules ([example](https://github.com/DavidAnson/markdownlint/blob/master/README.md#rules--aliases))
-are followed for Markdown syntax. Our [Documentation Style Guide](styleguide.md) and
-[Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/)
+[`markdownlint`](https://github.com/DavidAnson/markdownlint) checks that markdown
+syntax follows [certain rules](https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#rules),
+and is used by the [`docs-lint` test](#testing) with a [configuration file](#markdownlint-configuration).
+Our [Documentation Style Guide](styleguide.md#markdown) and [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/)
elaborate on which choices must be made when selecting Markdown syntax for GitLab
-documentation. This tool helps catch deviations from those guidelines, and matches the
-tests run on the documentation by [`docs-lint`](#testing).
+documentation. This tool helps catch deviations from those guidelines.
`markdownlint` can be used [on the command line](https://github.com/igorshubovych/markdownlint-cli#markdownlint-cli--),
either on a single Markdown file or on all Markdown files in a project. For example, to run
`markdownlint` on all documentation in the [`gitlab-ce` project](https://gitlab.com/gitlab-org/gitlab-ce),
-run the following commands from within the `gitlab-ce` project:
+run the following commands from within your `gitlab-ce` project root directory, which will
+automatically detect the [`.markdownlint.json`](#markdownlint-configuration) config
+file in the root of the project, and test all files in `/doc` and its subdirectories:
```sh
-cd doc
-markdownlint **/*.md
+markdownlint 'doc/**/*.md'
```
-`markdownlint` can also be run from within editors using plugins. For example, the following plugins
- are available:
+If you wish to use a different config file, use the `-c` flag:
+
+```sh
+markdownlint -c <config-file-name> 'doc/**/*.md'
+```
+
+`markdownlint` can also be run from within text editors using [plugins/extensions](https://github.com/DavidAnson/markdownlint#related),
+such as:
- [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-markdownlint)
- [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint)
-- [Others](https://github.com/DavidAnson/markdownlint#related)
+- [Atom](https://atom.io/packages/linter-node-markdownlint)
-##### Sample `markdownlint` configuration
+It is best to use the [same configuration file](#markdownlint-configuration) as what
+is in use in the four repos that are the sources for <https://docs.gitlab.com>. Each
+plugin/extension has different requirements regarding the configuration file, which
+is explained in each editor's docs.
-The following sample `markdownlint` configuration modifies the available default rules to:
+##### `markdownlint` configuration
-- Adhere to the [Documentation Style Guide](styleguide.md).
-- Apply conventions found in the GitLab documentation.
-- Allow the flexibility of using some inline HTML.
+Each formatting issue that `markdownlint` checks has an associated
+[rule](https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#rules).
+These rules are configured in the `.markdownlint.json` files located in the root of
+four repos that are the sources for <https://docs.gitlab.com>:
-```json
-{
- "default": true,
- "header-style": { "style": "atx" },
- "ul-style": { "style": "dash" },
- "line-length": false,
- "no-trailing-punctuation": false,
- "ol-prefix": { "style": "one" },
- "blanks-around-fences": true,
- "no-inline-html": {
- "allowed_elements": [
- "table",
- "tbody",
- "tr",
- "td",
- "ul",
- "ol",
- "li",
- "br",
- "img",
- "a",
- "strong",
- "i",
- "div",
- "b"
- ]
- },
- "hr-style": { "style": "---" },
- "code-block-style": { "style": "fenced" },
- "fenced-code-language": false,
- "no-duplicate-header": { "allow_different_nesting": true },
- "commands-show-output": false
-}
-```
+- <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.markdownlint.json>
+- <https://gitlab.com/gitlab-org/gitlab-runner/blob/master/.markdownlint.json>
+- <https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/.markdownlint.json>
+- <https://gitlab.com/charts/gitlab/blob/master/.markdownlint.json>
-For [`markdownlint`](https://github.com/DavidAnson/markdownlint/), this configuration must be
-placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
-example, `~/.markdownlintrc`.
+By default all rules are enabled, so the configuration file is used to disable unwanted
+rules, and also to configure optional parameters for enabled rules as needed.
## Danger Bot
diff --git a/doc/development/documentation/site_architecture/global_nav.md b/doc/development/documentation/site_architecture/global_nav.md
index 753a636a779..be4d5b5353f 100644
--- a/doc/development/documentation/site_architecture/global_nav.md
+++ b/doc/development/documentation/site_architecture/global_nav.md
@@ -4,30 +4,74 @@ description: "Learn how GitLab docs' global navigation works and how to add new
# Global navigation
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-docs/merge_requests/362)
-in GitLab 11.6.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-docs/merge_requests/362) in GitLab 11.6.
> - [Updated](https://gitlab.com/gitlab-org/gitlab-docs/merge_requests/482) in GitLab 12.1.
+> - [Per-project](https://gitlab.com/gitlab-org/gitlab-docs/merge_requests/498) navigation added in GitLab 12.2.
-The global nav adds to the left sidebar the ability to
-navigate and explore the contents of GitLab's documentation.
+Global navigation (the left-most pane in our three pane documentation) provides:
-The global nav should be maintained consistent through time to allow the
-users to locate their most-visited links easily to facilitate navigation.
-Therefore, any updates must be carefully considered by the technical writers.
+- A high-level grouped view of product features.
+- The ability to discover new features by browsing the menu structure.
+- A way to allow the reader to focus on product areas.
+- The ability to refine landing pages, so they don't have to do all the work of surfacing
+ every page contained within the documentation.
-## Adding new items to the global nav
+## Adding new items
-To add a new doc to the nav, first and foremost, check with the technical writing team:
+All new pages need a new navigation item. Without a navigation, the page becomes "orphaned". That
+is:
-- If it's applicable
-- What's the exact position the doc will be added to the nav
+- The navigation shuts when the page is opened, and the reader loses their place.
+- The page doesn't belong in a group with other pages.
-Once you get their approval and their guidance in regards to the position on the nav,
-read trhough this page to understand how it works, and submit a merge request to the
-docs site, adding the doc you wish to include in the nav into the
-[global nav data file](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/content/_data/global-nav.yaml).
+This means the decision to create a new page is a decision to create new navigation item and vice
+versa.
-Don't forget to ask a technical writer to review your changes before merging.
+### Where to add
+
+Documentation pages can be said to belong in the following groups:
+
+- GitLab users. This is documentation for day-to-day use of GitLab for users with any level
+ of permissions, from Reporter to Owner.
+- GitLab administrators. This tends to be documentation for self-managed instances that requires
+ access to the underlying infrastructure hosting GitLab.
+- Other documentation. This includes documentation for customers outside their day-to-day use of
+ GitLab and for contributors. Documentation that doesn't fit in the other groups belongs here.
+
+With these groups in mind, the following are general rules for where new items should be added.
+
+- User documentation for:
+ - Group-level features belongs under **Groups**.
+ - Project-level features belongs under **Projects**.
+ - Features outside a group or project level (sometimes called "instance-level") can be placed at
+ the top-level, but care must be taken not to overwhelm that top-level space. If possible, such
+ features could be grouped in some way.
+ - Outside the above, most other miscellaneous user documentation belongs under **User**.
+- Administration documentation belongs under **Administrator**.
+- Other documentation belongs at the top-level, but care must be taken to not create an enormously
+ long top-level navigation, which defeats the purpose of it.
+
+NOTE: **Note:**
+Making all documentation and navigation items adhere to these principles is being progressively
+rolled out.
+
+### What to add
+
+Having decided where to add a navigation element, the next step is deciding what to add. The
+mechanics of what is required is [documented below](#data-file) but, in principle:
+
+- Navigation item text (that which the reader sees) should:
+ - Be as short as possible.
+ - Be contextual. It's rare to need to repeat text from a parent item.
+ - Avoid jargon or terms of art, unless ubiquitous. For example, **CI** is an acceptable
+ substitution for **Continuous Integration**.
+- Navigation links must follow the rules documented in the [data file](#data-file).
+- EE badging is subject to the following:
+ - Required when linking to an EE-only page.
+ - Not required when linking to a page that is a mix of CE and EE-only content.
+ - Required when all sub-items are EE-only. In this case, no sub-items are EE badged.
+ - Not required when sub-items are a mix of CE and EE-only items. In this case, each item is
+ badged appropriately.
## How it works
@@ -55,7 +99,7 @@ To see the improvements planned, check the
[global nav epic](https://gitlab.com/groups/gitlab-com/-/epics/21).
CAUTION: **Attention!**
-**Do not** [add items](#adding-new-items-to-the-global-nav) to the global nav without
+**Do not** [add items](#adding-new-items) to the global nav without
the consent of one of the technical writers.
## Composition
@@ -70,8 +114,13 @@ the data among the nav in containers properly [styled](#css-classes).
### Data file
-The [data file](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/content/_data/global-nav.yaml)
-is structured in three components: sections, categories, and docs.
+The data file describes the structure of the navigation for the applicable project. All data files
+are stored at <https://gitlab.com/gitlab-org/gitlab-docs/blob/master/content/_data/> and comprise
+three components:
+
+- Sections
+- Categories
+- Docs
#### Sections
diff --git a/doc/development/documentation/site_architecture/index.md b/doc/development/documentation/site_architecture/index.md
index 1aef0ed855c..bb598906967 100644
--- a/doc/development/documentation/site_architecture/index.md
+++ b/doc/development/documentation/site_architecture/index.md
@@ -66,10 +66,12 @@ the GitLab Documentation website.
- [Google Analytics](https://marketingplatform.google.com/about/analytics/)
- [Google Tag Manager](https://developers.google.com/tag-manager/)
-## Global nav
+## Global navigation
-To understand how the global nav (left sidebar) is built, please
-read through the [global navigation](global_nav.md) doc.
+Read through the global navigation](global_nav.md) documentation to understand:
+
+- How the global navigation is built.
+- How to add new navigation items.
## Deployment
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 283e8bea8d5..39b5e191a7b 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -85,7 +85,7 @@ The more we reflexively add useful information to the docs, the more (and more s
If you have questions when considering, authoring, or editing docs, ask the Technical Writing team on Slack in `#docs` or in GitLab by mentioning the writer for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). Otherwise, forge ahead with your best effort. It does not need to be perfect; the team is happy to review and improve upon your content. Please review the [Documentation guidelines](index.md) before you begin your first documentation MR.
-Having a knowledge base is any form that is separate from the documentation would be against the docs-first methodology because the content would overlap with the documentation.
+Having a knowledge base in any form that is separate from the documentation would be against the docs-first methodology because the content would overlap with the documentation.
## Markdown
@@ -110,18 +110,11 @@ Hard-coded HTML is valid, although it's discouraged to be used while we have `/h
### Markdown Rules
GitLab ensures that the Markdown used across all documentation is consistent, as
-well as easy to review and maintain, by testing documentation changes with
-[Markdownlint (mdl)](https://github.com/markdownlint/markdownlint). This lint test
-checks many common problems with Markdown, and fails when any document has an issue
+well as easy to review and maintain, by [testing documentation changes](index.md#testing) with
+[`markdownlint`](index.md#markdownlint). This lint test fails when any document has an issue
with Markdown formatting that may cause the page to render incorrectly within GitLab.
It will also fail when a document is using non-standard Markdown (which may render
-correctly, but is not the current standard in GitLab documentation).
-
-Each formatting issue that mdl checks has an associated [rule](https://github.com/markdownlint/markdownlint/blob/master/docs/RULES.md),
-and the rules that are currently enabled for GitLab documentation are listed in the
-[`.mdlrc.style`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.mdlrc.style) file.
-Configuration options are set in the [`.mdlrc`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.mdlrc.style)
-file.
+correctly, but is not the current standard for GitLab documentation).
## Structure
@@ -269,7 +262,7 @@ table_display_block: true
## Punctuation
Check the general punctuation rules for the GitLab documentation on the table below.
-Check specific punctuation rules for [list items](#list-items) below.
+Check specific punctuation rules for [lists](#lists) below.
| Rule | Example |
| ---- | ------- |
@@ -281,37 +274,44 @@ Check specific punctuation rules for [list items](#list-items) below.
| Always add a space before and after dashes when using it in a sentence (for replacing a comma, for example). | _You should try this - or not._ |
| Always use lowercase after a colon. | _Related Issues: a way to create a relationship between issues._ |
-## List items
+## Lists
- Always start list items with a capital letter, unless they are parameters or commands
that are in backticks, or similar.
- Always leave a blank line before and after a list.
- Begin a line with spaces (not tabs) to denote a [nested subitem](#nesting-inside-a-list-item).
-- Only use ordered lists when their items describe a sequence of steps to follow:
- Do:
+### Ordered vs. unordered lists
- These are the steps to do something:
+Only use ordered lists when their items describe a sequence of steps to follow.
- 1. First, do step 1
- 1. Then, do step 2
- 1. Finally, do step 3
+Do:
- Don't:
+```md
+These are the steps to do something:
- This is a list of different features:
+1. First, do the first step.
+1. Then, do the next step.
+1. Finally, do the last step.
+```
- 1. Feature 1
- 1. Feature 2
- 1. Feature 3
+Don't:
+
+```md
+This is a list of available features:
+
+1. Feature 1
+1. Feature 2
+1. Feature 3
+```
-**Markup:**
+### Markup
- Use dashes (`-`) for unordered lists instead of asterisks (`*`).
-- Prefix `1.` to each item in an ordered list.
+- Prefix `1.` to every item in an ordered list.
When rendered, the list items will appear with sequential numbering automatically.
-**Punctuation:**
+### Punctuation
- Do not add commas (`,`) or semicolons (`;`) to the end of list items.
- Only add periods to the end of a list item if the item consists of a complete sentence.
@@ -350,7 +350,7 @@ Do:
- Let's say this is also a complete sentence.
- Not a complete sentence.
-Don't (third item should have a `.` to match the first and second items):
+Don't (vary use of periods; majority rules):
- Let's say this is a complete sentence.
- Let's say this is also a complete sentence.
@@ -460,7 +460,7 @@ For other punctuation rules, please refer to the
This is to ensure that no document with wrong heading is going
live without an audit, thus preventing dead links and redirection issues when
corrected.
-- Leave exactly one new line after a heading.
+- Leave exactly one blank line before and after a heading.
- Do not use links in headings.
- Add the corresponding [product badge](#product-badges) according to the tier the feature belongs.
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index ae0e2361840..9224a2548ab 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -58,6 +58,24 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations.
```
+### Merge Request Review
+
+With the purpose of being [respectful of others' time](https://about.gitlab.com/handbook/values/#be-respectful-of-others-time) please follow these guidelines when asking for a review:
+
+- Make sure your Merge Request:
+ - milestone is set
+ - at least the labels suggested by danger-bot are set
+ - has a clear description
+ - includes before/after screenshots if there is a UI change
+ - pipeline is green
+ - includes tests
+ - includes a changelog entry (when necessary)
+- Before assigning to a maintainer, assign to a reviewer.
+- If you assigned a merge request, or pinged someone directly, keep in mind that we work in different timezones and asynchronously, so be patient. Unless the merge request is urgent (like fixing a broken master), please don't DM or reassign the merge request before waiting for a 24-hour window.
+- If you have a question regarding your merge request/issue, make it on the merge request/issue. When we DM each other, we no longer have a SSOT and [no one else is able to contribute](https://about.gitlab.com/handbook/values/#public-by-default).
+- When you have a big WIP merge request with many changes, you're adivsed to get the review started before adding/removing significant code. Make sure it is assigned well before the release cut-off, as the reviewer(s)/maintainer(s) would always prioritize reviewing finished MRs before WIP ones.
+- Make sure to remove the WIP title before the last round of review.
+
### Share your work early
1. Before writing code, ensure your vision of the architecture is aligned with
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 557d3132d71..336ef4ab278 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -13,7 +13,7 @@ _Note:_ The action itself will not update the state, only a mutation should upda
## File structure
-When using Vuex at GitLab, separate this concerns into different files to improve readability:
+When using Vuex at GitLab, separate these concerns into different files to improve readability:
```
└── store
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 777d372ec60..b22a63621dc 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -81,9 +81,7 @@ If you're using Pry you can use the `$` command to display the source code of a
method (along with its source location), this is easier than running the above
Ruby code. In case of the above snippet you'd run the following:
-```
-$ Banzai::Renderer.render
-```
+- `$ Banzai::Renderer.render`
This will print out something along the lines of:
diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md
index e1350b02262..9ac87a17232 100644
--- a/doc/development/integrations/jira_connect.md
+++ b/doc/development/integrations/jira_connect.md
@@ -10,8 +10,10 @@ The following are required to install and test the app:
For the app to work, Jira Cloud should be able to connect to the GitLab instance through the internet.
- To easily expose your local development environment, you can use tools like [serveo](https://serveo.net) or [ngrok](https://ngrok.com).
- These also take care of SSL for you because Jira requires all connections to the app host to be over SSL.
+ To easily expose your local development environment, you can use tools like
+ [serveo](https://medium.com/@osanda.deshan/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf)
+ or [ngrok](https://ngrok.com). These also take care of SSL for you because Jira
+ requires all connections to the app host to be over SSL.
> This feature is currently behind the `:jira_connect_app` feature flag
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 14b3f8204d2..6e6c80b7a7c 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -123,7 +123,7 @@ Keeping that in mind, to create a profile, identify (or create) a spec that
exercises the troublesome code path, then run it using the `bin/rspec-stackprof`
helper, e.g.:
-```
+```sh
$ LIMIT=10 bin/rspec-stackprof spec/policies/project_policy_spec.rb
8/8 |====== 100 ======>| Time: 00:00:18
@@ -157,22 +157,22 @@ it calls, were being executed.
To create a graphical view of the call stack:
-```shell
-$ stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot
-$ dot -Tsvg project_policy_spec.dot > project_policy_spec.svg
+```sh
+stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot
+dot -Tsvg project_policy_spec.dot > project_policy_spec.svg
```
To load the profile in [kcachegrind](https://kcachegrind.github.io/):
-```
-$ stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind
-$ kcachegrind project_policy_spec.callgrind # Linux
-$ qcachegrind project_policy_spec.callgrind # Mac
+```sh
+stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind
+kcachegrind project_policy_spec.callgrind # Linux
+qcachegrind project_policy_spec.callgrind # Mac
```
It may be useful to zoom in on a specific method, e.g.:
-```
+```sh
$ stackprof tmp/project_policy_spec.rb.dump --method warm_asset_cache
TestEnv#warm_asset_cache (/Users/lupine/dev/gitlab.com/gitlab-org/gitlab-development-kit/gitlab/spec/support/test_env.rb:164)
samples: 0 self (0.0%) / 6288 total (36.9%)
@@ -225,9 +225,9 @@ may have changed over time.
To activate profiling in your local environment, run the following:
-```
-$ export RSPEC_PROFILING=yes
-$ rake rspec_profiling:install
+```sh
+export RSPEC_PROFILING=yes
+rake rspec_profiling:install
```
This creates an SQLite3 database in `tmp/rspec_profiling`, into which statistics
@@ -237,7 +237,7 @@ variable set.
Ad-hoc investigation of the collected results can be performed in an interactive
shell:
-```
+```sh
$ rake rspec_profiling:console
irb(main):001:0> results.count
=> 231
diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md
index 850ea6b60ac..8820b54fa87 100644
--- a/doc/development/testing_guide/end_to_end/page_objects.md
+++ b/doc/development/testing_guide/end_to_end/page_objects.md
@@ -167,6 +167,18 @@ There are two supported methods of defining elements within a view.
Any existing `.qa-selector` class should be considered deprecated
and we should prefer the `data-qa-selector` method of definition.
+### Exceptions
+
+In some cases it might not be possible or worthwhile to add a selector.
+
+Some UI components use external libraries, including some maintained by third parties.
+Even if a library is maintained by GitLab, the selector sanity test only runs
+on code within the GitLab project, so it's not possible to specify the path for
+the view for code in a library.
+
+In such rare cases it's reasonable to use CSS selectors in page object methods,
+with a comment explaining why an `element` can't be added.
+
## Running the test locally
During development, you can run the `qa:selectors` test by running
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 7dc89a3fcdb..91004bf177d 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -20,9 +20,18 @@ We have started to migrate frontend tests to the [Jest](https://jestjs.io) testi
Jest tests can be found in `/spec/frontend` and `/ee/spec/frontend` in EE.
-It is not yet a requirement to use Jest. You can view the
-[epic](https://gitlab.com/groups/gitlab-org/-/epics/873) of issues
-we need to solve before being able to use Jest for all our needs.
+### When should I use Jest over Karma?
+
+If you need to update an existing Karma test file (found in `spec/javascripts`), you do not
+need to migrate the whole spec to Jest. Simply updating the Karma spec to test your change
+is fine. It is probably more appropriate to migrate to Jest in a separate merge request.
+
+If you need to create a new test file, we strongly recommend creating one in Jest. This will
+help support our migration and we think you'll love using Jest.
+
+As always, please use discretion. Jest solves a lot of issues we experienced in Karma and
+provides a better developer experience, however there are potentially unexpected issues
+which could arise (especially with testing against browser specific features).
### Differences to Karma
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index f4cee410066..00371057d3c 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -88,7 +88,7 @@ class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration[4.2]
end
def down
- cleanup_concurrent_column_rename :users, :updated_at_timestamp, :updated_at
+ undo_rename_column_concurrently :users, :updated_at, :updated_at_timestamp
end
end
```
@@ -118,7 +118,7 @@ class CleanupUsersUpdatedAtRename < ActiveRecord::Migration[4.2]
end
def down
- rename_column_concurrently :users, :updated_at_timestamp, :updated_at
+ undo_cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
end
end
```
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index a3f6f2b327c..0dc029a4cd1 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -47,14 +47,14 @@ to avoid getting this error, you need to remove all instances of the
**Omnibus Installation**
-```
-$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
+```sh
+sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
```
**Source Installation**
-```
-$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
+```sh
+bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
### Variables environment scopes
@@ -89,10 +89,10 @@ To downgrade a source installation, you need to replace the current remote of
your GitLab installation with the Community Edition's remote, fetch the latest
changes, and checkout the latest stable branch:
-```
-$ git remote set-url origin git@gitlab.com:gitlab-org/gitlab-ce.git
-$ git fetch --all
-$ git checkout 8-x-stable
+```sh
+git remote set-url origin git@gitlab.com:gitlab-org/gitlab-ce.git
+git fetch --all
+git checkout 8-x-stable
```
Remember to follow the correct [update guides](../update/README.md) to make
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 358ba971049..ddf2b2fb738 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -613,6 +613,9 @@ To back up GitLab:
sudo gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Restoring GitLab from a backup
To restore GitLab, first review the [restore documentation](../../raketasks/backup_restore.md#restore),
@@ -631,6 +634,9 @@ released, you can update your GitLab instance:
sudo gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
1. Update the repositories and install GitLab:
```sh
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index fbbe2a34952..cfd0fd48c70 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -225,7 +225,7 @@ First, we will create a new project to host our application. You can do this
either by running the CLI client:
```bash
-$ oc new-project gitlab
+oc new-project gitlab
```
or by using the web interface:
diff --git a/doc/integration/github.md b/doc/integration/github.md
index f19b3109d15..23dd67f6891 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -158,7 +158,7 @@ For installation from source:
You will also need to disable Git SSL verification on the server hosting GitLab.
```
-$ git config --global http.sslVerify false
+git config --global http.sslVerify false
```
For the changes to take effect, [reconfigure GitLab] if you installed
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index ef319f7f0ce..f1456146032 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -298,3 +298,33 @@ gitlab_rails['omniauth_allow_bypass_two_factor'] = ['twitter', 'google_oauth2']
omniauth:
allow_bypass_two_factor: ['twitter', 'google_oauth2']
```
+
+## Automatically sign in with provider
+
+You can add the `auto_sign_in_with_provider` setting to your
+GitLab configuration to automatically redirect login requests
+to your OmniAuth provider for authentication, thus removing the need to click a button
+before actually signing in.
+
+For example, when using the Azure integration, you would set the following
+to enable auto sign in.
+
+For Omnibus package:
+
+```ruby
+gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'azure_oauth2'
+```
+
+For installations from source:
+
+```yaml
+omniauth:
+ auto_sign_in_with_provider: azure_oauth2
+```
+
+Please keep in mind that every sign in attempt will be redirected to the OmniAuth provider,
+so you will not be able to sign in using local credentials. Make sure that at least one
+of the OmniAuth users has admin permissions.
+
+You may also bypass the auto signin feature by browsing to
+`https://gitlab.example.com/users/sign_in?auto_sign_in=false`.
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 50e491f29a2..4a96001f2de 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -77,6 +77,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r
If this fails you need to fix it before upgrading to 8.0. Also see
<https://about.gitlab.com/get-help/>
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### 2. Check source and target database types
Check what databases you use on your GitLab server and your CI server.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 59b775d13bd..c230bb413f2 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -80,6 +80,9 @@ Use this command if you've installed GitLab with the Omnibus package:
sudo gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
Use this if you've installed GitLab from source:
```sh
@@ -92,6 +95,9 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
If you are using the [GitLab helm chart](https://gitlab.com/gitlab-org/charts/gitlab) on a
Kubernetes cluster, you can run the backup task using `backup-utility` script on
the GitLab task runner pod via `kubectl`. Refer to [backing up a GitLab installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/backup.md#backing-up-a-gitlab-installation) for more details:
@@ -190,7 +196,7 @@ it` may occur, and will cause the backup process to fail. To combat this, 8.17
introduces a new backup strategy called `copy`. The strategy copies data files
to a temporary location before calling `tar` and `gzip`, avoiding the error.
-A side-effect is that the backup process with take up to an additional 1X disk
+A side-effect is that the backup process will take up to an additional 1X disk
space. The process does its best to clean up the temporary files at each stage
so the problem doesn't compound, but it could be a considerable change for large
installations. This is why the `copy` strategy is not the default in 8.17.
@@ -202,6 +208,9 @@ To use the `copy` strategy instead of the default streaming strategy, specify
sudo gitlab-backup create STRATEGY=copy
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Backup filename
By default a backup file is created according to the specification in [the Backup timestamp](#backup-timestamp) section above. You can however override the `[TIMESTAMP]` part of the filename by setting the `BACKUP` environment variable. For example:
@@ -210,6 +219,9 @@ By default a backup file is created according to the specification in [the Backu
sudo gitlab-backup create BACKUP=dump
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
The resulting file will then be `dump_gitlab_backup.tar`. This is useful for systems that make use of rsync and incremental backups, and will result in considerably faster transfer speeds.
### Rsyncable
@@ -222,6 +234,9 @@ Note that the `--rsyncable` option in `gzip` is not guaranteed to be available o
sudo gitlab-backup create BACKUP=dump GZIP_RSYNCABLE=yes
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Excluding specific directories from the backup
You can choose what should be exempt from the backup up by adding the environment variable `SKIP`.
@@ -247,6 +262,9 @@ For Omnibus GitLab packages:
sudo gitlab-backup create SKIP=db,uploads
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
For installations from source:
```sh
@@ -452,6 +470,9 @@ sudo gitlab-backup create DIRECTORY=daily
sudo gitlab-backup create DIRECTORY=weekly
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
@@ -569,6 +590,9 @@ There, add the following line to schedule the backup for everyday at 2 AM:
0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
You may also want to set a limited lifetime for backups to prevent regular
backups using all your disk space.
@@ -729,6 +753,14 @@ restore:
sudo gitlab-backup restore BACKUP=1493107454_2018_04_25_10.6.4-ce
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:restore`.
+
+CAUTION: **Warning:**
+`gitlab-rake gitlab:backup:restore` does not set the right file system permissions on your Registry directory.
+This is a [known issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/62759). On GitLab 12.2 or newer, you can
+use `gitlab-backup restore` to avoid this issue.
+
Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above.
Reconfigure, restart and check GitLab:
@@ -763,6 +795,14 @@ For docker installations, the restore task can be run from host:
docker exec -it <name of container> gitlab-backup restore
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:restore`.
+
+CAUTION: **Warning:**
+`gitlab-rake gitlab:backup:restore` does not set the right file system permissions on your Registry directory.
+This is a [known issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/62759). On GitLab 12.2 or newer, you can
+use `gitlab-backup restore` to avoid this issue.
+
The GitLab helm chart uses a different process, documented in
[restoring a GitLab helm chart installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/restore.md).
@@ -978,7 +1018,7 @@ sudo chown -R registry:registry /var/opt/gitlab/gitlab-rails/shared/registry/doc
NOTE: **Note:**
If you have changed the default filesystem location for the registry, you will
-want to run the chown against your custom location instead of
+want to run the `chown` against your custom location instead of
`/var/opt/gitlab/gitlab-rails/shared/registry/docker`.
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
@@ -990,6 +1030,7 @@ While running the backup, you may receive a gzip error:
```sh
sudo /opt/gitlab/bin/gitlab-backup create
+...
Dumping ...
...
gzip: stdout: Input/output error
@@ -999,5 +1040,5 @@ Backup failed
If this happens, check the following:
-1. Confirm there is sufficent diskspace for the gzip operation.
-1. If NFS is being used, check if the mount option `timeo` is set. The default is `600`, and changing this to smaller values have resulted in this error.
+1. Confirm there is sufficient disk space for the gzip operation.
+1. If NFS is being used, check if the mount option `timeout` is set. The default is `600`, and changing this to smaller values have resulted in this error.
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index d93e7241fda..326f7e4b982 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -42,17 +42,17 @@ If you are using an installation from source, replace `/var/opt/gitlab/` with `/
#### Omnibus Installation
-```
-$ sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
+```sh
+sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
```
#### Installation from source
Before running this command you need to change the directory to where your GitLab installation is located:
-```
-$ cd /home/git/gitlab
-$ sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
+```sh
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
```
#### Example output
diff --git a/doc/security/reset_root_password.md b/doc/security/reset_root_password.md
index ec360e2d338..00c9dc1407d 100644
--- a/doc/security/reset_root_password.md
+++ b/doc/security/reset_root_password.md
@@ -9,7 +9,7 @@ To reset your root password, first log into your server with root privileges.
Start a Ruby on Rails console with this command:
```bash
-gitlab-rails console production
+gitlab-rails console -e production
```
Wait until the console has loaded.
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index f6a1b6abdbf..b202dd7e9d2 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -13,7 +13,7 @@ NOTE: **Note:**
Support for MySQL was removed in GitLab 12.1. This procedure should be performed
**before** installing GitLab 12.1.
-[pgloader](https://pgloader.io/) 3.4.1+ is required.
+[pgloader](https://pgloader.io/) 3.4.1+ is required, confirm with `pgloader -V`.
You can install it directly from your distribution, for example in
Debian/Ubuntu:
@@ -125,6 +125,10 @@ new PostgreSQL one:
create no indexes, preserve index names, no foreign keys,
data only
+ SET MySQL PARAMETERS
+ net_read_timeout = '90',
+ net_write_timeout = '180'
+
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
;
@@ -222,6 +226,10 @@ new PostgreSQL one:
create no indexes, preserve index names, no foreign keys,
data only
+ SET MySQL PARAMETERS
+ net_read_timeout = '90',
+ net_write_timeout = '180'
+
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
;
diff --git a/doc/user/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md
index 9fe4b50a991..5117b5f476f 100644
--- a/doc/user/admin_area/diff_limits.md
+++ b/doc/user/admin_area/diff_limits.md
@@ -6,6 +6,8 @@ type: reference
You can set a maximum size for display of diff files (patches).
+For details about diff files, [View changes between files](../project/merge_requests/index.md#view-changes-between-file-versions).
+
## Maximum diff patch size
Diff files which exceed this value will be presented as 'too large' and won't
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index fa2df667031..166a71b6fbe 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -4,8 +4,11 @@ type: reference, howto
# Dependency Scanning **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5105)
-in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5105) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.7.
+
+Dependency Scanning helps to automatically find security vulnerabilities in your dependencies
+while you are developing and testing your applications, for example when your
+application is using an external (open source) library which is known to be vulnerable.
## Overview
@@ -18,7 +21,7 @@ in your existing `.gitlab-ci.yml` file or by implicitly using
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
GitLab checks the Dependency Scanning report, compares the found vulnerabilities
-between the source and target branches, and shows the information right on the
+between the source and target branches, and shows the information on the
merge request.
![Dependency Scanning Widget](img/dependency_scanning.png)
@@ -32,12 +35,6 @@ The results are sorted by the severity of the vulnerability:
1. Unknown
1. Everything else
-## Use cases
-
-It helps to automatically find security vulnerabilities in your dependencies
-while you are developing and testing your applications. For example when your
-application is using an external (open source) library which is known to be vulnerable.
-
## Requirements
To run a Dependency Scanning job, you need GitLab Runner with the
@@ -46,6 +43,10 @@ To run a Dependency Scanning job, you need GitLab Runner with the
executor running in privileged mode. If you're using the shared Runners on GitLab.com,
this is enabled by default.
+CAUTION: **Caution:**
+If you use your own Runners, make sure that the Docker version you have installed
+is **not** `19.03.00`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
+
## Supported languages and package managers
The following languages and dependency managers are supported.
@@ -142,7 +143,7 @@ Dependency Scanning can be [configured](#customizing-the-dependency-scanning-set
using environment variables.
| Environment variable | Description | Example usage |
-|-------------------------------- |-------------| |
+| --------------------------------------- | ----------- | ------------- |
| `DS_ANALYZER_IMAGES` | Comma separated list of custom images. The official default images are still enabled. Read more about [customizing analyzers](analyzers.md). | |
| `DS_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). | |
| `DS_ANALYZER_IMAGE_TAG` | Override the Docker tag of the official default images. Read more about [customizing analyzers](analyzers.md). | |
@@ -158,10 +159,39 @@ using environment variables.
| `PIP_INDEX_URL` | Base URL of Python Package Index (default `https://pypi.org/simple`). | |
| `PIP_EXTRA_INDEX_URL` | Array of [extra URLs](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-extra-index-url) of package indexes to use in addition to `PIP_INDEX_URL`. Comma separated. | |
+## Interacting with the vulnerabilities
+
+Once a vulnerability is found, you can interact with it. Read more on how to
+[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
+
+## Solutions for vulnerabilities (auto-remediation)
+
+Some vulnerabilities can be fixed by applying the solution that GitLab
+automatically generates.
+
+Read more about the [solutions for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation).
+
+## Security Dashboard
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups, projects and pipelines. Read more about the
+[Security Dashboard](../security_dashboard/index.md).
+
+## Vulnerabilities database update
+
+For more information about the vulnerabilities database update, check the
+[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
+
+## Dependency List
+
+An additional benefit of Dependency Scanning is the ability to view your
+project's dependencies and their known vulnerabilities. Read more about
+the [Dependency List](../dependency_list/index.md).
+
## Reports JSON format
CAUTION: **Caution:**
-The JSON report artifacts are not a public API of Dependency Scanning and their format may change in future.
+The JSON report artifacts are not a public API of Dependency Scanning and their format may change in the future.
The Dependency Scanning tool emits a JSON report file. Here is an example of the report structure with all important parts of
it highlighted:
@@ -311,28 +341,6 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
-## Security Dashboard
-
-The Security Dashboard is a good place to get an overview of all the security
-vulnerabilities in your groups, projects and pipelines. Read more about the
-[Security Dashboard](../security_dashboard/index.md).
-
-## Interacting with the vulnerabilities
-
-Once a vulnerability is found, you can interact with it. Read more on how to
-[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
-
-## Vulnerabilities database update
-
-For more information about the vulnerabilities database update, check the
-[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
-
-## Dependency List **(ULTIMATE)**
-
-An additional benefit of Dependency Scanning is the ability to view your
-project's dependencies and their known vulnerabilities. Read more about
-the [Dependency List](../dependency_list/index.md).
-
## Versioning and release process
Please check the [Release Process documentation](https://gitlab.com/gitlab-org/security-products/release/blob/master/docs/release_process.md).
@@ -343,14 +351,11 @@ You can search the [gemnasium-db](https://gitlab.com/gitlab-org/security-product
to find a vulnerability in the Gemnasium database.
You can also [submit new vulnerabilities](https://gitlab.com/gitlab-org/security-products/gemnasium-db/blob/master/CONTRIBUTING.md).
-<!-- ## Troubleshooting
+## Troubleshooting
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+### Error response from daemon: error processing tar file: docker-tar: relocation error
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+This error occurs when the Docker version used to run the SAST job is `19.03.00`.
+You are advised to update to Docker `19.03.01` or greater. Older versions are not
+affected. Read more in
+[this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/13830#note_211354992 "Current SAST container fails").
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 69529d7420b..f25d792cb90 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -71,8 +71,7 @@ entry, a detailed information will pop up with different possible options:
- [Create issue](#creating-an-issue-for-a-vulnerability): The new issue will
have the title and description pre-populated with the information from the
vulnerability report and will be created as [confidential](../project/issues/confidential_issues.md) by default.
-- [Solution](#solutions-for-vulnerabilities): For some vulnerabilities
- ([Dependency Scanning](dependency_scanning/index.md) and [Container Scanning](container_scanning/index.md))
+- [Solution](#solutions-for-vulnerabilities-auto-remediation): For some vulnerabilities,
a solution is provided for how to fix the vulnerability.
![Interacting with security reports](img/interactive_reports.png)
@@ -109,17 +108,16 @@ the vulnerability will now have an associated issue next to the name.
![Linked issue in the group security dashboard](img/issue.png)
-### Solutions for vulnerabilities
+### Solutions for vulnerabilities (auto-remediation)
-> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.7.
-
-CAUTION: **Warning:**
-Automatic Patch creation is only available for a subset of
-[Dependency Scanning](dependency_scanning/index.md). At the moment only Node.JS
-projects managed with yarn are supported.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5656) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.7.
Some vulnerabilities can be fixed by applying the solution that GitLab
-automatically generates.
+automatically generates. The following scanners are supported:
+
+- [Dependency Scanning](dependency_scanning/index.md):
+ Automatic Patch creation is only available for Node.JS projects managed with
+ `yarn`.
#### Manually applying the suggested patch
@@ -136,13 +134,12 @@ generated by GitLab. To apply the fix:
#### Creating a merge request from a vulnerability
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9224) in
-> [GitLab Ultimate](https://about.gitlab.com/pricing) 11.9.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9224) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.9.
In certain cases, GitLab will allow you to create a merge request that will
automatically remediate the vulnerability. Any vulnerability that has a
-[solution](#solutions-for-vulnerabilities) can have a merge request created to
-automatically solve the issue.
+[solution](#solutions-for-vulnerabilities-auto-remediation) can have a merge
+request created to automatically solve the issue.
If this action is available there will be a **Create merge request** button in the vulnerability modal.
Clicking on this button will create a merge request to apply the solution onto the source branch.
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index fbc130689e0..956d3ef7c8c 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -51,6 +51,10 @@ To run a SAST job, you need GitLab Runner with the
executor running in privileged mode. If you're using the shared Runners on GitLab.com,
this is enabled by default.
+CAUTION: **Caution:**
+If you use your own Runners, make sure that the Docker version you have installed
+is **not** `19.03.00`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
+
## Supported languages and frameworks
The following table shows which languages, package managers and frameworks are supported and which tools are used.
@@ -205,6 +209,7 @@ Some analyzers can be customized with environment variables.
| `GRADLE_PATH` | spotbugs | Path to the `gradle` executable. |
| `JAVA_OPTS` | spotbugs | Additional arguments for the `java` executable. |
| `JAVA_PATH` | spotbugs | Path to the `java` executable. |
+| `SAST_JAVA_VERSION` | spotbugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. |
| `MAVEN_CLI_OPTS` | spotbugs | Additional arguments for the `mvn` or `mvnw` executable. |
| `MAVEN_PATH` | spotbugs | Path to the `mvn` executable. |
| `MAVEN_REPO_PATH` | spotbugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). |
@@ -349,14 +354,11 @@ Once a vulnerability is found, you can interact with it. Read more on how to
For more information about the vulnerabilities database update, check the
[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
-<!-- ## Troubleshooting
+## Troubleshooting
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+### Error response from daemon: error processing tar file: docker-tar: relocation error
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+This error occurs when the Docker version used to run the SAST job is `19.03.00`.
+You are advised to update to Docker `19.03.01` or greater. Older versions are not
+affected. Read more in
+[this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/13830#note_211354992 "Current SAST container fails").
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
index 61f683c1335..1fe76a9e08f 100644
--- a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
+++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png
index 0b2dfecd9e7..09979ba99b3 100644
--- a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png
+++ b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
deleted file mode 100644
index baa136fd885..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
new file mode 100644
index 00000000000..51e80bdb50d
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index a98ca1fb338..ac539509e22 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -52,7 +52,7 @@ At the project level, the Security Dashboard displays the latest security report
for your project. Use it to find and fix vulnerabilities affecting the
[default branch](../../project/repository/branches/index.md#default-branch).
-![Project Security Dashboard](img/project_security_dashboard.png)
+![Project Security Dashboard](img/project_security_dashboard_v12_3.png)
## Group Security Dashboard
@@ -71,12 +71,15 @@ Once you're on the dashboard, at the top you should see a series of filters for:
- Report type
- Project
+To the right of the filters, you should see a **Hide dismissed** toggle button.
+
NOTE: **Note:**
The dashboard only shows projects with [security reports](#supported-reports) enabled in a group.
![dashboard with action buttons and metrics](img/group_security_dashboard_v12_3.png)
-Selecting one or more filters will filter the results in this page.
+Selecting one or more filters will filter the results in this page. Disabling the **Hide dismissed**
+toggle button will let you also see vulnerabilities that have been dismissed.
The main section is a list of all the vulnerabilities in the group, sorted by severity.
In that list, you can see the severity of the vulnerability, its name, its
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 40ed0db4c57..e43b1ca6826 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -19,13 +19,11 @@ This namespace:
- Is created once.
- Has a non-configurable name.
-To see a list of available applications to install:
+To see a list of available applications to install. For a:
-1. For a:
- - [Project-level cluster](../project/clusters/index.md),
- navigate to your project's **Operations > Kubernetes**.
- - [Group-level cluster](../group/clusters/index.md),
- navigate to your group's **Kubernetes** page.
+- [Project-level cluster](../project/clusters/index.md), navigate to your project's
+ **Operations > Kubernetes**.
+- [Group-level cluster](../group/clusters/index.md), navigate to your group's **Kubernetes** page.
Install Helm first as it's used to install other applications.
@@ -61,8 +59,8 @@ can lead to confusion during deployments.
### Helm
-> - Available for project-level clusters since GitLab 10.2.
-> - Available for group-level clusters since GitLab 11.6.
+> - Introduced in GitLab 10.2 for project-level clusters.
+> - Introduced in GitLab 11.6 for group-level clusters.
[Helm](https://docs.helm.sh/) is a package manager for Kubernetes and is
required to install all the other applications. It is installed in its
@@ -71,8 +69,7 @@ environment.
### Cert-Manager
-> - Available for project-level clusters since GitLab 11.6.
-> - Available for group-level clusters since GitLab 11.6.
+> Introduced in GitLab 11.6 for project- and group-level clusters.
[Cert-Manager](https://docs.cert-manager.io/en/latest/) is a native
Kubernetes certificate management controller that helps with issuing
@@ -82,15 +79,17 @@ certificates are valid and up-to-date.
NOTE: **Note:**
The
-[stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
+[jetstack/cert-manager](https://github.com/jetstack/cert-manager)
chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/cert_manager/values.yaml)
-file.
+file. Prior to GitLab 12.3,
+the [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
+chart was used.
### GitLab Runner
-> - Available for project-level clusters since GitLab 10.6.
-> - Available for group-level clusters since GitLab 11.10.
+> - Introduced in GitLab 10.6 for project-level clusters.
+> - Introduced in GitLab 11.10 for group-level clusters.
[GitLab Runner](https://docs.gitlab.com/runner/) is the open source
project that is used to run your jobs and send the results back to
@@ -110,8 +109,8 @@ file.
### Ingress
-> - Available for project-level clusters since GitLab 10.2.
-> - Available for group-level clusters since GitLab 11.6.
+> - Introduced in GitLab 10.2 for project-level clusters.
+> - Introduced in GitLab 11.6 for group-level clusters.
[Ingress](https://kubernetes.github.io/ingress-nginx/) can provide load
balancing, SSL termination, and name-based virtual hosting. It acts as a
@@ -127,7 +126,8 @@ file.
### JupyterHub
-> Available for project-level clusters since GitLab 11.0.
+> - Introduced in GitLab 11.0 for project-level clusters.
+> - Introduced in GitLab 12.3 for group-level clusters.
[JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a
multi-user service for managing notebooks across a team. [Jupyter
@@ -136,8 +136,9 @@ web-based interactive programming environment used for data analysis,
visualization, and machine learning.
Authentication will be enabled only for [project
-members](../project/members/index.md) with [Developer or
-higher](../permissions.md) access to the project.
+members](../project/members/index.md) for project-level clusters and group
+members for group-level clusters with [Developer or
+higher](../permissions.md) access to the associated project or group.
We use a [custom Jupyter
image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile)
@@ -159,7 +160,7 @@ file.
#### Jupyter Git Integration
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12 for project-level clusters.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12.0 for project-level clusters.
When installing JupyterHub onto your Kubernetes cluster, [JupyterLab's Git extension](https://github.com/jupyterlab/jupyterlab-git)
is automatically provisioned and configured using the authenticated user's:
@@ -186,7 +187,8 @@ You can clone repositories from the files tab in Jupyter:
### Knative
-> Available for project-level clusters since GitLab 11.5.
+> - Introduced in GitLab 11.5 for project-level clusters.
+> - Introduced in GitLab 12.3 for group- and instance-level clusters.
[Knative](https://cloud.google.com/knative) provides a platform to
create, deploy, and manage serverless workloads from a Kubernetes
@@ -209,8 +211,8 @@ chart is used to install this application.
### Prometheus
-> - Available for project-level clusters since GitLab 10.4.
-> - Available for group-level clusters since GitLab 11.11.
+> - Introduced in GitLab 10.4 for project-level clusters.
+> - Introduced in GitLab 11.11 for group-level clusters.
[Prometheus](https://prometheus.io/docs/introduction/overview/) is an
open-source monitoring and alerting system useful to supervise your
@@ -250,8 +252,7 @@ chart plus the values set by
## Uninstalling applications
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in
-> GitLab 11.11.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in GitLab 11.11.
The applications below can be uninstalled.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index edf2fedab3c..75a1ac8e9ff 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1292,7 +1292,7 @@ Example:
Additionally, you can choose the alignment of text within columns by adding colons (`:`)
to the sides of the "dash" lines in the second row. This will affect every cell in the column.
-> Note that the headers are always right aligned [within GitLab itself itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#tables).
+> Note that the headers are always right aligned [within GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#tables).
```markdown
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
diff --git a/doc/user/profile/account/create_accounts.md b/doc/user/profile/account/create_accounts.md
new file mode 100644
index 00000000000..c0a887d0779
--- /dev/null
+++ b/doc/user/profile/account/create_accounts.md
@@ -0,0 +1,36 @@
+---
+type: reference
+---
+
+# Creating users **(CORE ONLY)**
+
+You can create users:
+
+- Manually through the sign in page or Admin Area.
+- Automatically through user authentication integrations.
+
+## Create users on sign in page
+
+If you have [sign-up enabled](../../admin_area/settings/sign_up_restrictions.md), users can create their own accounts using the **Register** tab on the sign in page.
+
+![Register Tab](img/register_tab.png)
+
+## Create users in admin area
+
+As an admin user, you can manually create users by:
+
+1. Navigating to **Admin Area > Overview > Users** (`/admin/users` page).
+1. Selecting the **New User** button.
+
+You can also [create users through the API](../../../api/users.md) as an admin.
+
+![Admin User Button](img/admin_user_button.png)
+
+![Admin User Form](img/admin_user_form.png)
+
+## Create users through integrations
+
+Users will be:
+
+- Automatically created upon first login with the [LDAP integration](../../../administration/auth/ldap.md).
+- Created when first logging in via an [OmniAuth provider](../../../integration/omniauth.md) if the `allow_single_sign_on` setting is present.
diff --git a/doc/user/profile/account/img/admin_user_button.png b/doc/user/profile/account/img/admin_user_button.png
new file mode 100644
index 00000000000..6be9c1e266a
--- /dev/null
+++ b/doc/user/profile/account/img/admin_user_button.png
Binary files differ
diff --git a/doc/user/profile/account/img/admin_user_form.png b/doc/user/profile/account/img/admin_user_form.png
new file mode 100644
index 00000000000..ede96373c73
--- /dev/null
+++ b/doc/user/profile/account/img/admin_user_form.png
Binary files differ
diff --git a/doc/user/profile/account/img/register_tab.png b/doc/user/profile/account/img/register_tab.png
new file mode 100644
index 00000000000..73faa3edd1c
--- /dev/null
+++ b/doc/user/profile/account/img/register_tab.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index e5ccc8ee758..40b7294c3bb 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -8,6 +8,10 @@ Each GitLab account has a user profile, and settings. Your [profile](#user-profi
contains information about you, and your GitLab activity. Your [settings](#profile-settings)
allow you to customize some aspects of GitLab to suit yourself.
+## Creating users
+
+There are several ways to create users on GitLab. See the [creating users documentation](account/create_accounts.md) for more details.
+
## Signing in
There are several ways to sign into your GitLab account.
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
index 5068d2757be..d4c8bf0d309 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -2,7 +2,7 @@
> [Introduced][ee-1659] in [GitLab Premium][eep] 9.1.
-A popular [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration)
+A popular [Continuous Deployment](https://en.wikipedia.org/wiki/Continuous_deployment)
strategy, where a small portion of the fleet is updated to the new version of
your application.
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index f53dc056010..a79f368499c 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -87,7 +87,7 @@ pre-filled with the text you entered in the template(s).
We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples.
> **Tip:**
-It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions.
+It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions to perform the relevant actions.
Here is an example for a Bug report template:
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index cdb7f837158..dad53a600dc 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -6,14 +6,15 @@ your self-hosted GitLab instance.
## Overview
NOTE: **Note:**
-While these instructions will always work for users on GitLab.com, if you are an
-administrator of a self-hosted GitLab instance, you will need to enable the
-[GitHub integration][gh-import] in order for users to follow the preferred
-import method described on this page. If this is not enabled, users can alternatively import their
-GitHub repositories using a [personal access token](#using-a-github-token) from GitHub,
-but this method will not be able to associate all user activity (such as issues and pull requests)
-with matching GitLab users. As an administrator of a self-hosted GitLab instance, you can also use
-the [GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from
+These instructions work for users on GitLab.com, but if you are an
+administrator of a self-hosted GitLab instance or if you are importing from GitHub Enterprise,
+you must enable [GitHub integration][gh-import]. GitHub integration is the only method for
+importing from GitHub Enterprise. If you are using GitLab.com, you can alternatively import
+GitHub repositories using a [personal access token](#using-a-github-token),
+but this method is not recommended because it cannot associate all user activity
+(such as issues and pull requests) with matching GitLab users.
+If you are an administrator of a self-hosted GitLab instance, you can also use the
+[GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from
GitHub without the constraints of a Sidekiq worker.
The following aspects of a project are imported:
@@ -76,7 +77,7 @@ User-matching attempts occur in that order, and if a user is not identified eith
the user account that is performing the import.
NOTE: **Note:**
-If you are using a self-hosted GitLab instance, this process requires that you have configured the
+If you are using a self-hosted GitLab instance or if you are importing from GitHub Enterprise, this process requires that you have configured
[GitHub integration][gh-import].
1. From the top navigation bar, click **+** and select **New project**.
@@ -88,9 +89,13 @@ If you are using a self-hosted GitLab instance, this process requires that you h
### Using a GitHub token
NOTE: **Note:**
-For a proper author/assignee mapping for issues and pull requests, the [GitHub integration method (above)](#using-the-github-integration)
-should be used instead of the personal access token. If you are using GitLab.com or a self-hosted GitLab instance with the GitHub
-integration enabled, that should be the preferred method to import your repositories. Read more in the [How it works](#how-it-works) section.
+Using a personal access token to import projects is not recommended. If you are a GitLab.com user,
+you can use a personal access token to import your project from GitHub, but this method cannot
+associate all user activity (such as issues and pull requests) with matching GitLab users.
+If you are an administrator of a self-hosted GitLab instance or if you are importing from
+GitHub Enterprise, you cannot use a personal access token.
+The [GitHub integration method (above)](#using-the-github-integration) is recommended for all users.
+Read more in the [How it works](#how-it-works) section.
If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories:
diff --git a/doc/user/project/import/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png
index c453c7e558a..90bcff5d31b 100644
--- a/doc/user/project/import/img/import_projects_from_repo_url.png
+++ b/doc/user/project/import/img/import_projects_from_repo_url.png
Binary files differ
diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md
index 49b9826a52a..2339cfa0db8 100644
--- a/doc/user/project/merge_requests/browser_performance_testing.md
+++ b/doc/user/project/merge_requests/browser_performance_testing.md
@@ -4,8 +4,7 @@ type: reference, howto
# Browser Performance Testing **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507)
-in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
If your application offers a web interface and you are using
[GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance
@@ -25,18 +24,20 @@ for [additional metrics](https://gitlab.com/gitlab-org/gitlab-ee/issues/4370)
in a future release.
Going a step further, GitLab can show the Performance report right
-in the merge request widget area:
+in the merge request widget area (see below).
## Use cases
For instance, consider the following workflow:
-1. A member of the marketing team is attempting to track engagement by adding a new tool
-1. With browser performance metrics, they see how their changes are impacting the usability of the page for end users
-1. The metrics show that after their changes the performance score of the page has gone down
-1. When looking at the detailed report, they see that the new Javascript library was included in `<head>` which affects loading page speed
-1. They ask a front end developer to help them, who sets the library to load asynchronously
-1. The frontend developer approves the merge request and authorizes its deployment to production
+1. A member of the marketing team is attempting to track engagement by adding a new tool.
+1. With browser performance metrics, they see how their changes are impacting the usability
+ of the page for end users.
+1. The metrics show that after their changes the performance score of the page has gone down.
+1. When looking at the detailed report, they see that the new JavaScript library was
+ included in `<head>` which affects loading page speed.
+1. They ask a front end developer to help them, who sets the library to load asynchronously.
+1. The frontend developer approves the merge request and authorizes its deployment to production.
## How it works
@@ -48,15 +49,165 @@ example on [Testing Browser Performance](../../../ci/examples/browser_performanc
GitLab then checks this report, compares key performance metrics for each page
between the source and target branches, and shows the information right on the merge request.
->**Note:**
+NOTE: **Note:**
If the Performance report doesn't have anything to compare to, no information
will be displayed in the merge request area. That is the case when you add the
Performance job in your `.gitlab-ci.yml` for the very first time.
-Consecutive merge requests will have something to compare to and the Performance
+Consecutive merge requests will have something to compare to, and the Performance
report will be shown properly.
![Performance Widget](img/browser_performance_testing.png)
+## Configuring Browser Performance Testing
+
+NOTE: **Note:**
+The job definition shown below is supported in GitLab 11.5 and later versions.
+It also requires GitLab Runner 11.5 or later. For earlier versions, use the
+[previous job definitions](#previous-job-definitions).
+
+This example shows how to run the [sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/)
+on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
+using Docker-in-Docker.
+
+First, you need GitLab Runner with
+[docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+
+Once you set up the Runner, add a new job to `.gitlab-ci.yml` that generates the
+expected report:
+
+```yaml
+performance:
+ stage: performance
+ image: docker:git
+ variables:
+ URL: https://example.com
+ services:
+ - docker:stable-dind
+ script:
+ - mkdir gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - sitespeed-results/
+ reports:
+ performance: performance.json
+```
+
+The above example will create a `performance` job in your CI/CD pipeline and will run
+sitespeed.io against the webpage you defined in `URL` to gather key metrics.
+The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance)
+is downloaded in order to save the report as a [Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium)
+that you can later download and analyze. Due to implementation limitations we always
+take the latest Performance artifact available.
+
+The full HTML sitespeed.io report will also be saved as an artifact, and if you have
+[GitLab Pages](../pages/index.md) enabled, it can be viewed directly in your browser.
+
+For further customization options for sitespeed.io, including the ability to provide a
+list of URLs to test, please see the [Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/)
+documentation.
+
+TIP: **Tip:**
+Key metrics are automatically extracted and shown in the merge request widget.
+
+### Performance testing on Review Apps
+
+The above CI YML is great for testing against static environments, and it can
+be extended for dynamic environments. There are a few extra steps to take to
+set this up:
+
+1. The `performance` job should run after the dynamic environment has started.
+1. In the `review` job, persist the hostname and upload it as an artifact so
+ it's available to the `performance` job (the same can be done for static
+ environments like staging and production to unify the code path). Saving it
+ as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
+ in your job's `script`.
+1. In the `performance` job, read the previous artifact into an environment
+ variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
+ URLs.
+1. You can now run the sitespeed.io container against the desired hostname and
+ paths.
+
+Your `.gitlab-ci.yml` file would look like:
+
+```yaml
+stages:
+ - deploy
+ - performance
+
+review:
+ stage: deploy
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN
+ script:
+ - run_deploy_script
+ - echo $CI_ENVIRONMENT_URL > environment_url.txt
+ artifacts:
+ paths:
+ - environment_url.txt
+ only:
+ - branches
+ except:
+ - master
+
+performance:
+ stage: performance
+ image: docker:git
+ services:
+ - docker:stable-dind
+ dependencies:
+ - review
+ script:
+ - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+ - mkdir gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - sitespeed-results/
+ reports:
+ performance: performance.json
+```
+
+A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml).
+
+### Previous job definitions
+
+CAUTION: **Caution:**
+Before GitLab 11.5, Performance job and artifact had to be named specifically
+to automatically extract report data and show it in the merge request widget.
+While these old job definitions are still maintained they have been deprecated
+and may be removed in next major release, GitLab 12.0.
+You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+performance:
+ stage: performance
+ image: docker:git
+ variables:
+ URL: https://example.com
+ services:
+ - docker:stable-dind
+ script:
+ - mkdir gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - performance.json
+ - sitespeed-results/
+```
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png
new file mode 100644
index 00000000000..e56fbb9750f
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index d6da8cb99c7..c19e96b4482 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -164,6 +164,26 @@ you hide threads that are no longer relevant.
[Read more about resolving threads in merge requests reviews.](../../discussions/index.md)
+## View changes between file versions
+
+The **Changes** tab of a merge request shows the changes to files between branches or
+commits. This view of changes to a file is also known as a **diff**. By default, the diff view
+compares the file in the merge request branch and the file in the target branch.
+
+The diff view includes the following:
+
+- The file's name and path.
+- The number of lines added and deleted.
+- Buttons for the following options:
+ - Toggle comments for this file; useful for inline reviews.
+ - Edit the file in the merge request's branch.
+ - Show full file, in case you want to look at the changes in context with the rest of the file.
+ - View file at the current commit.
+ - Preview the changes with [Review Apps](../../../ci/review_apps/index.md).
+- The changed lines, with the specific changes highlighted.
+
+![Example screenshot of a source code diff](img/merge_request_diff_v12_2.png)
+
## Commenting on any file line in merge requests
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13950) in GitLab 11.5.
@@ -289,6 +309,7 @@ as pushing changes:
- Set the merge request to remove the source branch when it's merged.
- Set the title of the merge request to a particular title.
- Set the description of the merge request to a particular description.
+- Add or remove labels from the merge request.
### Create a new merge request using git push options
@@ -375,6 +396,35 @@ git push -o merge_request.description="The description I want"
You can also use this push option in addition to the
`merge_request.create` push option.
+### Add or remove labels using git push options
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31831) in GitLab 12.3.
+
+You can add or remove labels from merge requests using push options.
+
+For example, to add two labels to an existing merge request, use the
+`merge_request.label` push option:
+
+```sh
+git push -o merge_request.label="label1" -o merge_request.label="label2"
+```
+
+To remove two labels from an existing merge request, use
+the `merge_request.unlabel` push option:
+
+```sh
+git push -o merge_request.unlabel="label1" -o merge_request.unlabel="label2"
+```
+
+You can also use these push options in addition to the
+`merge_request.create` push option.
+
+To create a merge request and add two labels to it, use:
+
+```sh
+git push -o merge_request.create -o merge_request.label="label1" -o merge_request.label="label2"
+```
+
## Find the merge request that introduced a change
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5.
@@ -580,10 +630,7 @@ troubleshooting steps.
### Merge request cannot retrieve the pipeline status
-This can occur for one of two reasons:
-
-- Sidekiq doesn't pick up the changes fast enough
-- Because of the bug described in [#41545](https://gitlab.com/gitlab-org/gitlab-ce/issues/41545)
+This can occur if Sidekiq doesn't pick up the changes fast enough.
#### Sidekiq
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index ca9ddd91e0d..a18c2d3a21f 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -184,6 +184,10 @@ the merge request. To enable this feature:
When this feature is enabled, all merge requests will need approval
from one code owner per matched rule before it can be merged.
+NOTE: **Note:** Only the `CODEOWNERS` file on the default branch is evaluated for
+Merge Request approvals. If `CODEOWNERS` is changed on a non-default branch, those
+changes will not affect approvals until merged to the default branch.
+
## Overriding the merge request approvals default settings
> Introduced in GitLab Enterprise Edition 9.4.
@@ -196,7 +200,7 @@ If approvals are [set at the project level](#editing-approvals), the
default configuration (number of required approvals and approvers) can be
overridden for each merge request in that project.
-One possible scenario would be to to assign a group of approvers at the project
+One possible scenario would be to assign a group of approvers at the project
level and change them later when creating or editing the merge request.
First, you have to enable this option in the project's settings:
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index d844d4222b1..80fa64b162d 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -53,9 +53,9 @@ write in the `.gitlab-ci.yml` the script you want to run so
GitLab Runner will do it for you. It looks more complicated than it
is. What you need to tell the Runner:
-```
-$ gem install jekyll
-$ jekyll build
+```sh
+gem install jekyll
+jekyll build
```
### Script
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 8423b962948..895c8ac88e7 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -15,7 +15,7 @@ By default, a protected branch does four simple things:
- It prevents its creation, if not already created, from everybody except users
with Maintainer permission.
-- It prevents pushes from everybody except users with Maintainer permission.
+- It prevents pushes from everybody except users with **Allowed** permission.
- It prevents **anyone** from force pushing to the branch.
- It prevents **anyone** from deleting the branch.
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 9bf400e7dff..ecd8f74194e 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -194,7 +194,7 @@ terminal:
Once the terminal has started, the console will be displayed and we could access
the project repository files.
-**Important**. The terminal job is branch dependant. This means that the
+**Important**. The terminal job is branch dependent. This means that the
configuration file used to trigger and configure the terminal will be the one in
the selected branch of the Web IDE.
diff --git a/doc/workflow/img/repository_mirroring_push_settings.png b/doc/workflow/img/repository_mirroring_push_settings.png
index 21a6aca4526..3c0eacaa2df 100644
--- a/doc/workflow/img/repository_mirroring_push_settings.png
+++ b/doc/workflow/img/repository_mirroring_push_settings.png
Binary files differ
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 7e880b3009d..8db62997b51 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -134,13 +134,13 @@ There are two ways to manually do the same thing as automatic uploading (describ
**Option 1: rake task**
-```
-$ rake gitlab:lfs:migrate
+```sh
+rake gitlab:lfs:migrate
```
**Option 2: rails console**
-```
+```sh
$ sudo gitlab-rails console # Login to rails console
> # Upload LFS files manually
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index f5593927cc2..36db4f73885 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -62,9 +62,8 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
-NOTE: **Note:**
**Make sure** that `.gitattributes` is tracked by Git. Otherwise Git
-LFS will not be working properly for people cloning the project.
+LFS will not be working properly for people cloning the project:
```bash
git add .gitattributes
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 753518d0424..a00d7d6f775 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -37,7 +37,8 @@ The following are some possible use cases for repository mirroring:
## Pushing to a remote repository **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7.
+> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
For an existing project, you can set up push mirroring as follows:
@@ -66,7 +67,8 @@ section.
### Push only protected branches **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
You can choose to only push your protected branches from GitLab to your remote repository.
@@ -96,10 +98,11 @@ The repository will push soon. To force a push, click the appropriate button.
## Pulling from a remote repository **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2.
+> - [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab-ee/issues/10361) to
-GitLab.com users until September 22nd, 2019.
+GitLab.com users until March 22nd, 2020.
You can set up a repository to automatically have its branches, tags, and commits updated from an
upstream repository.
@@ -154,7 +157,8 @@ Repository mirrors are updated as Sidekiq becomes available to process them. If
### SSH authentication
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
SSH authentication is mutual:
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 5d08bf5e77d..2ec733182f8 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -1,109 +1,134 @@
+---
+type: reference
+---
+
# GitLab keyboard shortcuts
-You can see GitLab's keyboard shortcuts by using <kbd>shift</kbd> + <kbd>?</kbd>
+GitLab has many useful keyboard shortcuts to make it easier to access different features.
+You can see the quick reference sheet within GitLab itself with <kbd>Shift</kbd> + <kbd>?</kbd>.
-## Global Shortcuts
+The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must
+be in specific pages for the other shortcuts to be available, as explained in each
+section below.
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>n</kbd> | Main navigation |
-| <kbd>s</kbd> | Focus search |
-| <kbd>f</kbd> | Focus filter |
-| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
-| <kbd>?</kbd> | Show/hide this dialog |
-| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
-| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
+## Global Shortcuts
-## Project Files Browsing
+These shortcuts are available in most areas of GitLab
+
+| Keyboard Shortcut | Description |
+| ------------------------------- | ----------- |
+| <kbd>?</kbd> | Show/hide shortcut reference sheet. |
+| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to your Projects page. |
+| <kbd>Shift</kbd> + <kbd>g</kbd> | Go to your Groups page. |
+| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to your Activity page. |
+| <kbd>Shift</kbd> + <kbd>l</kbd> | Go to your Milestones page. |
+| <kbd>Shift</kbd> + <kbd>s</kbd> | Go to your Snippets page. |
+| <kbd>s</kbd> | Put cursor in the issues/merge requests search. |
+| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. |
+| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.|
+| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
+| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. |
+
+Additionally, the following shortcuts are available when editing text in text fields,
+for example comments, replies, or issue and merge request descriptions:
+
+| Keyboard Shortcut | Description |
+| ---------------------------------------------------------------------- | ----------- |
+| <kbd>↑</kbd> | Edit your last comment. You must be in a blank text field below a thread, and you must already have at least one comment in the thread. |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle Markdown preview, when editing text in a text field that has **Write** and **Preview** tabs at the top. |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>↑</kbd> | Move selection up |
-| <kbd>↓</kbd> | Move selection down |
-| <kbd>enter</kbd> | Open selection |
+## Project
-## Finding Project File
+These shortcuts are available from any page within a project. You must type them
+relatively quickly to work, and they will take you to another page in the project.
+
+| Keyboard Shortcut | Description |
+| --------------------------- | ----------- |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project home page (**Project > Details**). |
+| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project activity feed (**Project > Activity**). |
+| <kbd>g</kbd> + <kbd>r</kbd> | Go to the project releases list (**Project > Releases**). |
+| <kbd>g</kbd> + <kbd>f</kbd> | Go to the [project files](#project-files) list (**Repository > Files**). |
+| <kbd>t</kbd> | Go to the project file search page. (**Repository > Files**, click **Find Files**). |
+| <kbd>g</kbd> + <kbd>c</kbd> | Go to the project commits list (**Repository > Commits**). |
+| <kbd>g</kbd> + <kbd>n</kbd> | Go to the [repository graph](#repository-graph) page (**Repository > Graph**). |
+| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts (**Repository > Charts**). |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to the project issues list (**Issues > List**). |
+| <kbd>i</kbd> | Go to the New Issue page (**Issues**, click **New Issue** ). |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to the project issue boards list (**Issues > Boards**). |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to the project merge requests list (**Merge Requests**). |
+| <kbd>g</kbd> + <kbd>j</kbd> | Go to the CI/CD jobs list (**CI/CD > Jobs**). |
+| <kbd>g</kbd> + <kbd>l</kbd> | Go to the project metrics (**Operations > Metrics**). |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project environments (**Operations > Environments**). |
+| <kbd>g</kbd> + <kbd>k</kbd> | Go to the project Kubernetes cluster integration page (**Operations > Kubernetes**). Note that you must have at least [`maintainer` permissions](../user/permissions.md) to access this page. |
+| <kbd>g</kbd> + <kbd>s</kbd> | Go to the project snippets list (**Snippets**). |
+| <kbd>g</kbd> + <kbd>w</kbd> | Go to the project wiki (**Wiki**), if enabled. |
+
+### Issues and Merge Requests
+
+These shortcuts are available when viewing issues and merge requests.
+
+| Keyboard Shortcut | Description |
+| ---------------------------- | ----------- |
+| <kbd>e</kbd> | Edit description. |
+| <kbd>a</kbd> | Change assignee. |
+| <kbd>m</kbd> | Change milestone. |
+| <kbd>l</kbd> | Change label. |
+| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
+| <kbd>n</kbd> | Move to next unresolved discussion (Merge requests only). |
+| <kbd>p</kbd> | Move to previous unresolved discussion (Merge requests only). |
+| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (Merge requests only). |
+| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (Merge requests only). |
+
+### Project Files
+
+These shortcuts are available when browsing the files in a project (navigate to
+**Repository** > **Files**):
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>↑</kbd> | Move selection up |
-| <kbd>↓</kbd> | Move selection down |
-| <kbd>enter</kbd> | Open selection |
-| <kbd>esc</kbd> | Go back |
+| <kbd>↑</kbd> | Move selection up. |
+| <kbd>↓</kbd> | Move selection down. |
+| <kbd>enter</kbd> | Open selection. |
+| <kbd>esc</kbd> | Go back to file list screen (only while searching for files, **Repository > Files** then click on **Find File**). |
+| <kbd>y</kbd> | Go to file permalink (only while viewing a file). |
-## Global Dashboard
+### Web IDE
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to the activity feed |
-| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to projects |
-| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to issues |
-| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to merge requests |
-| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to todos |
+These shortcuts are available when editing a file with the [Web IDE](../user/project/web_ide/index.md):
-## Project
+| Keyboard Shortcut | Description |
+| ------------------------------------------------------- | ----------- |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message). |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
-| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
-| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
-| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
-| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
-| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
-| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
-| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
-| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
-| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
-| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
-| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
-| <kbd>t</kbd> | Go to finding file |
-| <kbd>i</kbd> | New issue |
-
-## Network Graph
+### Repository Graph
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left |
-| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right |
-| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up |
-| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down |
-| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top |
-| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
+These shortcuts are available when viewing the project [repository graph](../user/project/repository/index.md#repository-graph)
+page (navigate to **Repository > Graph**):
-## Issues and Merge Requests
+| Keyboard Shortcut | Description |
+| ------------------------------------------------------------------ | ----------- |
+| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left. |
+| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right. |
+| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up. |
+| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down. |
+| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top. |
+| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom. |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>a</kbd> | Change assignee |
-| <kbd>m</kbd> | Change milestone |
-| <kbd>r</kbd> | Reply (quoting selected text) |
-| <kbd>e</kbd> | Edit issue/merge request |
-| <kbd>l</kbd> | Change label |
-| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file |
-| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file |
-| <kbd>n</kbd> | Move to next unresolved discussion |
-| <kbd>p</kbd> | Move to previous unresolved discussion |
+### Wiki pages
-## Epics **(ULTIMATE)**
+This shortcut is available when viewing a [wiki page](../user/project/wiki/index.md):
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>r</kbd> | Reply (quoting selected text) |
-| <kbd>e</kbd> | Edit description |
-| <kbd>l</kbd> | Change label |
-
-## Wiki pages
+| <kbd>e</kbd> | Edit wiki page. |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>e</kbd> | Edit wiki page|
+## Epics **(ULTIMATE)**
-## Web IDE
+These shortcuts are available when viewing [Epics](../user/group/epics/index.md):
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>p</kbd> | Go to file |
-| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message) |
+| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
+| <kbd>e</kbd> | Edit description. |
+| <kbd>l</kbd> | Change label. |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index aa6a67d817a..3bf16dc41d2 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -119,6 +119,7 @@ module API
mount ::API::GroupVariables
mount ::API::ImportGithub
mount ::API::Internal::Base
+ mount ::API::Internal::Pages
mount ::API::Issues
mount ::API::JobArtifacts
mount ::API::Jobs
@@ -161,6 +162,7 @@ module API
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
+ mount ::API::Statistics
mount ::API::Submodules
mount ::API::Subscriptions
mount ::API::Suggestions
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f7cd6d35854..312c8d5b548 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1169,6 +1169,55 @@ module API
expose :message, :starts_at, :ends_at, :color, :font
end
+ class ApplicationStatistics < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+ include CountHelper
+
+ expose :forks do |counts|
+ approximate_fork_count_with_delimiters(counts)
+ end
+
+ expose :issues do |counts|
+ approximate_count_with_delimiters(counts, ::Issue)
+ end
+
+ expose :merge_requests do |counts|
+ approximate_count_with_delimiters(counts, ::MergeRequest)
+ end
+
+ expose :notes do |counts|
+ approximate_count_with_delimiters(counts, ::Note)
+ end
+
+ expose :snippets do |counts|
+ approximate_count_with_delimiters(counts, ::Snippet)
+ end
+
+ expose :ssh_keys do |counts|
+ approximate_count_with_delimiters(counts, ::Key)
+ end
+
+ expose :milestones do |counts|
+ approximate_count_with_delimiters(counts, ::Milestone)
+ end
+
+ expose :users do |counts|
+ approximate_count_with_delimiters(counts, ::User)
+ end
+
+ expose :projects do |counts|
+ approximate_count_with_delimiters(counts, ::Project)
+ end
+
+ expose :groups do |counts|
+ approximate_count_with_delimiters(counts, ::Group)
+ end
+
+ expose :active_users do |_|
+ number_with_delimiter(::User.active.count)
+ end
+ end
+
class ApplicationSetting < Grape::Entity
def self.exposed_attributes
attributes = ::ApplicationSettingsHelper.visible_attributes
@@ -1263,6 +1312,10 @@ module API
options[:project].releases.find_by(tag: repo_tag.name)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ expose :protected do |repo_tag, options|
+ ::ProtectedTag.protected?(options[:project], repo_tag.name)
+ end
end
class Runner < Grape::Entity
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 5755f4b8d74..e61b25721fb 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -5,10 +5,10 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- SUDO_HEADER = "HTTP_SUDO".freeze
- GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret".freeze
+ SUDO_HEADER = "HTTP_SUDO"
+ GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret"
SUDO_PARAM = :sudo
- API_USER_ENV = 'gitlab.api.user'.freeze
+ API_USER_ENV = 'gitlab.api.user'
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 6b438235258..9a80671f996 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -17,6 +17,18 @@ module API
@project # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+ def access_checker_for(actor, protocol)
+ access_checker_klass.new(actor.key_or_user, project, protocol,
+ authentication_abilities: ssh_authentication_abilities,
+ namespace_path: namespace_path,
+ project_path: project_path,
+ redirected_path: redirected_path)
+ end
+
+ def access_checker_klass
+ repo_type.access_checker_class
+ end
+
def ssh_authentication_abilities
[
:read_project,
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 5b87eccf860..41ee34a8fdf 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -3,7 +3,7 @@
module API
module Helpers
module Runner
- JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
+ JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
JOB_TOKEN_PARAM = :token
def runner_registration_token_valid?
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 622032b8355..087985d34b0 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -35,36 +35,14 @@ module API
# project - project full_path (not path on disk)
# action - git action (git-upload-pack or git-receive-pack)
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
- # rubocop: disable CodeReuse/ActiveRecord
post "/allowed" do
# Stores some Git-specific env thread-safely
env = parse_env
Gitlab::Git::HookEnv.set(gl_repository, env) if project
- actor =
- if params[:key_id]
- Key.find_by(id: params[:key_id])
- elsif params[:user_id]
- User.find_by(id: params[:user_id])
- elsif params[:username]
- UserFinder.new(params[:username]).find_by_username
- end
-
- protocol = params[:protocol]
-
- actor.update_last_used_at if actor.is_a?(Key)
- user =
- if actor.is_a?(Key)
- actor.user
- else
- actor
- end
-
- access_checker_klass = repo_type.access_checker_class
- access_checker = access_checker_klass.new(actor, project,
- protocol, authentication_abilities: ssh_authentication_abilities,
- namespace_path: namespace_path, project_path: project_path,
- redirected_path: redirected_path)
+ actor = Support::GitAccessActor.from_params(params)
+ actor.update_last_used_at!
+ access_checker = access_checker_for(actor, params[:protocol])
check_result = begin
result = access_checker.check(params[:action], params[:changes])
@@ -78,15 +56,15 @@ module API
break response_with_status(code: 404, success: false, message: e.message)
end
- log_user_activity(actor)
+ log_user_activity(actor.user)
case check_result
when ::Gitlab::GitAccessResult::Success
payload = {
gl_repository: gl_repository,
gl_project_path: gl_project_path,
- gl_id: Gitlab::GlId.gl_id(user),
- gl_username: user&.username,
+ gl_id: Gitlab::GlId.gl_id(actor.user),
+ gl_username: actor.username,
git_config_options: [],
gitaly: gitaly_payload(params[:action]),
gl_console_messages: check_result.console_messages
@@ -105,7 +83,6 @@ module API
response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR)
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
post "/lfs_authenticate" do
diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb
new file mode 100644
index 00000000000..6ea048bde03
--- /dev/null
+++ b/lib/api/internal/pages.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module API
+ # Pages Internal API
+ module Internal
+ class Pages < Grape::API
+ before do
+ not_found! unless Feature.enabled?(:pages_internal_api)
+ authenticate_gitlab_pages_request!
+ end
+
+ helpers do
+ def authenticate_gitlab_pages_request!
+ unauthorized! unless Gitlab::Pages.verify_api_request(headers)
+ end
+ end
+
+ namespace 'internal' do
+ namespace 'pages' do
+ get "/" do
+ status :ok
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index dd27ebab83d..acf03051a5b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -50,10 +50,8 @@ module API
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
- given domain_blacklist_enabled: ->(val) { val } do
- requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
- end
- optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
@@ -74,7 +72,7 @@ module API
requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
end
optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
- optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project manifest],
+ optional :import_sources, type: Array[String], values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb
new file mode 100644
index 00000000000..d2dce34dfa5
--- /dev/null
+++ b/lib/api/statistics.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ class Statistics < Grape::API
+ before { authenticated_as_admin! }
+
+ COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
+ MergeRequest, Note, Snippet, Key, Milestone].freeze
+
+ desc 'Get the current application statistics' do
+ success Entities::ApplicationStatistics
+ end
+ get "application/statistics" do
+ counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
+ present counts, with: Entities::ApplicationStatistics
+ end
+ end
+end
diff --git a/lib/api/support/git_access_actor.rb b/lib/api/support/git_access_actor.rb
new file mode 100644
index 00000000000..2e0962c6295
--- /dev/null
+++ b/lib/api/support/git_access_actor.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module API
+ module Support
+ class GitAccessActor
+ attr_reader :user
+
+ def initialize(user: nil, key: nil)
+ @user = user
+ @key = key
+
+ @user = key.user if !user && key
+ end
+
+ def self.from_params(params)
+ if params[:key_id]
+ new(key: Key.find_by_id(params[:key_id]))
+ elsif params[:user_id]
+ new(user: UserFinder.new(params[:user_id]).find_by_id)
+ elsif params[:username]
+ new(user: UserFinder.new(params[:username]).find_by_username)
+ end
+ end
+
+ def key_or_user
+ key || user
+ end
+
+ def username
+ user&.username
+ end
+
+ def update_last_used_at!
+ key&.update_last_used_at
+ end
+
+ private
+
+ attr_reader :key
+ end
+ end
+end
diff --git a/lib/api/validations/types/workhorse_file.rb b/lib/api/validations/types/workhorse_file.rb
new file mode 100644
index 00000000000..18d111f6556
--- /dev/null
+++ b/lib/api/validations/types/workhorse_file.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Validations
+ module Types
+ class WorkhorseFile < Virtus::Attribute
+ def coerce(input)
+ # Processing of multipart file objects
+ # is already taken care of by Gitlab::Middleware::Multipart.
+ # Nothing to do here.
+ input
+ end
+
+ def value_coerced?(value)
+ value.is_a?(::UploadedFile)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 5724adb2c40..c5a5488950d 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -4,11 +4,21 @@ module API
class Wikis < Grape::API
helpers do
def commit_params(attrs)
- {
- file_name: attrs[:file][:filename],
- file_content: attrs[:file][:tempfile].read,
- branch_name: attrs[:branch]
- }
+ # In order to avoid service disruption this can work with an old workhorse without the acceleration
+ # the first branch of this if must be removed when we drop support for non accelerated uploads
+ if attrs[:file].is_a?(Hash)
+ {
+ file_name: attrs[:file][:filename],
+ file_content: attrs[:file][:tempfile].read,
+ branch_name: attrs[:branch]
+ }
+ else
+ {
+ file_name: attrs[:file].original_filename,
+ file_content: attrs[:file].read,
+ branch_name: attrs[:branch]
+ }
+ end
end
params :common_wiki_page_params do
@@ -106,7 +116,7 @@ module API
success Entities::WikiAttachment
end
params do
- requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
+ requires :file, types: [::API::Validations::Types::SafeFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
post ":id/wikis/attachments" do
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index aeaf61cda39..c0390959269 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -4,7 +4,7 @@ module Backup
class Manager
ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
FOLDERS_TO_BACKUP = %w[repositories db].freeze
- FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'
attr_reader :progress
diff --git a/lib/banzai/filter/asset_proxy_filter.rb b/lib/banzai/filter/asset_proxy_filter.rb
index 0a9a52a73a1..8acd3917d81 100644
--- a/lib/banzai/filter/asset_proxy_filter.rb
+++ b/lib/banzai/filter/asset_proxy_filter.rb
@@ -44,7 +44,7 @@ module Banzai
Gitlab.config.asset_proxy['enabled'] = application_settings.asset_proxy_enabled
Gitlab.config.asset_proxy['url'] = application_settings.asset_proxy_url
Gitlab.config.asset_proxy['secret_key'] = application_settings.asset_proxy_secret_key
- Gitlab.config.asset_proxy['whitelist'] = application_settings.asset_proxy_whitelist || [Gitlab.config.gitlab.host]
+ Gitlab.config.asset_proxy['whitelist'] = determine_whitelist(application_settings)
Gitlab.config.asset_proxy['domain_regexp'] = compile_whitelist(Gitlab.config.asset_proxy.whitelist)
else
Gitlab.config.asset_proxy['enabled'] = ::ApplicationSetting.defaults[:asset_proxy_enabled]
@@ -57,6 +57,10 @@ module Banzai
escaped = domain_list.map { |domain| Regexp.escape(domain).gsub('\*', '.*?') }
Regexp.new("^(#{escaped.join('|')})$", Regexp::IGNORECASE)
end
+
+ def self.determine_whitelist(application_settings)
+ application_settings.asset_proxy_whitelist.presence || [Gitlab.config.gitlab.host]
+ end
end
end
end
diff --git a/lib/banzai/filter/color_filter.rb b/lib/banzai/filter/color_filter.rb
index 6d9bdb9cbd3..0aca7441638 100644
--- a/lib/banzai/filter/color_filter.rb
+++ b/lib/banzai/filter/color_filter.rb
@@ -5,7 +5,7 @@ module Banzai
# HTML filter that renders `color` followed by a color "chip".
#
class ColorFilter < HTML::Pipeline::Filter
- COLOR_CHIP_CLASS = 'gfm-color_chip'.freeze
+ COLOR_CHIP_CLASS = 'gfm-color_chip'
def call
doc.css('code').each do |node|
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index fb721fe12b1..67019454e44 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -5,8 +5,8 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
SCHEMES = ['http', 'https', nil].freeze
- RTLO = "\u202E".freeze
- ENCODED_RTLO = '%E2%80%AE'.freeze
+ RTLO = "\u202E"
+ ENCODED_RTLO = '%E2%80%AE'
def call
links.each do |node|
diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb
index de133774dfa..5474242e03c 100644
--- a/lib/banzai/filter/footnote_filter.rb
+++ b/lib/banzai/filter/footnote_filter.rb
@@ -17,8 +17,8 @@ module Banzai
#
class FootnoteFilter < HTML::Pipeline::Filter
INTEGER_PATTERN = /\A\d+\z/.freeze
- FOOTNOTE_ID_PREFIX = 'fn'.freeze
- FOOTNOTE_LINK_ID_PREFIX = 'fnref'.freeze
+ FOOTNOTE_ID_PREFIX = 'fn'
+ FOOTNOTE_LINK_ID_PREFIX = 'fnref'
FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_START_NUMBER = 1
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index 8dd5a8979c8..c915f0ee35b 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -11,14 +11,14 @@ module Banzai
#
class MathFilter < HTML::Pipeline::Filter
# Attribute indicating inline or display math.
- STYLE_ATTRIBUTE = 'data-math-style'.freeze
+ STYLE_ATTRIBUTE = 'data-math-style'
# Class used for tagging elements that should be rendered
- TAG_CLASS = 'js-render-math'.freeze
+ TAG_CLASS = 'js-render-math'
- INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
+ INLINE_CLASSES = "code math #{TAG_CLASS}"
- DOLLAR_SIGN = '$'.freeze
+ DOLLAR_SIGN = '$'
def call
doc.css('code').each do |code|
diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb
index 3d40abfc540..ae093580001 100644
--- a/lib/banzai/filter/suggestion_filter.rb
+++ b/lib/banzai/filter/suggestion_filter.rb
@@ -5,7 +5,7 @@ module Banzai
module Filter
class SuggestionFilter < HTML::Pipeline::Filter
# Class used for tagging elements that should be rendered
- TAG_CLASS = 'js-render-suggestion'.freeze
+ TAG_CLASS = 'js-render-suggestion'
def call
return doc unless suggestions_filter_enabled?
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 9b66759a5fb..c1a5f33c225 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -10,8 +10,8 @@ module Banzai
class SyntaxHighlightFilter < HTML::Pipeline::Filter
include OutputSafety
- PARAMS_DELIMITER = ':'.freeze
- LANG_PARAMS_ATTR = 'data-lang-params'.freeze
+ PARAMS_DELIMITER = ':'
+ LANG_PARAMS_ATTR = 'data-lang-params'
def call
doc.search('pre:not([data-math-style]) > code').each do |node|
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 341dbb74fe0..31bb4f2e01c 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -11,8 +11,8 @@ module Banzai
class IssuableExtractor
attr_reader :context
- ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'.freeze
- MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'.freeze
+ ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'
+ MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'
# context - An instance of Banzai::RenderContext.
def initialize(context)
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 0041634f9e3..9937236fc44 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -2,8 +2,8 @@
module Bitbucket
class Connection
- DEFAULT_API_VERSION = '2.0'.freeze
- DEFAULT_BASE_URI = 'https://api.bitbucket.org/'.freeze
+ DEFAULT_API_VERSION = '2.0'
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
DEFAULT_QUERY = {}.freeze
attr_reader :expires_at, :expires_in, :refresh_token, :token
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index e8b938e46b1..582541c0679 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -29,13 +29,13 @@ module Gitlab
if result.status.success?
result.stdout.chomp.freeze
else
- "Unknown".freeze
+ "Unknown"
end
end
end
end
- COM_URL = 'https://gitlab.com'.freeze
+ COM_URL = 'https://gitlab.com'
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}.freeze
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze
VERSION = File.read(root.join("VERSION")).strip.freeze
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
index 31dd61ae6cf..97e78ecf094 100644
--- a/lib/gitlab/auth/unique_ips_limiter.rb
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -3,7 +3,7 @@
module Gitlab
module Auth
class UniqueIpsLimiter
- USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'.freeze
+ USER_UNIQUE_IPS_PREFIX = 'user_unique_ips'
class << self
def limit_user_id!(user_id)
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index bba7e2cbb3c..97755117edc 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -20,7 +20,7 @@ module Gitlab
module UserAuthFinders
include Gitlab::Utils::StrongMemoize
- PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze
+ PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'
PRIVATE_TOKEN_PARAM = :private_token
# Check the Rails session for valid authentication details
diff --git a/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check.rb b/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check.rb
index e948cedaad5..de0c357ab1c 100644
--- a/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check.rb
+++ b/lib/gitlab/background_migration/merge_request_assignees_migration_progress_check.rb
@@ -7,7 +7,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
RESCHEDULE_DELAY = 3.hours
- WORKER = 'PopulateMergeRequestAssigneesTable'.freeze
+ WORKER = 'PopulateMergeRequestAssigneesTable'
DeadJobsError = Class.new(StandardError)
def perform
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index 2ac51dd7b55..3d943205783 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -10,15 +10,15 @@ module Gitlab
include ::Gitlab::Utils::StrongMemoize
FIND_BATCH_SIZE = 500
- RELATIVE_UPLOAD_DIR = "uploads".freeze
+ RELATIVE_UPLOAD_DIR = "uploads"
ABSOLUTE_UPLOAD_DIR = File.join(
Gitlab.config.uploads.storage_path,
RELATIVE_UPLOAD_DIR
)
- FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
+ FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'
START_WITH_ROOT_REGEX = %r{\A#{Gitlab.config.uploads.storage_path}/}.freeze
- EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*".freeze
- EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*".freeze
+ EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*"
+ EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*"
# This class is used to iterate over batches of
# `untracked_files_for_uploads` rows.
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index ff2694abd5e..93c6fdcf69c 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -7,7 +7,7 @@ module Gitlab
attr_reader :project, :project_key, :repository_slug, :client, :errors, :users
attr_accessor :logger
- REMOTE_NAME = 'bitbucket_server'.freeze
+ REMOTE_NAME = 'bitbucket_server'
BATCH_SIZE = 100
TempBranch = Struct.new(:name, :sha)
diff --git a/lib/gitlab/checks/lfs_check.rb b/lib/gitlab/checks/lfs_check.rb
index 67a65d61441..7b013567a03 100644
--- a/lib/gitlab/checks/lfs_check.rb
+++ b/lib/gitlab/checks/lfs_check.rb
@@ -3,8 +3,8 @@
module Gitlab
module Checks
class LfsCheck < BaseChecker
- LOG_MESSAGE = "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...".freeze
- ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'.freeze
+ LOG_MESSAGE = 'Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...'
+ ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
def validate!
return unless Feature.enabled?(:lfs_check, default_enabled: true)
diff --git a/lib/gitlab/checks/project_created.rb b/lib/gitlab/checks/project_created.rb
index 0058a402a62..362562068e9 100644
--- a/lib/gitlab/checks/project_created.rb
+++ b/lib/gitlab/checks/project_created.rb
@@ -3,7 +3,7 @@
module Gitlab
module Checks
class ProjectCreated < PostPushMessage
- PROJECT_CREATED = "project_created".freeze
+ PROJECT_CREATED = "project_created"
def message
<<~MESSAGE
diff --git a/lib/gitlab/checks/project_moved.rb b/lib/gitlab/checks/project_moved.rb
index cb3b7acaaad..6f04fddc6c4 100644
--- a/lib/gitlab/checks/project_moved.rb
+++ b/lib/gitlab/checks/project_moved.rb
@@ -3,7 +3,7 @@
module Gitlab
module Checks
class ProjectMoved < PostPushMessage
- REDIRECT_NAMESPACE = "redirect_namespace".freeze
+ REDIRECT_NAMESPACE = "redirect_namespace"
def initialize(project, user, protocol, redirected_path)
@redirected_path = redirected_path
diff --git a/lib/gitlab/ci/build/port.rb b/lib/gitlab/ci/build/port.rb
index 6c4656ffea2..017b31b4f77 100644
--- a/lib/gitlab/ci/build/port.rb
+++ b/lib/gitlab/ci/build/port.rb
@@ -4,8 +4,8 @@ module Gitlab
module Ci
module Build
class Port
- DEFAULT_PORT_NAME = 'default_port'.freeze
- DEFAULT_PORT_PROTOCOL = 'http'.freeze
+ DEFAULT_PORT_NAME = 'default_port'
+ DEFAULT_PORT_PROTOCOL = 'http'
attr_reader :number, :protocol, :name
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index 89623a809c9..43399c74457 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -6,7 +6,14 @@ module Gitlab
class Rules
include ::Gitlab::Utils::StrongMemoize
- Result = Struct.new(:when, :start_in)
+ Result = Struct.new(:when, :start_in) do
+ def build_attributes
+ {
+ when: self.when,
+ options: { start_in: start_in }.compact
+ }.compact
+ end
+ end
def initialize(rule_hashes, default_when = 'on_success')
@rule_list = Rule.fabricate_list(rule_hashes)
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
index ff0baf3348c..bf787fe95a6 100644
--- a/lib/gitlab/ci/build/rules/rule/clause.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -13,9 +13,7 @@ module Gitlab
UnknownClauseError = Class.new(StandardError)
def self.fabricate(type, value)
- type = type.to_s.camelize
-
- self.const_get(type).new(value) if self.const_defined?(type)
+ "#{self}::#{type.to_s.camelize}".safe_constantize&.new(value)
end
def initialize(spec)
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
index 7fcabc035ac..48111ae5717 100644
--- a/lib/gitlab/ci/build/step.rb
+++ b/lib/gitlab/ci/build/step.rb
@@ -4,9 +4,9 @@ module Gitlab
module Ci
module Build
class Step
- WHEN_ON_FAILURE = 'on_failure'.freeze
- WHEN_ON_SUCCESS = 'on_success'.freeze
- WHEN_ALWAYS = 'always'.freeze
+ WHEN_ON_FAILURE = 'on_failure'
+ WHEN_ON_SUCCESS = 'on_success'
+ WHEN_ALWAYS = 'always'
attr_reader :name
attr_accessor :script, :timeout, :when, :allow_failure
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 7b94af24c09..ef07c319ce4 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[key untracked paths policy].freeze
- DEFAULT_POLICY = 'pull-push'.freeze
+ DEFAULT_POLICY = 'pull-push'
validations do
validates :config, allowed_keys: ALLOWED_KEYS
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 6e11c582750..f750886a8c5 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -15,7 +15,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except rules type image services
allow_failure type stage when start_in artifacts cache
dependencies needs before_script after_script variables
- environment coverage retry parallel extends].freeze
+ environment coverage retry parallel extends interruptible].freeze
REQUIRED_BY_NEEDS = %i[stage].freeze
@@ -37,6 +37,7 @@ module Gitlab
with_options allow_nil: true do
validates :tags, array_of_strings: true
validates :allow_failure, boolean: true
+ validates :interruptible, boolean: true
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
@@ -121,11 +122,12 @@ module Gitlab
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :environment, :coverage, :retry,
- :parallel, :needs
+ :artifacts, :environment, :coverage, :retry, :rules,
+ :parallel, :needs, :interruptible
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :needs, :retry, :parallel, :extends, :start_in, :rules
+ :needs, :retry, :parallel, :extends, :start_in, :rules,
+ :interruptible
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -143,6 +145,13 @@ module Gitlab
end
@entries.delete(:type)
+
+ # This is something of a hack, see issue for details:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/67150
+ if !only_defined? && has_rules?
+ @entries.delete(:only)
+ @entries.delete(:except)
+ end
end
inherit!(deps)
@@ -201,12 +210,14 @@ module Gitlab
cache: cache_value,
only: only_value,
except: except_value,
+ rules: has_rules? ? rules_value : nil,
variables: variables_defined? ? variables_value : {},
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
retry: retry_defined? ? retry_value : nil,
parallel: parallel_defined? ? parallel_value.to_i : nil,
+ interruptible: interruptible_defined? ? interruptible_value : nil,
artifacts: artifacts_value,
after_script: after_script_value,
ignore: ignored?,
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
index 65cad0880f5..2fbc3d9e367 100644
--- a/lib/gitlab/ci/config/entry/rules.rb
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -26,6 +26,10 @@ module Gitlab
end
end
end
+
+ def value
+ @config
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/external/file/template.rb b/lib/gitlab/ci/config/external/file/template.rb
index 54f4cf74c4d..db56f6a9b00 100644
--- a/lib/gitlab/ci/config/external/file/template.rb
+++ b/lib/gitlab/ci/config/external/file/template.rb
@@ -8,7 +8,7 @@ module Gitlab
class Template < Base
attr_reader :location, :project
- SUFFIX = '.gitlab-ci.yml'.freeze
+ SUFFIX = '.gitlab-ci.yml'
def initialize(params, context)
@location = params[:template]
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index 94f4a4e36c9..1d7e7ea0f9a 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -3,8 +3,8 @@
module Gitlab
module Ci
class CronParser
- VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'.freeze
- VALID_SYNTAX_SAMPLE_CRON = '* * * * *'.freeze
+ VALID_SYNTAX_SAMPLE_TIME_ZONE = 'UTC'
+ VALID_SYNTAX_SAMPLE_CRON = '* * * * *'
def initialize(cron, cron_timezone = 'UTC')
@cron = cron
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 164a4634d84..899df81ea5c 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -19,6 +19,7 @@ module Gitlab
user: @command.current_user,
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
+ external_pull_request: @command.external_pull_request,
variables_attributes: Array(@command.variables_attributes)
)
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index afad391e8e0..58f89a6be5e 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -7,7 +7,7 @@ module Gitlab
Command = Struct.new(
:source, :project, :current_user,
:origin_ref, :checkout_sha, :after_sha, :before_sha, :source_sha, :target_sha,
- :trigger_request, :schedule, :merge_request,
+ :trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 1066331062b..1f6b3853069 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -145,7 +145,7 @@ module Gitlab
def rules_attributes
strong_memoize(:rules_attributes) do
- @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {}
+ @using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
end
end
end
diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb
index 292e273a03a..fdeaad698b9 100644
--- a/lib/gitlab/ci/reports/test_case.rb
+++ b/lib/gitlab/ci/reports/test_case.rb
@@ -4,10 +4,10 @@ module Gitlab
module Ci
module Reports
class TestCase
- STATUS_SUCCESS = 'success'.freeze
- STATUS_FAILED = 'failed'.freeze
- STATUS_SKIPPED = 'skipped'.freeze
- STATUS_ERROR = 'error'.freeze
+ STATUS_SUCCESS = 'success'
+ STATUS_FAILED = 'failed'
+ STATUS_SKIPPED = 'skipped'
+ STATUS_ERROR = 'error'
STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze
attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 5c1c0c142e5..f704266b73d 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -54,7 +54,7 @@ variables:
ROLLOUT_RESOURCE_TYPE: deployment
- DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501
+ DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501
stages:
- build
@@ -73,16 +73,16 @@ stages:
- cleanup
include:
- - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
- - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
- - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
- - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
- - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
- - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
- - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
- - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
- - template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
- - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+ - template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+ - template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+ - template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+ - template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+ - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+ - template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+ - template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+ - template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+ - template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
+ - template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
# Override DAST job to exclude master branch
dast:
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 2e1eab270ff..986605efdc3 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -41,6 +41,8 @@ module Gitlab
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
needs_attributes: job[:needs]&.map { |need| { name: need } },
+ interruptible: job[:interruptible],
+ rules: job[:rules],
options: {
image: job[:image],
services: job[:services],
diff --git a/lib/gitlab/cleanup/project_upload_file_finder.rb b/lib/gitlab/cleanup/project_upload_file_finder.rb
index 5aace564c2d..3d35d474f5d 100644
--- a/lib/gitlab/cleanup/project_upload_file_finder.rb
+++ b/lib/gitlab/cleanup/project_upload_file_finder.rb
@@ -5,9 +5,9 @@ module Gitlab
class ProjectUploadFileFinder
FIND_BATCH_SIZE = 500
ABSOLUTE_UPLOAD_DIR = FileUploader.root.freeze
- EXCLUDED_SYSTEM_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/-/*".freeze
- EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*".freeze
- EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*".freeze
+ EXCLUDED_SYSTEM_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/-/*"
+ EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*"
+ EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*"
# Paths are relative to the upload directory
def each_file_batch(batch_size: FIND_BATCH_SIZE, &block)
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 5424298723e..e2911b4e6c8 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -38,8 +38,17 @@ module Gitlab
ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md')
end
+ def gitlab_helper
+ # Unfortunately the following does not work:
+ # - respond_to?(:gitlab)
+ # - respond_to?(:gitlab, true)
+ gitlab
+ rescue NoMethodError
+ nil
+ end
+
def release_automation?
- gitlab.mr_author == RELEASE_TOOLS_BOT
+ gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
end
def project_name
@@ -83,7 +92,8 @@ module Gitlab
docs: "~Documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
none: "",
qa: "~QA",
- test: "~test for `spec/features/*`"
+ test: "~test for `spec/features/*`",
+ engineering_productivity: "Engineering Productivity for CI config review"
}.freeze
CATEGORIES = {
%r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
@@ -110,7 +120,8 @@ module Gitlab
karma\.config\.js |
webpack\.config\.js |
package\.json |
- yarn\.lock
+ yarn\.lock |
+ \.gitlab/ci/frontend\.gitlab-ci\.yml
)\z}x => :frontend,
%r{\A(ee/)?db/(?!fixtures)[^/]+} => :database,
@@ -124,7 +135,8 @@ module Gitlab
%r{\A(ee/)?spec/(?!javascripts|frontend)[^/]+} => :backend,
%r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend,
%r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend,
- %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile|\.gitlab-ci\.yml)\z} => :backend,
+ %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
+ %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile)\z} => :backend,
%r{\A[A-Z_]+_VERSION\z} => :backend,
%r{\A\.rubocop(_todo)?\.yml\z} => :backend,
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 2789706aa3b..4ad66f61c2b 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -42,6 +42,8 @@ module Gitlab
area = role[/Test Automation Engineer(?:.*?, (\w+))/, 1]
area && labels.any?("devops::#{area.downcase}") if kind == :reviewer
+ when :engineering_productivity
+ role[/Engineering Productivity/] if kind == :reviewer
else
capabilities(project).include?("#{kind} #{category}")
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 57a413f8e04..5a42952796c 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -459,29 +459,19 @@ module Gitlab
check_trigger_permissions!(table)
- old_col = column_for(table, old)
- new_type = type || old_col.type
-
- add_column(table, new, new_type,
- limit: old_col.limit,
- precision: old_col.precision,
- scale: old_col.scale)
-
- # We set the default value _after_ adding the column so we don't end up
- # updating any existing data with the default value. This isn't
- # necessary since we copy over old values further down.
- change_column_default(table, new, old_col.default) unless old_col.default.nil?
+ create_column_from(table, old, new, type: type)
install_rename_triggers(table, old, new)
-
- update_column_in_batches(table, new, Arel::Table.new(table)[old])
-
- change_column_null(table, new, false) unless old_col.null
-
- copy_indexes(table, old, new)
- copy_foreign_keys(table, old, new)
end
+ # Reverses operations performed by rename_column_concurrently.
+ #
+ # This method takes care of removing previously installed triggers as well
+ # as removing the new column.
+ #
+ # table - The name of the database table.
+ # old - The name of the old column.
+ # new - The name of the new column.
def undo_rename_column_concurrently(table, old, new)
trigger_name = rename_trigger_name(table, old, new)
@@ -557,6 +547,18 @@ module Gitlab
remove_column(table, old)
end
+ # Reverses the operations performed by cleanup_concurrent_column_rename.
+ #
+ # This method adds back the old_column removed
+ # by cleanup_concurrent_column_rename.
+ # It also adds back the (old_column > new_column) trigger that is removed
+ # by cleanup_concurrent_column_rename.
+ #
+ # table - The name of the database table containing the column.
+ # old - The old column name.
+ # new - The new column name.
+ # type - The type of the old column. If no type is given the new column's
+ # type is used.
def undo_cleanup_concurrent_column_rename(table, old, new, type: nil)
if transaction_open?
raise 'undo_cleanup_concurrent_column_rename can not be run inside a transaction'
@@ -564,26 +566,9 @@ module Gitlab
check_trigger_permissions!(table)
- new_column = column_for(table, new)
-
- add_column(table, old, type || new_column.type,
- limit: new_column.limit,
- precision: new_column.precision,
- scale: new_column.scale)
-
- # We set the default value _after_ adding the column so we don't end up
- # updating any existing data with the default value. This isn't
- # necessary since we copy over old values further down.
- change_column_default(table, old, new_column.default) unless new_column.default.nil?
+ create_column_from(table, new, old, type: type)
install_rename_triggers(table, old, new)
-
- update_column_in_batches(table, old, Arel::Table.new(table)[new])
-
- change_column_null(table, old, false) unless new_column.null
-
- copy_indexes(table, new, old)
- copy_foreign_keys(table, new, old)
end
# Changes the column type of a table using a background migration.
@@ -1076,6 +1061,28 @@ into similar problems in the future (e.g. when new tables are created).
private
+ def create_column_from(table, old, new, type: nil)
+ old_col = column_for(table, old)
+ new_type = type || old_col.type
+
+ add_column(table, new, new_type,
+ limit: old_col.limit,
+ precision: old_col.precision,
+ scale: old_col.scale)
+
+ # We set the default value _after_ adding the column so we don't end up
+ # updating any existing data with the default value. This isn't
+ # necessary since we copy over old values further down.
+ change_column_default(table, new, old_col.default) unless old_col.default.nil?
+
+ update_column_in_batches(table, new, Arel::Table.new(table)[old])
+
+ change_column_null(table, new, false) unless old_col.null
+
+ copy_indexes(table, old, new)
+ copy_foreign_keys(table, old, new)
+ end
+
def validate_timestamp_column_name!(column_name)
return if PERMITTED_TIMESTAMP_COLUMNS.member?(column_name)
diff --git a/lib/gitlab/database/sha_attribute.rb b/lib/gitlab/database/sha_attribute.rb
index ddbabc9098e..776e80701f1 100644
--- a/lib/gitlab/database/sha_attribute.rb
+++ b/lib/gitlab/database/sha_attribute.rb
@@ -12,7 +12,7 @@ module Gitlab
# using them as if they were stored as string values. This gives you the
# ease of use of string values, but without the storage overhead.
class ShaAttribute < BINARY_TYPE
- PACK_FORMAT = 'H*'.freeze
+ PACK_FORMAT = 'H*'
# Casts binary data to a SHA1 in hexadecimal.
def deserialize(value)
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
index ec38bd769a3..5debb754943 100644
--- a/lib/gitlab/downtime_check/message.rb
+++ b/lib/gitlab/downtime_check/message.rb
@@ -5,8 +5,8 @@ module Gitlab
class Message
attr_reader :path, :offline
- OFFLINE = "\e[31moffline\e[0m".freeze
- ONLINE = "\e[32monline\e[0m".freeze
+ OFFLINE = "\e[31moffline\e[0m"
+ ONLINE = "\e[32monline\e[0m"
# path - The file path of the migration.
# offline - When set to `true` the migration will require downtime.
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 86e532766b1..20c31e06905 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -1,11 +1,12 @@
+# coding: utf-8
# frozen_string_literal: true
# rubocop: disable Rails/Output
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
- CANONICAL_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
- CANONICAL_EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ CANONICAL_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'
+ CANONICAL_EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'
CHECK_DIR = Rails.root.join('ee_compat_check')
IGNORED_FILES_REGEX = /VERSION|CHANGELOG\.md|doc\/.+/i.freeze
PLEASE_READ_THIS_BANNER = %Q{
diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb
index 2395e7be026..1d2f0d7bbf4 100644
--- a/lib/gitlab/etag_caching/store.rb
+++ b/lib/gitlab/etag_caching/store.rb
@@ -4,7 +4,7 @@ module Gitlab
module EtagCaching
class Store
EXPIRY_TIME = 20.minutes
- SHARED_STATE_NAMESPACE = 'etag:'.freeze
+ SHARED_STATE_NAMESPACE = 'etag:'
def get(key)
Gitlab::Redis::SharedState.with { |redis| redis.get(redis_shared_state_key(key)) }
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index df9f33baec2..8d13c74dca2 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -7,11 +7,11 @@ module Gitlab
# The ID of empty tree.
# See http://stackoverflow.com/a/40884093/1856239 and
# https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
- EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
BLANK_SHA = ('0' * 40).freeze
COMMIT_ID = /\A[0-9a-f]{40}\z/.freeze
- TAG_REF_PREFIX = "refs/tags/".freeze
- BRANCH_REF_PREFIX = "refs/heads/".freeze
+ TAG_REF_PREFIX = "refs/tags/"
+ BRANCH_REF_PREFIX = "refs/heads/"
BaseError = Class.new(StandardError)
CommandError = Class.new(BaseError)
diff --git a/lib/gitlab/git/lfs_pointer_file.rb b/lib/gitlab/git/lfs_pointer_file.rb
index b7019a221ac..efd84e30ad9 100644
--- a/lib/gitlab/git/lfs_pointer_file.rb
+++ b/lib/gitlab/git/lfs_pointer_file.rb
@@ -3,8 +3,8 @@
module Gitlab
module Git
class LfsPointerFile
- VERSION = "https://git-lfs.github.com/spec/v1".freeze
- VERSION_LINE = "version #{VERSION}".freeze
+ VERSION = "https://git-lfs.github.com/spec/v1"
+ VERSION_LINE = "version #{VERSION}"
def initialize(data)
@data = data
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 27032602828..4ea618f063b 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -15,9 +15,9 @@ module Gitlab
SEARCH_CONTEXT_LINES = 3
REV_LIST_COMMIT_LIMIT = 2_000
- GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
+ GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'
GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
- EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
+ EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
NoRepository = Class.new(StandardError)
InvalidRepository = Class.new(StandardError)
diff --git a/lib/gitlab/git/util.rb b/lib/gitlab/git/util.rb
index 03c2c1367b0..cf8571bf917 100644
--- a/lib/gitlab/git/util.rb
+++ b/lib/gitlab/git/util.rb
@@ -5,7 +5,7 @@
module Gitlab
module Git
module Util
- LINE_SEP = "\n".freeze
+ LINE_SEP = "\n"
def self.count_lines(string)
case string[-1]
diff --git a/lib/gitlab/github_import/issuable_finder.rb b/lib/gitlab/github_import/issuable_finder.rb
index 211915f1d87..c81603a1aa9 100644
--- a/lib/gitlab/github_import/issuable_finder.rb
+++ b/lib/gitlab/github_import/issuable_finder.rb
@@ -10,7 +10,7 @@ module Gitlab
attr_reader :project, :object
# The base cache key to use for storing/retrieving issuable IDs.
- CACHE_KEY = 'github-import/issuable-finder/%{project}/%{type}/%{iid}'.freeze
+ CACHE_KEY = 'github-import/issuable-finder/%{project}/%{type}/%{iid}'
# project - An instance of `Project`.
# object - The object to look up or set a database ID for.
diff --git a/lib/gitlab/github_import/label_finder.rb b/lib/gitlab/github_import/label_finder.rb
index d2479a8f565..cad39e48e43 100644
--- a/lib/gitlab/github_import/label_finder.rb
+++ b/lib/gitlab/github_import/label_finder.rb
@@ -6,7 +6,7 @@ module Gitlab
attr_reader :project
# The base cache key to use for storing/retrieving label IDs.
- CACHE_KEY = 'github-import/label-finder/%{project}/%{name}'.freeze
+ CACHE_KEY = 'github-import/label-finder/%{project}/%{name}'
# project - An instance of `Project`.
def initialize(project)
diff --git a/lib/gitlab/github_import/milestone_finder.rb b/lib/gitlab/github_import/milestone_finder.rb
index 5625730e796..a157a1e1ff5 100644
--- a/lib/gitlab/github_import/milestone_finder.rb
+++ b/lib/gitlab/github_import/milestone_finder.rb
@@ -6,7 +6,7 @@ module Gitlab
attr_reader :project
# The base cache key to use for storing/retrieving milestone IDs.
- CACHE_KEY = 'github-import/milestone-finder/%{project}/%{iid}'.freeze
+ CACHE_KEY = 'github-import/milestone-finder/%{project}/%{iid}'
# project - An instance of `Project`
def initialize(project)
diff --git a/lib/gitlab/github_import/page_counter.rb b/lib/gitlab/github_import/page_counter.rb
index c3db2d0b469..a3e7b3c1afc 100644
--- a/lib/gitlab/github_import/page_counter.rb
+++ b/lib/gitlab/github_import/page_counter.rb
@@ -9,7 +9,7 @@ module Gitlab
attr_reader :cache_key
# The base cache key to use for storing the last page number.
- CACHE_KEY = 'github-importer/page-counter/%{project}/%{collection}'.freeze
+ CACHE_KEY = 'github-importer/page-counter/%{project}/%{collection}'
def initialize(project, collection)
@cache_key = CACHE_KEY % { project: project.id, collection: collection }
diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb
index d4d1357f5a3..849a66d47ed 100644
--- a/lib/gitlab/github_import/parallel_scheduling.rb
+++ b/lib/gitlab/github_import/parallel_scheduling.rb
@@ -7,7 +7,7 @@ module Gitlab
# The base cache key to use for tracking already imported objects.
ALREADY_IMPORTED_CACHE_KEY =
- 'github-importer/already-imported/%{project}/%{collection}'.freeze
+ 'github-importer/already-imported/%{project}/%{collection}'
# project - An instance of `Project`.
# client - An instance of `Gitlab::GithubImport::Client`.
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
index 30283f147ef..51a532437bd 100644
--- a/lib/gitlab/github_import/user_finder.rb
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -16,17 +16,17 @@ module Gitlab
# The base cache key to use for caching user IDs for a given GitHub user
# ID.
- ID_CACHE_KEY = 'github-import/user-finder/user-id/%s'.freeze
+ ID_CACHE_KEY = 'github-import/user-finder/user-id/%s'
# The base cache key to use for caching user IDs for a given GitHub email
# address.
ID_FOR_EMAIL_CACHE_KEY =
- 'github-import/user-finder/id-for-email/%s'.freeze
+ 'github-import/user-finder/id-for-email/%s'
# The base cache key to use for caching the Email addresses of GitHub
# usernames.
EMAIL_FOR_USERNAME_CACHE_KEY =
- 'github-import/user-finder/email-for-username/%s'.freeze
+ 'github-import/user-finder/email-for-username/%s'
# project - An instance of `Project`
# client - An instance of `Gitlab::GithubImport::Client`
diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb
index 898733fea5d..e560f87bf98 100644
--- a/lib/gitlab/health_checks/gitaly_check.rb
+++ b/lib/gitlab/health_checks/gitaly_check.rb
@@ -5,7 +5,7 @@ module Gitlab
class GitalyCheck
extend BaseAbstractCheck
- METRIC_PREFIX = 'gitaly_health_check'.freeze
+ METRIC_PREFIX = 'gitaly_health_check'
class << self
def readiness
diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb
index 3e9a035010f..c0687cd9b79 100644
--- a/lib/gitlab/i18n/po_linter.rb
+++ b/lib/gitlab/i18n/po_linter.rb
@@ -170,13 +170,13 @@ module Gitlab
end
def translate_plural(entry)
- used_variables = entry.plural_id.scan(VARIABLE_REGEX)
- variables = fill_in_variables(used_variables)
-
numbers_covering_all_plurals.map do |number|
translation = FastGettext::Translation.n_(entry.msgid, entry.plural_id, number)
+ index = index_for_pluralization(number)
+ used_variables = index == 0 ? entry.msgid.scan(VARIABLE_REGEX) : entry.plural_id.scan(VARIABLE_REGEX)
+ variables = fill_in_variables(used_variables)
- translation % variables if used_variables.any?
+ translation % variables if variables.any?
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index bb46bd657e8..d08848a65a8 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -7,7 +7,7 @@ module Gitlab
# For every version update the version history in these docs must be kept up to date:
# - development/import_export.md
# - user/project/settings/import_export.md
- VERSION = '0.2.4'.freeze
+ VERSION = '0.2.4'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
index d39b6fe5955..c5fb39b7b52 100644
--- a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -10,7 +10,7 @@ module Gitlab
StrategyError = Class.new(StandardError)
- AFTER_EXPORT_LOCK_FILE_NAME = '.after_export_action'.freeze
+ AFTER_EXPORT_LOCK_FILE_NAME = '.after_export_action'
private
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index acb7f225b17..aaa70f0b36d 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -4,9 +4,9 @@ module Gitlab
module ImportExport
module AfterExportStrategies
class WebUploadStrategy < BaseAfterExportStrategy
- PUT_METHOD = 'PUT'.freeze
- POST_METHOD = 'POST'.freeze
- INVALID_HTTP_METHOD = 'invalid. Only PUT and POST methods allowed.'.freeze
+ PUT_METHOD = 'PUT'
+ POST_METHOD = 'POST'
+ INVALID_HTTP_METHOD = 'invalid. Only PUT and POST methods allowed.'
validates :url, addressable_url: true
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 42cd94add79..28d48ce6dfe 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -3,49 +3,62 @@
module Gitlab
module ImportExport
class AttributesFinder
- def initialize(included_attributes:, excluded_attributes:, methods:)
- @included_attributes = included_attributes || {}
- @excluded_attributes = excluded_attributes || {}
- @methods = methods || {}
+ def initialize(config:)
+ @tree = config[:tree] || {}
+ @included_attributes = config[:included_attributes] || {}
+ @excluded_attributes = config[:excluded_attributes] || {}
+ @methods = config[:methods] || {}
+ @preloads = config[:preloads] || {}
end
- def find(model_object)
- parsed_hash = find_attributes_only(model_object)
- parsed_hash.empty? ? model_object : { model_object => parsed_hash }
+ def find_root(model_key)
+ find(model_key, @tree[model_key])
end
- def parse(model_object)
- parsed_hash = find_attributes_only(model_object)
- yield parsed_hash unless parsed_hash.empty?
+ def find_relations_tree(model_key)
+ @tree[model_key]
end
- def find_included(value)
- key = key_from_hash(value)
- @included_attributes[key].nil? ? {} : { only: @included_attributes[key] }
+ def find_excluded_keys(klass_name)
+ @excluded_attributes[klass_name.to_sym]&.map(&:to_s) || []
end
- def find_excluded(value)
- key = key_from_hash(value)
- @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] }
+ private
+
+ def find(model_key, model_tree)
+ {
+ only: @included_attributes[model_key],
+ except: @excluded_attributes[model_key],
+ methods: @methods[model_key],
+ include: resolve_model_tree(model_tree),
+ preload: resolve_preloads(model_key, model_tree)
+ }.compact
end
- def find_method(value)
- key = key_from_hash(value)
- @methods[key].nil? ? {} : { methods: @methods[key] }
+ def resolve_preloads(model_key, model_tree)
+ model_tree
+ .map { |submodel_key, submodel_tree| resolve_preload(model_key, submodel_key, submodel_tree) }
+ .compact
+ .to_h
+ .deep_merge(@preloads[model_key].to_h)
+ .presence
end
- def find_excluded_keys(klass_name)
- @excluded_attributes[klass_name.to_sym]&.map(&:to_s) || []
+ def resolve_preload(parent_model_key, model_key, model_tree)
+ return if @methods[parent_model_key]&.include?(model_key)
+
+ [model_key, resolve_preloads(model_key, model_tree)]
end
- private
+ def resolve_model_tree(model_tree)
+ return unless model_tree
- def find_attributes_only(value)
- find_included(value).merge(find_excluded(value)).merge(find_method(value))
+ model_tree
+ .map(&method(:resolve_model))
end
- def key_from_hash(value)
- value.is_a?(Hash) ? value.first.first : value
+ def resolve_model(model_key, model_tree)
+ { model_key => find(model_key, model_tree) }
end
end
end
diff --git a/lib/gitlab/import_export/config.rb b/lib/gitlab/import_export/config.rb
index f6cd4eb5e0c..6f4919ead4e 100644
--- a/lib/gitlab/import_export/config.rb
+++ b/lib/gitlab/import_export/config.rb
@@ -3,70 +3,49 @@
module Gitlab
module ImportExport
class Config
+ def initialize
+ @hash = parse_yaml
+ @hash.deep_symbolize_keys!
+ @ee_hash = @hash.delete(:ee) || {}
+
+ @hash[:tree] = normalize_tree(@hash[:tree])
+ @ee_hash[:tree] = normalize_tree(@ee_hash[:tree] || {})
+ end
+
# Returns a Hash of the YAML file, including EE specific data if EE is
# used.
def to_h
- hash = parse_yaml
- ee_hash = hash['ee']
-
- if merge? && ee_hash
- ee_hash.each do |key, value|
- if key == 'project_tree'
- merge_project_tree(value, hash[key])
- else
- merge_attributes_list(value, hash[key])
- end
- end
+ if merge_ee?
+ deep_merge(@hash, @ee_hash)
+ else
+ @hash
end
-
- # We don't want to expose this section after this point, as it is no
- # longer needed.
- hash.delete('ee')
-
- hash
end
- # Merges a project relationships tree into the target tree.
- #
- # @param [Array<Hash|Symbol>] source_values
- # @param [Array<Hash|Symbol>] target_values
- def merge_project_tree(source_values, target_values)
- source_values.each do |value|
- if value.is_a?(Hash)
- # Examples:
- #
- # { 'project_tree' => [{ 'labels' => [...] }] }
- # { 'notes' => [:author, { 'events' => [:push_event_payload] }] }
- value.each do |key, val|
- target = target_values
- .find { |h| h.is_a?(Hash) && h[key] }
+ private
- if target
- merge_project_tree(val, target[key])
- else
- target_values << { key => val.dup }
- end
- end
- else
- # Example: :priorities, :author, etc
- target_values << value
- end
+ def deep_merge(hash_a, hash_b)
+ hash_a.deep_merge(hash_b) do |_, this_val, other_val|
+ this_val.to_a + other_val.to_a
end
end
- # Merges a Hash containing a flat list of attributes, such as the entries
- # in a `excluded_attributes` section.
- #
- # @param [Hash] source_values
- # @param [Hash] target_values
- def merge_attributes_list(source_values, target_values)
- source_values.each do |key, values|
- target_values[key] ||= []
- target_values[key].concat(values)
+ def normalize_tree(item)
+ case item
+ when Array
+ item.reduce({}) do |hash, subitem|
+ hash.merge!(normalize_tree(subitem))
+ end
+ when Hash
+ item.transform_values(&method(:normalize_tree))
+ when Symbol
+ { item => {} }
+ else
+ raise ArgumentError, "#{item} needs to be Array, Hash, Symbol or NilClass"
end
end
- def merge?
+ def merge_ee?
Gitlab.ee?
end
diff --git a/lib/gitlab/import_export/fast_hash_serializer.rb b/lib/gitlab/import_export/fast_hash_serializer.rb
new file mode 100644
index 00000000000..a6ab4f3a3d9
--- /dev/null
+++ b/lib/gitlab/import_export/fast_hash_serializer.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+# ActiveModel::Serialization (https://github.com/rails/rails/blob/v5.0.7/activemodel/lib/active_model/serialization.rb#L184)
+# is simple in that it recursively calls `as_json` on each object to
+# serialize everything. However, for a model like a Project, this can
+# generate a query for every single association, which can add up to tens
+# of thousands of queries and lead to memory bloat.
+#
+# To improve this, we can do several things:
+
+# 1. Use the option tree in http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
+# to generate the necessary preload clauses.
+#
+# 2. We observe that a single project has many issues, merge requests,
+# etc. Instead of serializing everything at once, which could lead to
+# database timeouts and high memory usage, we take each top-level
+# association and serialize the data in batches.
+#
+# For example, we serialize the first 100 issues and preload all of
+# their associated events, notes, etc. before moving onto the next
+# batch. When we're done, we serialize merge requests in the same way.
+# We repeat this pattern for the remaining associations specified in
+# import_export.yml.
+module Gitlab
+ module ImportExport
+ class FastHashSerializer
+ attr_reader :subject, :tree
+
+ BATCH_SIZE = 100
+
+ def initialize(subject, tree, batch_size: BATCH_SIZE)
+ @subject = subject
+ @batch_size = batch_size
+ @tree = tree
+ end
+
+ # Serializes the subject into a Hash for the given option tree
+ # (e.g. Project#as_json)
+ def execute
+ simple_serialize.merge(serialize_includes)
+ end
+
+ private
+
+ def simple_serialize
+ subject.as_json(
+ tree.merge(include: nil, preloads: nil))
+ end
+
+ def serialize_includes
+ return {} unless includes
+
+ includes
+ .map(&method(:serialize_include_definition))
+ .compact
+ .to_h
+ end
+
+ # definition:
+ # { labels: { includes: ... } }
+ def serialize_include_definition(definition)
+ raise ArgumentError, 'definition needs to be Hash' unless definition.is_a?(Hash)
+ raise ArgumentError, 'definition needs to have exactly one Hash element' unless definition.one?
+
+ key = definition.first.first
+ options = definition.first.second
+
+ record = subject.public_send(key) # rubocop: disable GitlabSecurity/PublicSend
+ return unless record
+
+ serialized_record = serialize_record(key, record, options)
+ return unless serialized_record
+
+ # `#as_json` always returns keys as `strings`
+ [key.to_s, serialized_record]
+ end
+
+ def serialize_record(key, record, options)
+ unless record.respond_to?(:as_json)
+ raise "Invalid type of #{key} is #{record.class}"
+ end
+
+ # no has-many relation
+ unless record.is_a?(ActiveRecord::Relation)
+ return record.as_json(options)
+ end
+
+ # has-many relation
+ data = []
+
+ record.in_batches(of: @batch_size) do |batch| # rubocop:disable Cop/InBatches
+ batch = batch.preload(preloads[key]) if preloads&.key?(key)
+ data += batch.as_json(options)
+ end
+
+ data
+ end
+
+ def includes
+ tree[:include]
+ end
+
+ def preloads
+ tree[:preload]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index bd0f3e70749..511b702553e 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -3,87 +3,92 @@
# This list _must_ only contain relationships that are available to both CE and
# EE. EE specific relationships must be defined in the `ee` section further
# down below.
-project_tree:
- - labels:
- - :priorities
- - milestones:
- - events:
- - :push_event_payload
- - issues:
- - events:
- - :push_event_payload
- - :timelogs
- - notes:
- - :author
- - events:
- - :push_event_payload
- - label_links:
- - label:
- - :priorities
- - milestone:
- - events:
- - :push_event_payload
- - resource_label_events:
- - label:
- - :priorities
- - :issue_assignees
- - snippets:
- - :award_emoji
- - notes:
- - :author
- - releases:
- - :links
- - project_members:
- - :user
- - merge_requests:
- - :metrics
- - notes:
- - :author
+tree:
+ project:
+ - labels:
+ - :priorities
+ - milestones:
- events:
- :push_event_payload
- - :suggestions
- - merge_request_diff:
- - :merge_request_diff_commits
- - :merge_request_diff_files
- - events:
- - :push_event_payload
- - :timelogs
- - label_links:
- - label:
- - :priorities
- - milestone:
+ - issues:
- events:
- :push_event_payload
- - resource_label_events:
- - label:
- - :priorities
- - ci_pipelines:
- - notes:
- - :author
+ - :timelogs
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - label_links:
+ - label:
+ - :priorities
+ - milestone:
+ - events:
+ - :push_event_payload
+ - resource_label_events:
+ - label:
+ - :priorities
+ - :issue_assignees
+ - snippets:
+ - :award_emoji
+ - notes:
+ - :author
+ - releases:
+ - :links
+ - project_members:
+ - :user
+ - merge_requests:
+ - :metrics
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - :suggestions
+ - merge_request_diff:
+ - :merge_request_diff_commits
+ - :merge_request_diff_files
- events:
- :push_event_payload
- - stages:
- - :statuses
- - :auto_devops
- - :triggers
- - :pipeline_schedules
- - :services
- - protected_branches:
- - :merge_access_levels
- - :push_access_levels
- - protected_tags:
- - :create_access_levels
- - :project_feature
- - :custom_attributes
- - :prometheus_metrics
- - :project_badges
- - :ci_cd_settings
- - :error_tracking_setting
- - :metrics_setting
- - boards:
- - lists:
- - label:
- - :priorities
+ - :timelogs
+ - label_links:
+ - label:
+ - :priorities
+ - milestone:
+ - events:
+ - :push_event_payload
+ - resource_label_events:
+ - label:
+ - :priorities
+ - ci_pipelines:
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - stages:
+ - :statuses
+ - :external_pull_request
+ - :external_pull_requests
+ - :auto_devops
+ - :triggers
+ - :pipeline_schedules
+ - :services
+ - protected_branches:
+ - :merge_access_levels
+ - :push_access_levels
+ - protected_tags:
+ - :create_access_levels
+ - :project_feature
+ - :custom_attributes
+ - :prometheus_metrics
+ - :project_badges
+ - :ci_cd_settings
+ - :error_tracking_setting
+ - :metrics_setting
+ - boards:
+ - lists:
+ - label:
+ - :priorities
+ group_members:
+ - :user
# Only include the following attributes for the models specified.
included_attributes:
@@ -223,12 +228,25 @@ methods:
- :type
lists:
- :list_type
+ ci_pipelines:
+ - :notes
+
+preloads:
+ statuses:
+ # TODO: We cannot preload tags, as they are not part of `GenericCommitStatus`
+ # tags: # needed by tag_list
+ project: # deprecated: needed by coverage_regex of Ci::Build
+ merge_requests:
+ source_project: # needed by source_branch_sha and diff_head_sha
+ target_project: # needed by target_branch_sha
+ assignees: # needed by assigne_id that is implemented by DeprecatedAssignee
# EE specific relationships and settings to include. All of this will be merged
# into the previous structures if EE is used.
ee:
- project_tree:
- - protected_branches:
- - :unprotect_access_levels
- - protected_environments:
- - :deploy_access_levels
+ tree:
+ project:
+ protected_branches:
+ - :unprotect_access_levels
+ protected_environments:
+ - :deploy_access_levels
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
deleted file mode 100644
index a92e3862361..00000000000
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- # Generates a hash that conforms with http://apidock.com/rails/Hash/to_json
- # and its peculiar options.
- class JsonHashBuilder
- def self.build(model_objects, attributes_finder)
- new(model_objects, attributes_finder).build
- end
-
- def initialize(model_objects, attributes_finder)
- @model_objects = model_objects
- @attributes_finder = attributes_finder
- end
-
- def build
- process_model_objects(@model_objects)
- end
-
- private
-
- # Called when the model is actually a hash containing other relations (more models)
- # Returns the config in the right format for calling +to_json+
- #
- # +model_object_hash+ - A model relationship such as:
- # {:merge_requests=>[:merge_request_diff, :notes]}
- def process_model_objects(model_object_hash)
- json_config_hash = {}
- current_key = model_object_hash.first.first
-
- model_object_hash.values.flatten.each do |model_object|
- @attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash }
- handle_model_object(current_key, model_object, json_config_hash)
- end
-
- json_config_hash
- end
-
- # Creates or adds to an existing hash an individual model or list
- #
- # +current_key+ main model that will be a key in the hash
- # +model_object+ model or list of models to include in the hash
- # +json_config_hash+ the original hash containing the root model
- def handle_model_object(current_key, model_object, json_config_hash)
- model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object
-
- if json_config_hash[current_key]
- add_model_value(current_key, model_or_sub_model, json_config_hash)
- else
- create_model_value(current_key, model_or_sub_model, json_config_hash)
- end
- end
-
- # Constructs a new hash that will hold the configuration for that particular object
- # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
- #
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- # +json_config_hash+ the original hash containing the root model
- def create_model_value(current_key, value, json_config_hash)
- json_config_hash[current_key] = parse_hash(value) || { include: value }
- end
-
- # Calls attributes finder to parse the hash and add any attributes to it
- #
- # +value+ existing model to be included in the hash
- # +parsed_hash+ the original hash
- def parse_hash(value)
- return if already_contains_methods?(value)
-
- @attributes_finder.parse(value) do |hash|
- { include: hash_or_merge(value, hash) }
- end
- end
-
- def already_contains_methods?(value)
- value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
- end
-
- # Adds new model configuration to an existing hash with key +current_key+
- # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
- #
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- # +json_config_hash+ the original hash containing the root model
- def add_model_value(current_key, value, json_config_hash)
- @attributes_finder.parse(value) do |hash|
- value = { value => hash } unless value.is_a?(Hash)
- end
-
- add_to_array(current_key, json_config_hash, value)
- end
-
- # Adds new model configuration to an existing hash with key +current_key+
- # it creates a new array if it was previously a single value
- #
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- # +json_config_hash+ the original hash containing the root model
- def add_to_array(current_key, json_config_hash, value)
- old_values = json_config_hash[current_key][:include]
-
- json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
- end
-
- # Construct a new hash or merge with an existing one a model configuration
- # This is to fulfil +to_json+ requirements.
- #
- # +hash+ hash containing configuration generated mainly from +@attributes_finder+
- # +value+ existing model to be included in the hash
- def hash_or_merge(value, hash)
- value.is_a?(Hash) ? value.merge(hash) : { value => hash }
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 91fe4e5d074..2dd18616cd6 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -58,11 +58,13 @@ module Gitlab
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project.
def create_relations
- default_relation_list.each do |relation|
- if relation.is_a?(Hash)
- create_sub_relations(relation, @tree_hash)
- elsif @tree_hash[relation.to_s].present?
- save_relation_hash(@tree_hash[relation.to_s], relation)
+ project_relations_without_project_members.each do |relation_key, relation_definition|
+ relation_key_s = relation_key.to_s
+
+ if relation_definition.present?
+ create_sub_relations(relation_key_s, relation_definition, @tree_hash)
+ elsif @tree_hash[relation_key_s].present?
+ save_relation_hash(relation_key_s, @tree_hash[relation_key_s])
end
end
@@ -71,7 +73,7 @@ module Gitlab
@saved
end
- def save_relation_hash(relation_hash_batch, relation_key)
+ def save_relation_hash(relation_key, relation_hash_batch)
relation_hash = create_relation(relation_key, relation_hash_batch)
remove_group_models(relation_hash) if relation_hash.is_a?(Array)
@@ -91,10 +93,13 @@ module Gitlab
end
end
- def default_relation_list
- reader.tree.reject do |model|
- model.is_a?(Hash) && model[:project_members]
- end
+ def project_relations_without_project_members
+ # We remove `project_members` as they are deserialized separately
+ project_relations.except(:project_members)
+ end
+
+ def project_relations
+ reader.attributes_finder.find_relations_tree(:project)
end
def restore_project
@@ -107,7 +112,7 @@ module Gitlab
def project_params
@project_params ||= begin
- attrs = json_params.merge(override_params).merge(visibility_level)
+ attrs = json_params.merge(override_params).merge(visibility_level, external_label)
# Cleaning all imported and overridden params
Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
@@ -135,6 +140,13 @@ module Gitlab
{ 'visibility_level' => level }
end
+ def external_label
+ label = override_params['external_authorization_classification_label'].presence ||
+ json_params['external_authorization_classification_label'].presence
+
+ { 'external_authorization_classification_label' => label }
+ end
+
# Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+
@@ -143,8 +155,7 @@ module Gitlab
# issue, finds any subrelations such as notes, creates them and assign them back to the hash
#
# Recursively calls this method if the sub-relation is a hash containing more sub-relations
- def create_sub_relations(relation, tree_hash, save: true)
- relation_key = relation.keys.first.to_s
+ def create_sub_relations(relation_key, relation_definition, tree_hash, save: true)
return if tree_hash[relation_key].blank?
tree_array = [tree_hash[relation_key]].flatten
@@ -164,13 +175,13 @@ module Gitlab
# But we can't have it in the upper level or GC won't get rid of the AR objects
# after we save the batch.
Project.transaction do
- process_sub_relation(relation, relation_item)
+ process_sub_relation(relation_key, relation_definition, relation_item)
# For every subrelation that hangs from Project, save the associated records altogether
# This effectively batches all records per subrelation item, only keeping those in memory
# We have to keep in mind that more batch granularity << Memory, but >> Slowness
if save
- save_relation_hash([relation_item], relation_key)
+ save_relation_hash(relation_key, [relation_item])
tree_hash[relation_key].delete(relation_item)
end
end
@@ -179,37 +190,35 @@ module Gitlab
tree_hash.delete(relation_key) if save
end
- def process_sub_relation(relation, relation_item)
- relation.values.flatten.each do |sub_relation|
+ def process_sub_relation(relation_key, relation_definition, relation_item)
+ relation_definition.each do |sub_relation_key, sub_relation_definition|
# We just use author to get the user ID, do not attempt to create an instance.
- next if sub_relation == :author
+ next if sub_relation_key == :author
- create_sub_relations(sub_relation, relation_item, save: false) if sub_relation.is_a?(Hash)
+ sub_relation_key_s = sub_relation_key.to_s
- relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
- relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
- end
- end
+ # create dependent relations if present
+ if sub_relation_definition.present?
+ create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false)
+ end
- def assign_relation_hash(relation_item, sub_relation)
- if sub_relation.is_a?(Hash)
- relation_hash = relation_item[sub_relation.keys.first.to_s]
- sub_relation = sub_relation.keys.first
- else
- relation_hash = relation_item[sub_relation.to_s]
+ # transform relation hash to actual object
+ sub_relation_hash = relation_item[sub_relation_key_s]
+ if sub_relation_hash.present?
+ relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash)
+ end
end
-
- [relation_hash, sub_relation]
end
- def create_relation(relation, relation_hash_list)
+ def create_relation(relation_key, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
- Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: relation_hash,
- members_mapper: members_mapper,
- user: @user,
- project: @restored_project,
- excluded_keys: excluded_keys_for_relation(relation))
+ Gitlab::ImportExport::RelationFactory.create(
+ relation_sym: relation_key.to_sym,
+ relation_hash: relation_hash,
+ members_mapper: members_mapper,
+ user: @user,
+ project: @restored_project,
+ excluded_keys: excluded_keys_for_relation(relation_key))
end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 2255635acdf..f75f69b2c75 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -18,7 +18,10 @@ module Gitlab
def save
mkdir_p(@shared.export_path)
- File.write(full_path, project_json_tree)
+ project_tree = serialize_project_tree
+ fix_project_tree(project_tree)
+ File.write(full_path, project_tree.to_json)
+
true
rescue => e
@shared.error(e)
@@ -27,27 +30,31 @@ module Gitlab
private
- def project_json_tree
+ def fix_project_tree(project_tree)
if @params[:description].present?
- project_json['description'] = @params[:description]
+ project_tree['description'] = @params[:description]
end
- project_json['project_members'] += group_members_json
-
- RelationRenameService.add_new_associations(project_json)
+ project_tree['project_members'] += group_members_array
- project_json.to_json
+ RelationRenameService.add_new_associations(project_tree)
end
- def project_json
- @project_json ||= @project.as_json(reader.project_tree)
+ def serialize_project_tree
+ if Feature.enabled?(:export_fast_serialize, default_enabled: true)
+ Gitlab::ImportExport::FastHashSerializer
+ .new(@project, reader.project_tree)
+ .execute
+ else
+ @project.as_json(reader.project_tree)
+ end
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
- def group_members_json
+ def group_members_array
group_members.as_json(reader.group_members_tree).each do |group_member|
group_member['source_type'] = 'Project' # Make group members project members of the future import
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 8bdf6ca491d..9e81c6a3d07 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -7,42 +7,22 @@ module Gitlab
def initialize(shared:)
@shared = shared
- config_hash = ImportExport::Config.new.to_h.deep_symbolize_keys
- @tree = config_hash[:project_tree]
- @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes],
- excluded_attributes: config_hash[:excluded_attributes],
- methods: config_hash[:methods])
+
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(
+ config: ImportExport::Config.new.to_h)
end
# Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
# for outputting a project in JSON format, including its relations and sub relations.
def project_tree
- attributes = @attributes_finder.find(:project)
- project_attributes = attributes.is_a?(Hash) ? attributes[:project] : {}
-
- project_attributes.merge(include: build_hash(@tree))
+ attributes_finder.find_root(:project)
rescue => e
@shared.error(e)
false
end
def group_members_tree
- @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
- end
-
- private
-
- # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
- #
- # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file
- def build_hash(model_list)
- model_list.map do |model_objects|
- if model_objects.is_a?(Hash)
- Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder)
- else
- @attributes_finder.find(model_objects)
- end
- end
+ attributes_finder.find_root(:group_members)
end
end
end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 8b346f6d7d2..eece4edf895 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -2,9 +2,9 @@
module Gitlab
module IncomingEmail
- UNSUBSCRIBE_SUFFIX = '-unsubscribe'.freeze
- UNSUBSCRIBE_SUFFIX_LEGACY = '+unsubscribe'.freeze
- WILDCARD_PLACEHOLDER = '%{key}'.freeze
+ UNSUBSCRIBE_SUFFIX = '-unsubscribe'
+ UNSUBSCRIBE_SUFFIX_LEGACY = '+unsubscribe'
+ WILDCARD_PLACEHOLDER = '%{key}'
class << self
def enabled?
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index e97e961771c..90dbe4d005d 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -17,7 +17,7 @@ module Gitlab
# push to that array when done. Once the waiter has popped `count` items, it
# knows all the jobs are done.
class JobWaiter
- KEY_PREFIX = "gitlab:job_waiter".freeze
+ KEY_PREFIX = "gitlab:job_waiter"
def self.notify(key, jid)
Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 6e4286589c1..16ed0cb0f8e 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -3,12 +3,12 @@
module Gitlab
module Kubernetes
module Helm
- HELM_VERSION = '2.14.3'.freeze
- KUBECTL_VERSION = '1.11.10'.freeze
- NAMESPACE = 'gitlab-managed-apps'.freeze
- SERVICE_ACCOUNT = 'tiller'.freeze
- CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze
- CLUSTER_ROLE = 'cluster-admin'.freeze
+ HELM_VERSION = '2.14.3'
+ KUBECTL_VERSION = '1.11.10'
+ NAMESPACE = 'gitlab-managed-apps'
+ SERVICE_ACCOUNT = 'tiller'
+ CLUSTER_ROLE_BINDING = 'tiller-admin'
+ CLUSTER_ROLE = 'cluster-admin'
end
end
end
diff --git a/lib/gitlab/kubernetes/pod.rb b/lib/gitlab/kubernetes/pod.rb
index 81317e532b2..d247662dc3b 100644
--- a/lib/gitlab/kubernetes/pod.rb
+++ b/lib/gitlab/kubernetes/pod.rb
@@ -3,11 +3,11 @@
module Gitlab
module Kubernetes
module Pod
- PENDING = 'Pending'.freeze
- RUNNING = 'Running'.freeze
- SUCCEEDED = 'Succeeded'.freeze
- FAILED = 'Failed'.freeze
- UNKNOWN = 'Unknown'.freeze
+ PENDING = 'Pending'
+ RUNNING = 'Running'
+ SUCCEEDED = 'Succeeded'
+ FAILED = 'Failed'
+ UNKNOWN = 'Unknown'
PHASES = [PENDING, RUNNING, SUCCEEDED, FAILED, UNKNOWN].freeze
end
end
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 128a5dd8936..2ec8c268d09 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -40,7 +40,7 @@ module Gitlab
end
def self.cache_key
- 'logger:'.freeze + self.full_log_path.to_s
+ 'logger:' + self.full_log_path.to_s
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index c068f8017fd..2ed5878286a 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -15,7 +15,7 @@ module Gitlab
attach_to :action_view
- SERIES = 'views'.freeze
+ SERIES = 'views'
def render_template(event)
track(event) if current_transaction
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index d7986685c65..ba2a0b2ecf8 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -14,7 +14,7 @@ module Gitlab
THREAD_KEY = :_gitlab_metrics_transaction
# The series to store events (e.g. Git pushes) in.
- EVENT_SERIES = 'events'.freeze
+ EVENT_SERIES = 'events'
attr_reader :tags, :values, :method, :metrics
@@ -78,7 +78,7 @@ module Gitlab
# tags - A set of tags to attach to the event.
def add_event(event_name, tags = {})
filtered_tags = filter_tags(tags)
- self.class.transaction_metric(event_name, :counter, prefix: 'event_', use_feature_flag: true, tags: filtered_tags).increment(filtered_tags.merge(labels))
+ self.class.transaction_metric(event_name, :counter, prefix: 'event_', tags: filtered_tags).increment(filtered_tags.merge(labels))
@metrics << Metric.new(EVENT_SERIES, { count: 1 }, filtered_tags.merge(event: event_name), :event)
end
@@ -155,12 +155,11 @@ module Gitlab
with_feature :prometheus_metrics_transaction_allocated_memory
end
- def self.transaction_metric(name, type, prefix: nil, use_feature_flag: false, tags: {})
+ def self.transaction_metric(name, type, prefix: nil, tags: {})
metric_name = "gitlab_transaction_#{prefix}#{name}_total".to_sym
fetch_metric(type, metric_name) do
docstring "Transaction #{prefix}#{name} #{type}"
base_labels tags.merge(BASE_LABELS)
- with_feature "prometheus_transaction_#{prefix}#{name}_total".to_sym if use_feature_flag
if type == :gauge
multiprocess_mode :livesum
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index b2a43d46fb2..fa17548723e 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -3,8 +3,8 @@
module Gitlab
module Metrics
class WebTransaction < Transaction
- CONTROLLER_KEY = 'action_controller.instance'.freeze
- ENDPOINT_KEY = 'api.endpoint'.freeze
+ CONTROLLER_KEY = 'action_controller.instance'
+ ENDPOINT_KEY = 'api.endpoint'
ALLOWED_SUFFIXES = Set.new(%w[json js atom rss xml zip])
def initialize(env)
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 433151b80e7..0ee9563c227 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -28,11 +28,11 @@
module Gitlab
module Middleware
class Multipart
- RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'.freeze
+ RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
class Handler
def initialize(env, message)
- @request = ActionDispatch::Request.new(env)
+ @request = Rack::Request.new(env)
@rewritten_fields = message['rewritten_fields']
@open_files = []
end
@@ -50,7 +50,7 @@ module Gitlab
value = decorate_params_value(value, @request.params[key])
end
- @request.update_param(key, value)
+ update_param(key, value)
end
yield
@@ -92,6 +92,20 @@ module Gitlab
::UploadedFile.from_params(params, key, allowed_paths)
end
+
+ # update_params ensures that both rails controllers and rack middleware can find
+ # workhorse accelerate files in the request
+ def update_param(key, value)
+ # we make sure we have key in POST otherwise update_params will add it in GET
+ @request.POST[key] ||= value
+
+ # this will force Rack::Request to properly update env keys
+ @request.update_param(key, value)
+
+ # ActionDispatch::Request is based on Rack::Request but it caches params
+ # inside other env keys, here we ensure everything is updated correctly
+ ActionDispatch::Request.new(@request.env).update_param(key, value)
+ end
end
def initialize(app)
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 53e55269c6e..802ff18fc58 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -5,9 +5,9 @@ module Gitlab
class ReadOnly
class Controller
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
- APPLICATION_JSON = 'application/json'.freeze
+ APPLICATION_JSON = 'application/json'
APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
- ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'.freeze
+ ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
WHITELISTED_GIT_ROUTES = {
'projects/git_http' => %w{git_upload_pack git_receive_pack}
diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb
index 16df0700b08..4899b1d3234 100644
--- a/lib/gitlab/pages.rb
+++ b/lib/gitlab/pages.rb
@@ -1,7 +1,22 @@
# frozen_string_literal: true
module Gitlab
- module Pages
+ class Pages
VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Pages-Api-Request'.freeze
+
+ include JwtAuthenticatable
+
+ class << self
+ def verify_api_request(request_headers)
+ decode_jwt_for_issuer('gitlab-pages', request_headers[INTERNAL_API_REQUEST_HEADER])
+ rescue JWT::DecodeError
+ false
+ end
+
+ def secret_path
+ Gitlab.config.pages.secret_file
+ end
+ end
end
end
diff --git a/lib/gitlab/patch/chronic_duration.rb b/lib/gitlab/patch/chronic_duration.rb
new file mode 100644
index 00000000000..ab3cba3657f
--- /dev/null
+++ b/lib/gitlab/patch/chronic_duration.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# Fixes a bug where parsing months doesn't take into account
+# the ChronicDuration.days_per_week setting
+#
+# We can remove this when we do a refactor and push upstream in
+# https://gitlab.com/gitlab-org/gitlab-ce/issues/66637
+
+module Gitlab
+ module Patch
+ module ChronicDuration
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def duration_units_seconds_multiplier(unit)
+ return 0 unless duration_units_list.include?(unit)
+
+ case unit
+ when 'months'
+ 3600 * ::ChronicDuration.hours_per_day * ::ChronicDuration.days_per_month
+ else
+ super
+ end
+ end
+
+ # ChronicDuration#output uses 1mo = 4w as the conversion so we do the same here.
+ # We do need to add a special case for the default days_per_week value because
+ # we want to retain existing behavior for the default case
+ def days_per_month
+ ::ChronicDuration.days_per_week == 7 ? 30 : ::ChronicDuration.days_per_week * 4
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index d9c28ff1181..ee2ef91c65c 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -125,9 +125,9 @@ module Gitlab
# allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of
# `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
- PATH_START_CHAR = '[a-zA-Z0-9_\.]'.freeze
- PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]*'.freeze
- NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+ PATH_START_CHAR = '[a-zA-Z0-9_\.]'
+ PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]*'
+ NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'
NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
index 68af290d069..4445c876e7a 100644
--- a/lib/gitlab/performance_bar.rb
+++ b/lib/gitlab/performance_bar.rb
@@ -2,7 +2,7 @@
module Gitlab
module PerformanceBar
- ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'.freeze
+ ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'
EXPIRY_TIME_L1_CACHE = 1.minute
EXPIRY_TIME_L2_CACHE = 5.minutes
diff --git a/lib/gitlab/polling_interval.rb b/lib/gitlab/polling_interval.rb
index 0f69990df63..fd66787b029 100644
--- a/lib/gitlab/polling_interval.rb
+++ b/lib/gitlab/polling_interval.rb
@@ -2,7 +2,7 @@
module Gitlab
class PollingInterval
- HEADER_NAME = 'Poll-Interval'.freeze
+ HEADER_NAME = 'Poll-Interval'
def self.set_header(response, interval:)
if polling_enabled?
diff --git a/lib/gitlab/private_commit_email.rb b/lib/gitlab/private_commit_email.rb
index 536fc9dae3a..886c2c32d36 100644
--- a/lib/gitlab/private_commit_email.rb
+++ b/lib/gitlab/private_commit_email.rb
@@ -2,7 +2,7 @@
module Gitlab
module PrivateCommitEmail
- TOKEN = "_private".freeze
+ TOKEN = "_private"
class << self
def regex
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 425c30d67fe..3f26b84be20 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -3,7 +3,7 @@
module Gitlab
module Profiler
- FILTERED_STRING = '[FILTERED]'.freeze
+ FILTERED_STRING = '[FILTERED]'
IGNORE_BACKTRACES = %w[
lib/gitlab/i18n.rb
diff --git a/lib/gitlab/prometheus/additional_metrics_parser.rb b/lib/gitlab/prometheus/additional_metrics_parser.rb
index bd4ca578840..ee3d98f3602 100644
--- a/lib/gitlab/prometheus/additional_metrics_parser.rb
+++ b/lib/gitlab/prometheus/additional_metrics_parser.rb
@@ -3,7 +3,7 @@
module Gitlab
module Prometheus
module AdditionalMetricsParser
- CONFIG_ROOT = 'config/prometheus'.freeze
+ CONFIG_ROOT = 'config/prometheus'
MUTEX = Mutex.new
extend self
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
index 682edfc4259..93c0f3132d0 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -7,10 +7,12 @@ module Gitlab
keys: [
:create,
:description,
+ :label,
:merge_when_pipeline_succeeds,
:remove_source_branch,
:target,
- :title
+ :title,
+ :unlabel
]
},
ci: {
@@ -18,6 +20,11 @@ module Gitlab
}
}).freeze
+ MULTI_VALUE_OPTIONS = [
+ %w[merge_request label],
+ %w[merge_request unlabel]
+ ].freeze
+
NAMESPACE_ALIASES = HashWithIndifferentAccess.new({
mr: :merge_request
}).freeze
@@ -49,13 +56,27 @@ module Gitlab
next if [namespace, key].any?(&:nil?)
- options[namespace] ||= HashWithIndifferentAccess.new
- options[namespace][key] = value
+ store_option_info(options, namespace, key, value)
end
options
end
+ def store_option_info(options, namespace, key, value)
+ options[namespace] ||= HashWithIndifferentAccess.new
+
+ if option_multi_value?(namespace, key)
+ options[namespace][key] ||= HashWithIndifferentAccess.new(0)
+ options[namespace][key][value] += 1
+ else
+ options[namespace][key] = value
+ end
+ end
+
+ def option_multi_value?(namespace, key)
+ MULTI_VALUE_OPTIONS.any? { |arr| arr == [namespace, key] }
+ end
+
def parse_option(option)
parts = OPTION_MATCHER.match(option)
return unless parts
diff --git a/lib/gitlab/query_limiting/middleware.rb b/lib/gitlab/query_limiting/middleware.rb
index 949ae79a047..f6ffbfe2645 100644
--- a/lib/gitlab/query_limiting/middleware.rb
+++ b/lib/gitlab/query_limiting/middleware.rb
@@ -5,8 +5,8 @@ module Gitlab
# Middleware for reporting (or raising) when a request performs more than a
# certain amount of database queries.
class Middleware
- CONTROLLER_KEY = 'action_controller.instance'.freeze
- ENDPOINT_KEY = 'api.endpoint'.freeze
+ CONTROLLER_KEY = 'action_controller.instance'
+ ENDPOINT_KEY = 'api.endpoint'
def initialize(app)
@app = app
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index e5d99ebee35..5cf24823ef5 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
# frozen_string_literal: true
module Gitlab
@@ -6,8 +7,8 @@ module Gitlab
extend ActiveSupport::Concern
include Gitlab::QuickActions::Dsl
- SHRUG = '¯\\_(ツ)_/¯'.freeze
- TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze
+ SHRUG = '¯\\_(ツ)_/¯'
+ TABLEFLIP = '(╯°□°)╯︵ ┻━┻'
included do
# Issue, MergeRequest, Epic: quick actions definitions
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index 533c74ba9b4..183191f31a6 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -122,7 +122,7 @@ module Gitlab
params '#issue | !merge_request'
types Issue, MergeRequest
condition do
- current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+ current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index 6e8403ad878..6e31c506438 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -6,9 +6,9 @@ require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present?
module Gitlab
module Redis
class Cache < ::Gitlab::Redis::Wrapper
- CACHE_NAMESPACE = 'cache:gitlab'.freeze
- DEFAULT_REDIS_CACHE_URL = 'redis://localhost:6380'.freeze
- REDIS_CACHE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CACHE_CONFIG_FILE'.freeze
+ CACHE_NAMESPACE = 'cache:gitlab'
+ DEFAULT_REDIS_CACHE_URL = 'redis://localhost:6380'
+ REDIS_CACHE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CACHE_CONFIG_FILE'
class << self
def default_url
diff --git a/lib/gitlab/redis/queues.rb b/lib/gitlab/redis/queues.rb
index 8b42c269dd0..0375e4a221a 100644
--- a/lib/gitlab/redis/queues.rb
+++ b/lib/gitlab/redis/queues.rb
@@ -6,10 +6,10 @@ require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
module Gitlab
module Redis
class Queues < ::Gitlab::Redis::Wrapper
- SIDEKIQ_NAMESPACE = 'resque:gitlab'.freeze
- MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
- DEFAULT_REDIS_QUEUES_URL = 'redis://localhost:6381'.freeze
- REDIS_QUEUES_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_QUEUES_CONFIG_FILE'.freeze
+ SIDEKIQ_NAMESPACE = 'resque:gitlab'
+ MAILROOM_NAMESPACE = 'mail_room:gitlab'
+ DEFAULT_REDIS_QUEUES_URL = 'redis://localhost:6381'
+ REDIS_QUEUES_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_QUEUES_CONFIG_FILE'
class << self
def default_url
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index 270a19e780c..35356083f26 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -6,12 +6,12 @@ require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper)
module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
- SESSION_NAMESPACE = 'session:gitlab'.freeze
- USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze
- USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze
- IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab'.freeze
- DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
- REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
+ SESSION_NAMESPACE = 'session:gitlab'
+ USER_SESSIONS_NAMESPACE = 'session:user:gitlab'
+ USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'
+ IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab'
+ DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'
+ REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'
class << self
def default_url
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index 07a1e20b076..fa1615a5953 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -8,8 +8,8 @@ require 'active_support/core_ext/module/delegation'
module Gitlab
module Redis
class Wrapper
- DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
- REDIS_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CONFIG_FILE'.freeze
+ DEFAULT_REDIS_URL = 'redis://localhost:6379'
+ REDIS_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CONFIG_FILE'
class << self
delegate :params, :url, to: :new
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
index 033e451dbee..dd1482da40d 100644
--- a/lib/gitlab/request_profiler.rb
+++ b/lib/gitlab/request_profiler.rb
@@ -4,7 +4,7 @@ require 'fileutils'
module Gitlab
module RequestProfiler
- PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles".freeze
+ PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
def all
Dir["#{PROFILES_DIR}/*.{html,txt}"].map do |path|
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index ce4c1611687..93e172299b9 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,7 +2,8 @@
module Gitlab
class SearchResults
- COUNT_LIMIT = 1001
+ COUNT_LIMIT = 101
+ COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+"
attr_reader :current_user, :query, :per_page
@@ -60,7 +61,7 @@ module Gitlab
def formatted_limited_count(count)
if count >= COUNT_LIMIT
- "#{COUNT_LIMIT - 1}+"
+ COUNT_LIMIT_MESSAGE
else
count.to_s
end
diff --git a/lib/gitlab/shard_health_cache.rb b/lib/gitlab/shard_health_cache.rb
index 6612347438e..eeb0cc75ef9 100644
--- a/lib/gitlab/shard_health_cache.rb
+++ b/lib/gitlab/shard_health_cache.rb
@@ -2,7 +2,7 @@
module Gitlab
class ShardHealthCache
- HEALTHY_SHARDS_KEY = 'gitlab-healthy-shards'.freeze
+ HEALTHY_SHARDS_KEY = 'gitlab-healthy-shards'
HEALTHY_SHARDS_TIMEOUT = 300
# Clears the Redis set storing the list of healthy shards
diff --git a/lib/gitlab/sidekiq_monitor.rb b/lib/gitlab/sidekiq_monitor.rb
index 9842f1f53f7..a58b33534bf 100644
--- a/lib/gitlab/sidekiq_monitor.rb
+++ b/lib/gitlab/sidekiq_monitor.rb
@@ -4,7 +4,7 @@ module Gitlab
class SidekiqMonitor < Daemon
include ::Gitlab::Utils::StrongMemoize
- NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'.freeze
+ NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'
CANCEL_DEADLINE = 24.hours.seconds
RECONNECT_TIME = 3.seconds
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 0f890a12134..0dafccb3d34 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -18,7 +18,7 @@ module Gitlab
# expire after a certain period of time to prevent storing too many keys in
# Redis.
module SidekiqStatus
- STATUS_KEY = 'gitlab-sidekiq-status:%s'.freeze
+ STATUS_KEY = 'gitlab-sidekiq-status:%s'
# The default time (in seconds) after which a status key is expired
# automatically. The default of 30 minutes should be more than sufficient
diff --git a/lib/gitlab/url_helpers.rb b/lib/gitlab/url_helpers.rb
index 883585c196f..2a7919883c5 100644
--- a/lib/gitlab/url_helpers.rb
+++ b/lib/gitlab/url_helpers.rb
@@ -2,7 +2,7 @@
module Gitlab
class UrlHelpers
- WSS_PROTOCOL = "wss".freeze
+ WSS_PROTOCOL = "wss"
def self.as_wss(url)
return unless url.present?
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 139ec6e384a..db67e4fd479 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -7,13 +7,13 @@ require 'uri'
module Gitlab
class Workhorse
- SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'.freeze
- VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'.freeze
- INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
- INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
- NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
+ SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
+ VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
+ INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+ NOTIFICATION_CHANNEL = 'workhorse:notifications'
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
- DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'.freeze
+ DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'
include JwtAuthenticatable
diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb
new file mode 100644
index 00000000000..b4768a9546d
--- /dev/null
+++ b/lib/gitlab_danger.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class GitlabDanger
+ LOCAL_RULES ||= %w[
+ changes_size
+ gemfile
+ documentation
+ frozen_string
+ duplicate_yarn_dependencies
+ prettier
+ eslint
+ database
+ ].freeze
+
+ CI_ONLY_RULES ||= %w[
+ metadata
+ changelog
+ specs
+ commit_messages
+ roulette
+ single_codebase
+ gitlab_ui_wg
+ ce_ee_vue_templates
+ only_documentation
+ ].freeze
+
+ MESSAGE_PREFIX = '==>'.freeze
+
+ attr_reader :gitlab_danger_helper
+
+ def initialize(gitlab_danger_helper)
+ @gitlab_danger_helper = gitlab_danger_helper
+ end
+
+ def self.local_warning_message
+ "#{MESSAGE_PREFIX} Only the following Danger rules can be run locally: #{LOCAL_RULES.join(', ')}"
+ end
+
+ def self.success_message
+ "#{MESSAGE_PREFIX} No Danger rule violations!"
+ end
+
+ def rule_names
+ ci? ? LOCAL_RULES | CI_ONLY_RULES : LOCAL_RULES
+ end
+
+ def html_link(str)
+ self.ci? ? gitlab_danger_helper.html_link(str) : str
+ end
+
+ def ci?
+ !gitlab_danger_helper.nil?
+ end
+end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index b5f99ea012b..9f01a3f97ce 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -8,9 +8,9 @@ require 'google/apis/cloudresourcemanager_v1'
module GoogleApi
module CloudPlatform
class Client < GoogleApi::Auth
- SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
+ SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
LEAST_TOKEN_LIFE_TIME = 10.minutes
- CLUSTER_MASTER_AUTH_USERNAME = 'admin'.freeze
+ CLUSTER_MASTER_AUTH_USERNAME = 'admin'
class << self
def session_key_for_token
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index 60d79b52680..a65657dadd0 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -53,7 +53,7 @@ module Quality
end
def pattern(level)
- @patterns[level] ||= "#{prefix}spec/{#{TEST_LEVEL_FOLDERS.fetch(level).join(',')}}{,/**/}*_spec.rb".freeze
+ @patterns[level] ||= "#{prefix}spec/{#{TEST_LEVEL_FOLDERS.fetch(level).join(',')}}{,/**/}*_spec.rb"
end
def regexp(level)
diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb
index 569c41df6e4..7e2e168af8b 100644
--- a/lib/system_check/app/init_script_up_to_date_check.rb
+++ b/lib/system_check/app/init_script_up_to_date_check.rb
@@ -3,7 +3,7 @@
module SystemCheck
module App
class InitScriptUpToDateCheck < SystemCheck::BaseCheck
- SCRIPT_PATH = '/etc/init.d/gitlab'.freeze
+ SCRIPT_PATH = '/etc/init.d/gitlab'
set_name 'Init script up-to-date?'
set_skip_reason 'skipped (omnibus-gitlab has no init script)'
diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb
index 890f8b44d13..da695cc9272 100644
--- a/lib/system_check/app/redis_version_check.rb
+++ b/lib/system_check/app/redis_version_check.rb
@@ -3,7 +3,7 @@
module SystemCheck
module App
class RedisVersionCheck < SystemCheck::BaseCheck
- MIN_REDIS_VERSION = '2.8.0'.freeze
+ MIN_REDIS_VERSION = '2.8.0'
set_name "Redis version >= #{MIN_REDIS_VERSION}?"
def check?
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index c53d55ceea2..fd8df015903 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -3,8 +3,8 @@
return if Rails.env.production?
namespace :gitlab do
- OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference").freeze
- TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'.freeze
+ OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference")
+ TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'
namespace :graphql do
desc 'GitLab | Generate GraphQL docs'
diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake
index 18fb8afe455..2eeb694d341 100644
--- a/lib/tasks/gitlab/uploads/legacy.rake
+++ b/lib/tasks/gitlab/uploads/legacy.rake
@@ -11,7 +11,7 @@ namespace :gitlab do
include ::EachBatch
end
- migration = 'LegacyUploadsMigrator'.freeze
+ migration = 'LegacyUploadsMigrator'
batch_size = 5000
delay_interval = 5.minutes.to_i
diff --git a/lib/tasks/gitlab_danger.rake b/lib/tasks/gitlab_danger.rake
new file mode 100644
index 00000000000..e75539f048c
--- /dev/null
+++ b/lib/tasks/gitlab_danger.rake
@@ -0,0 +1,17 @@
+desc 'Run local Danger rules'
+task :danger_local do
+ require 'gitlab_danger'
+ require 'gitlab/popen'
+
+ puts("#{GitlabDanger.local_warning_message}\n")
+
+ # _status will _always_ be 0, regardless of failure or success :(
+ output, _status = Gitlab::Popen.popen(%w{danger dry_run})
+
+ if output.empty?
+ puts(GitlabDanger.success_message)
+ else
+ puts(output)
+ exit(1)
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e15000b5184..32deab7dd68 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -810,6 +810,9 @@ msgstr ""
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr ""
+msgid "AdminDashboard|Error loading the statistics. Please try again"
+msgstr ""
+
msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered."
msgstr ""
@@ -837,6 +840,30 @@ msgstr ""
msgid "AdminSettings|When creating a new environment variable it will be protected by default."
msgstr ""
+msgid "AdminStatistics|Active Users"
+msgstr ""
+
+msgid "AdminStatistics|Forks"
+msgstr ""
+
+msgid "AdminStatistics|Issues"
+msgstr ""
+
+msgid "AdminStatistics|Merge Requests"
+msgstr ""
+
+msgid "AdminStatistics|Milestones"
+msgstr ""
+
+msgid "AdminStatistics|Notes"
+msgstr ""
+
+msgid "AdminStatistics|SSH Keys"
+msgstr ""
+
+msgid "AdminStatistics|Snippets"
+msgstr ""
+
msgid "AdminUsers|2FA Disabled"
msgstr ""
@@ -2035,10 +2062,10 @@ msgstr ""
msgid "Certificate (PEM)"
msgstr ""
-msgid "Change Label"
+msgid "Change assignee"
msgstr ""
-msgid "Change assignee"
+msgid "Change label"
msgstr ""
msgid "Change milestone"
@@ -2503,6 +2530,9 @@ msgstr ""
msgid "ClusterIntegration|Alternatively"
msgstr ""
+msgid "ClusterIntegration|Amazon EKS"
+msgstr ""
+
msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later."
msgstr ""
@@ -2581,6 +2611,9 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Create cluster on"
+msgstr ""
+
msgid "ClusterIntegration|Did you know?"
msgstr ""
@@ -2632,6 +2665,9 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
+msgid "ClusterIntegration|Google GKE"
+msgstr ""
+
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
@@ -2962,9 +2998,6 @@ msgstr ""
msgid "ClusterIntegration|pricing"
msgstr ""
-msgid "ClusterIntegration|properly configured"
-msgstr ""
-
msgid "ClusterIntegration|sign up"
msgstr ""
@@ -3019,6 +3052,9 @@ msgstr ""
msgid "Comment is being updated"
msgstr ""
+msgid "Comment/Reply (quoting selected text)"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -4189,9 +4225,15 @@ msgstr ""
msgid "Edit comment"
msgstr ""
+msgid "Edit description"
+msgstr ""
+
msgid "Edit environment"
msgstr ""
+msgid "Edit epic description"
+msgstr ""
+
msgid "Edit file"
msgstr ""
@@ -4204,25 +4246,22 @@ msgstr ""
msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Edit issue"
-msgstr ""
-
msgid "Edit issues"
msgstr ""
-msgid "Edit last comment (when focused on an empty textarea)"
+msgid "Edit public deploy key"
msgstr ""
-msgid "Edit merge request"
+msgid "Edit stage"
msgstr ""
-msgid "Edit public deploy key"
+msgid "Edit wiki page"
msgstr ""
-msgid "Edit stage"
+msgid "Edit your most recent comment in a thread (from an empty textarea)"
msgstr ""
-msgid "Edit wiki page"
+msgid "Editing"
msgstr ""
msgid "Email"
@@ -4543,6 +4582,9 @@ msgstr ""
msgid "Epic"
msgstr ""
+msgid "Epics (Ultimate / Gold license only)"
+msgstr ""
+
msgid "Error"
msgstr ""
@@ -5100,9 +5142,6 @@ msgstr ""
msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file."
msgstr ""
-msgid "Finding Project File"
-msgstr ""
-
msgid "Fingerprint"
msgstr ""
@@ -5127,12 +5166,6 @@ msgstr ""
msgid "FlowdockService|Flowdock is a collaboration web app for technical teams."
msgstr ""
-msgid "Focus Filter"
-msgstr ""
-
-msgid "Focus Search"
-msgstr ""
-
msgid "FogBugz Email"
msgstr ""
@@ -5376,6 +5409,9 @@ msgstr ""
msgid "Go back"
msgstr ""
+msgid "Go back (while searching for files"
+msgstr ""
+
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
msgstr ""
@@ -5397,16 +5433,17 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file permalink"
+msgid "Go to file (MRs only)"
msgstr ""
-msgid "Go to files"
+
+msgid "Go to file permalink (while viewing a file)"
msgstr ""
-msgid "Go to finding file"
+msgid "Go to files"
msgstr ""
-msgid "Go to groups"
+msgid "Go to find file"
msgstr ""
msgid "Go to issue boards"
@@ -5427,52 +5464,67 @@ msgstr ""
msgid "Go to metrics"
msgstr ""
-msgid "Go to milestones"
-msgstr ""
-
-msgid "Go to network graph"
-msgstr ""
-
msgid "Go to parent"
msgstr ""
msgid "Go to project"
msgstr ""
-msgid "Go to projects"
+msgid "Go to releases"
msgstr ""
msgid "Go to repository charts"
msgstr ""
+msgid "Go to repository graph"
+msgstr ""
+
msgid "Go to snippets"
msgstr ""
msgid "Go to the activity feed"
msgstr ""
+msgid "Go to the milestone list"
+msgstr ""
+
msgid "Go to the project's activity feed"
msgstr ""
msgid "Go to the project's overview page"
msgstr ""
-msgid "Go to todos"
+msgid "Go to wiki"
msgstr ""
-msgid "Go to wiki"
+msgid "Go to your To-Do list"
msgstr ""
msgid "Go to your fork"
msgstr ""
+msgid "Go to your groups"
+msgstr ""
+
+msgid "Go to your issues"
+msgstr ""
+
+msgid "Go to your merge requests"
+msgstr ""
+
+msgid "Go to your projects"
+msgstr ""
+
+msgid "Go to your snippets"
+msgstr ""
+
msgid "Google Code import"
msgstr ""
msgid "Google Takeout"
msgstr ""
-msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgid "Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service."
msgstr ""
msgid "Got it"
@@ -6233,6 +6285,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Issues / Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -7209,7 +7264,7 @@ msgstr ""
msgid "Monday"
msgstr ""
-msgid "Monitor your errors by integrating with Sentry"
+msgid "Monitor your errors by integrating with Sentry."
msgstr ""
msgid "Monitoring"
@@ -7254,18 +7309,6 @@ msgstr ""
msgid "Move this issue to another project."
msgstr ""
-msgid "Move to next file"
-msgstr ""
-
-msgid "Move to next unresolved discussion"
-msgstr ""
-
-msgid "Move to previous file"
-msgstr ""
-
-msgid "Move to previous unresolved discussion"
-msgstr ""
-
msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
msgstr ""
@@ -7326,9 +7369,6 @@ msgstr ""
msgid "Network"
msgstr ""
-msgid "Network Graph"
-msgstr ""
-
msgid "Never"
msgstr ""
@@ -7451,6 +7491,12 @@ msgstr ""
msgid "Next"
msgstr ""
+msgid "Next file in diff (MRs only)"
+msgstr ""
+
+msgid "Next unresolved discussion (MRs only)"
+msgstr ""
+
msgid "Nickname"
msgstr ""
@@ -8446,6 +8492,12 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
+msgid "Previous file in diff (MRs only)"
+msgstr ""
+
+msgid "Previous unresolved discussion (MRs only)"
+msgstr ""
+
msgid "Prioritize"
msgstr ""
@@ -8824,10 +8876,7 @@ msgstr ""
msgid "Project Badges"
msgstr ""
-msgid "Project File"
-msgstr ""
-
-msgid "Project Files browsing"
+msgid "Project Files"
msgstr ""
msgid "Project ID"
@@ -9621,9 +9670,6 @@ msgstr ""
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
-msgid "Reply (quoting selected text)"
-msgstr ""
-
msgid "Reply by email"
msgstr ""
@@ -9672,6 +9718,9 @@ msgstr ""
msgid "Repository"
msgstr ""
+msgid "Repository Graph"
+msgstr ""
+
msgid "Repository Settings"
msgstr ""
@@ -10499,9 +10548,6 @@ msgstr ""
msgid "Show whitespace changes"
msgstr ""
-msgid "Show/hide this dialog"
-msgstr ""
-
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
@@ -10525,6 +10571,9 @@ msgstr ""
msgid "Sign in via 2FA code"
msgstr ""
+msgid "Sign in with Google"
+msgstr ""
+
msgid "Sign out"
msgstr ""
@@ -10936,6 +10985,9 @@ msgstr ""
msgid "Start date"
msgstr ""
+msgid "Start search"
+msgstr ""
+
msgid "Start the Runner!"
msgstr ""
@@ -10966,6 +11018,9 @@ msgstr ""
msgid "State your message to activate"
msgstr ""
+msgid "Statistics"
+msgstr ""
+
msgid "Status"
msgstr ""
@@ -12253,6 +12308,9 @@ msgstr ""
msgid "Toggle backtrace"
msgstr ""
+msgid "Toggle collapse"
+msgstr ""
+
msgid "Toggle comments for this file"
msgstr ""
@@ -12274,6 +12332,9 @@ msgstr ""
msgid "Toggle the Performance Bar"
msgstr ""
+msgid "Toggle this dialog"
+msgstr ""
+
msgid "Toggle thread"
msgstr ""
@@ -13023,6 +13084,9 @@ msgid_plural "When these merge requests are accepted"
msgstr[0] ""
msgstr[1] ""
+msgid "When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed."
+msgstr ""
+
msgid "When:"
msgstr ""
@@ -14128,7 +14192,7 @@ msgstr ""
msgid "nounSeries|%{item}, and %{lastItem}"
msgstr ""
-msgid "or"
+msgid "or %{link_start}create a new Google account%{link_end}"
msgstr ""
msgid "out of %d total test"
diff --git a/package.json b/package.json
index 23e611ae6cc..4256b8bfdcc 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,8 @@
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.5.5",
- "@gitlab/svgs": "^1.71.0",
- "@gitlab/ui": "5.21.0",
+ "@gitlab/svgs": "^1.72.0",
+ "@gitlab/ui": "5.21.1",
"@gitlab/visual-review-tools": "1.0.1",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
@@ -197,6 +197,7 @@
"stylelint": "^10.1.0",
"stylelint-config-recommended": "^2.2.0",
"stylelint-scss": "^3.9.2",
+ "timezone-mock": "^1.0.8",
"vue-jest": "^4.0.0-beta.2",
"webpack-dev-server": "^3.1.14",
"yarn-deduplicate": "^1.1.1"
diff --git a/plugins/.gitignore b/plugins/.gitignore
new file mode 100644
index 00000000000..e4ccdc9e2ec
--- /dev/null
+++ b/plugins/.gitignore
@@ -0,0 +1,5 @@
+*
+!*/
+!.gitignore
+!.gitkeep
+!examples/*
diff --git a/qa/README.md b/qa/README.md
index dede3cd2473..332e5c8170f 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -36,7 +36,7 @@ using `package-and-qa-manual` manual action, to test if everything works fine.
You can use GitLab QA to exercise tests on any live instance! If you don't
have an instance available you can follow the instructions below to use
-the [GitLab Development Kit (GDK)][GDK].
+the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit).
This is the recommended option if you would like to contribute to the tests.
Note: GitLab QA uses [Selenium WebDriver](https://www.seleniumhq.org/) via
@@ -146,8 +146,6 @@ directory** (one level up from this directory):
docker build -t gitlab/gitlab-ce-qa:nightly --file ./qa/Dockerfile ./
```
-[GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/
-
### Quarantined tests
Tests can be put in quarantine by assigning `:quarantine` metadata. This means
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index e3039149ab4..4676dc8d077 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -61,6 +61,10 @@ module QA
end
end
+ def sign_out_if_signed_in
+ sign_out if has_personal_area?(wait: 0)
+ end
+
def click_settings_link
retry_until(reload: false) do
within_user_menu do
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index 67459208c8d..f877ba76b38 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -64,3 +64,5 @@ module QA
end
end
end
+
+QA::Page::MergeRequest::New.prepend_if_ee('QA::EE::Page::MergeRequest::New')
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 52929ece9ed..e5e26b1864b 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -35,12 +35,17 @@ module QA
element :labels_block
element :edit_link_labels
element :dropdown_menu_labels
+ element :milestone_link
end
view 'app/views/shared/issuable/_close_reopen_button.html.haml' do
element :reopen_issue_button
end
+ def click_milestone_link
+ click_element(:milestone_link)
+ end
+
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment: nil, filter: :all_activities)
diff --git a/qa/qa/page/project/milestone/index.rb b/qa/qa/page/project/milestone/index.rb
index 6895c44f72f..8ad7689ce70 100644
--- a/qa/qa/page/project/milestone/index.rb
+++ b/qa/qa/page/project/milestone/index.rb
@@ -17,3 +17,5 @@ module QA
end
end
end
+
+QA::Page::Project::Milestone::Index.prepend_if_ee('QA::EE::Page::Project::Milestone::Index')
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index 44d9dc8f296..b5beba64c61 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -10,6 +10,7 @@ module QA
end
attribute :id
+ attribute :name
def initialize
@path = Runtime::Namespace.name
@@ -47,6 +48,11 @@ module QA
super
end
+ def add_member(user, access_level = '30')
+ # 30 = developer access
+ post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
+ end
+
def api_get_path
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index 16ab59352f3..8539eaeb337 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -3,7 +3,7 @@
module QA
module Resource
class Issue < Base
- attr_writer :description, :milestone
+ attr_writer :description, :milestone, :weight
attribute :project do
Project.fabricate! do |resource|
@@ -46,6 +46,7 @@ module QA
title: title
}.tap do |hash|
hash[:milestone_id] = @milestone.id if @milestone
+ hash[:weight] = @weight if @weight
end
end
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 45ab2396a04..53126c67ba3 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -5,7 +5,8 @@ require 'securerandom'
module QA
module Resource
class MergeRequest < Base
- attr_accessor :id,
+ attr_accessor :approval_rules,
+ :id,
:title,
:description,
:source_branch,
@@ -46,6 +47,7 @@ module QA
end
def initialize
+ @approval_rules = nil
@title = 'QA test - merge request'
@description = 'This is a test merge request'
@source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
@@ -63,16 +65,17 @@ module QA
project.visit!
Page::Project::Show.perform(&:new_merge_request)
- Page::MergeRequest::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.choose_milestone(@milestone) if @milestone
- page.assign_to_me if @assignee == 'me'
+ Page::MergeRequest::New.perform do |new|
+ new.fill_title(@title)
+ new.fill_description(@description)
+ new.choose_milestone(@milestone) if @milestone
+ new.assign_to_me if @assignee == 'me'
labels.each do |label|
- page.select_label(label)
+ new.select_label(label)
end
+ new.add_approval_rules(approval_rules) if approval_rules
- page.create_merge_request
+ new.create_merge_request
end
end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 4a29a14c5c2..157064dfe37 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -75,6 +75,11 @@ module QA
super
end
+ def add_member(user, access_level = '30')
+ # 30 = developer access
+ post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
+ end
+
def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}"
end
@@ -83,6 +88,10 @@ module QA
"#{api_get_path}/repository/archive.#{type}"
end
+ def api_members_path
+ "#{api_get_path}/members"
+ end
+
def api_post_path
'/projects'
end
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
index eec46f46d99..911d2b2f506 100644
--- a/qa/qa/resource/user.rb
+++ b/qa/qa/resource/user.rb
@@ -9,6 +9,7 @@ module QA
attr_writer :username, :password
attr_accessor :provider, :extern_uid
+ attribute :id
attribute :name
attribute :email
diff --git a/shared/.gitignore b/shared/.gitignore
new file mode 100644
index 00000000000..5c0276adafd
--- /dev/null
+++ b/shared/.gitignore
@@ -0,0 +1,4 @@
+*
+!*/
+!.gitignore
+!.gitkeep
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index e5501535875..afc059d7561 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -73,8 +73,8 @@ describe Admin::ClustersController do
end
describe 'GET #new' do
- def get_new
- get :new
+ def get_new(provider: 'gke')
+ get :new, params: { provider: provider }
end
describe 'functionality for new cluster' do
@@ -85,6 +85,7 @@ describe Admin::ClustersController do
end
before do
+ stub_feature_flags(create_eks_clusters: false)
allow(SecureRandom).to receive(:hex).and_return(key)
end
@@ -94,6 +95,20 @@ describe Admin::ClustersController do
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path)
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when selected provider is gke and no valid gcp token exists' do
+ it 'redirects to gcp authorize_url' do
+ get_new
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
end
context 'when omniauth has not configured' do
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 09677b42887..5a3ba51d4df 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -85,8 +85,8 @@ describe Groups::ClustersController do
end
describe 'GET new' do
- def go
- get :new, params: { group_id: group }
+ def go(provider: 'gke')
+ get :new, params: { group_id: group, provider: provider }
end
describe 'functionality for new cluster' do
@@ -97,6 +97,7 @@ describe Groups::ClustersController do
end
before do
+ stub_feature_flags(create_eks_clusters: false)
allow(SecureRandom).to receive(:hex).and_return(key)
end
@@ -106,6 +107,20 @@ describe Groups::ClustersController do
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group))
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when selected provider is gke and no valid gcp token exists' do
+ it 'redirects to gcp authorize_url' do
+ go
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
end
context 'when omniauth has not configured' do
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 35cbab57037..8ac72df5d20 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -79,8 +79,12 @@ describe Projects::ClustersController do
end
describe 'GET new' do
- def go
- get :new, params: { namespace_id: project.namespace, project_id: project }
+ def go(provider: 'gke')
+ get :new, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ provider: provider
+ }
end
describe 'functionality for new cluster' do
@@ -91,6 +95,7 @@ describe Projects::ClustersController do
end
before do
+ stub_feature_flags(create_eks_clusters: false)
allow(SecureRandom).to receive(:hex).and_return(key)
end
@@ -100,6 +105,20 @@ describe Projects::ClustersController do
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_project_cluster_path(project))
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when selected provider is gke and no valid gcp token exists' do
+ it 'redirects to gcp authorize_url' do
+ go
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
end
context 'when omniauth has not configured' do
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 180d997a8e8..0c074714bf3 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -19,9 +19,9 @@ describe Projects::ServicesController do
it 'renders 404' do
allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param }
+ put :test, params: project_params
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -29,11 +29,11 @@ describe Projects::ServicesController do
let(:service_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(json_response['message']).to eq "Validations failed."
+ expect(json_response['message']).to eq 'Validations failed.'
expect(json_response['service_response']).to include "Url can't be blank"
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
end
end
@@ -47,9 +47,9 @@ describe Projects::ServicesController do
it 'returns success' do
allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param }
+ put :test, params: project_params
- expect(response.status).to eq(200)
+ expect(response).to be_successful
end
end
@@ -57,11 +57,11 @@ describe Projects::ServicesController do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 200, body: '{}')
- expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original
+ expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(response.status).to eq(200)
+ expect(response).to be_successful
end
end
@@ -69,14 +69,23 @@ describe Projects::ServicesController do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 200, body: '{}')
- expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original
+ expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(response.status).to eq(200)
+ expect(response).to be_successful
end
context 'when service is configured for the first time' do
+ let(:service_params) do
+ {
+ 'active' => '1',
+ 'push_events' => '1',
+ 'token' => 'token',
+ 'project_url' => 'http://test.com'
+ }
+ end
+
before do
allow_any_instance_of(ServiceHook).to receive(:execute).and_return(true)
end
@@ -84,7 +93,7 @@ describe Projects::ServicesController do
it 'persist the object' do
do_put
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
expect(json_response).to be_empty
expect(BuildkiteService.first).to be_present
end
@@ -92,18 +101,14 @@ describe Projects::ServicesController do
it 'creates the ServiceHook object' do
do_put
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
expect(json_response).to be_empty
expect(BuildkiteService.first.service_hook).to be_present
end
def do_put
- put :test, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: 'buildkite',
- service: { 'active' => '1', 'push_events' => '1', token: 'token', 'project_url' => 'http://test.com' }
- }
+ put :test, params: project_params(id: 'buildkite',
+ service: service_params)
end
end
end
@@ -113,9 +118,9 @@ describe Projects::ServicesController do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404)
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Test failed.',
@@ -127,39 +132,70 @@ describe Projects::ServicesController do
end
describe 'PUT #update' do
- context 'when param `active` is set to true' do
- it 'activates the service and redirects to integrations paths' do
- put :update,
- params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true } }
+ describe 'as HTML' do
+ let(:service_params) { { active: true } }
- expect(response).to redirect_to(project_settings_integrations_path(project))
- expect(flash[:notice]).to eq 'Jira activated.'
+ before do
+ put :update, params: project_params(service: service_params)
+ end
+
+ context 'when param `active` is set to true' do
+ it 'activates the service and redirects to integrations paths' do
+ expect(response).to redirect_to(project_settings_integrations_path(project))
+ expect(flash[:notice]).to eq 'Jira activated.'
+ end
+ end
+
+ context 'when param `active` is set to false' do
+ let(:service_params) { { active: false } }
+
+ it 'does not activate the service but saves the settings' do
+ expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
+ end
end
- end
- context 'when param `active` is set to false' do
- it 'does not activate the service but saves the settings' do
- put :update,
- params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: false } }
+ context 'when activating Jira service from a template' do
+ let(:service) do
+ create(:jira_service, project: project, template: true)
+ end
- expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
+ it 'activate Jira service from template' do
+ expect(flash[:notice]).to eq 'Jira activated.'
+ end
end
end
- context 'when activating Jira service from a template' do
- let(:template_service) { create(:jira_service, project: project, template: true) }
+ describe 'as JSON' do
+ before do
+ put :update, params: project_params(service: service_params, format: :json)
+ end
+
+ context 'when update succeeds' do
+ let(:service_params) { { url: 'http://example.com' } }
+
+ it 'returns JSON response with no errors' do
+ expect(response).to be_successful
+ expect(json_response).to include('active' => true, 'errors' => {})
+ end
+ end
- it 'activate Jira service from template' do
- put :update, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true } }
+ context 'when update fails' do
+ let(:service_params) { { url: '' } }
- expect(flash[:notice]).to eq 'Jira activated.'
+ it 'returns JSON response with errors' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ 'active' => true,
+ 'errors' => { 'url' => ['must be a valid URL', %{can't be blank}] }
+ )
+ end
end
end
end
- describe "GET #edit" do
+ describe 'GET #edit' do
before do
- get :edit, params: { namespace_id: project.namespace, project_id: project, id: 'jira' }
+ get :edit, params: project_params(id: 'jira')
end
context 'with approved services' do
@@ -168,4 +204,14 @@ describe Projects::ServicesController do
end
end
end
+
+ private
+
+ def project_params(opts = {})
+ opts.reverse_merge(
+ namespace_id: project.namespace,
+ project_id: project,
+ id: service.to_param
+ )
+ end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 35487682462..5d87dbdee8b 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -74,17 +74,19 @@ describe RegistrationsController do
end
context 'when reCAPTCHA is enabled' do
- def fail_recaptcha
- # Without this, `verify_recaptcha` arbitrarily returns true in test env
- Recaptcha.configuration.skip_verify_env.delete('test')
- end
-
before do
stub_application_setting(recaptcha_enabled: true)
end
+ after do
+ # Avoid test ordering issue and ensure `verify_recaptcha` returns true
+ unless Recaptcha.configuration.skip_verify_env.include?('test')
+ Recaptcha.configuration.skip_verify_env << 'test'
+ end
+ end
+
it 'displays an error when the reCAPTCHA is not solved' do
- fail_recaptcha
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
post(:create, params: user_params)
@@ -93,11 +95,6 @@ describe RegistrationsController do
end
it 'redirects to the dashboard when the recaptcha is solved' do
- # Avoid test ordering issue and ensure `verify_recaptcha` returns true
- unless Recaptcha.configuration.skip_verify_env.include?('test')
- Recaptcha.configuration.skip_verify_env << 'test'
- end
-
post(:create, params: user_params)
expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
@@ -105,7 +102,6 @@ describe RegistrationsController do
it 'does not require reCAPTCHA if disabled by feature flag' do
stub_feature_flags(registrations_recaptcha: false)
- fail_recaptcha
post(:create, params: user_params)
diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/external_pull_requests.rb
new file mode 100644
index 00000000000..08d0fa4d419
--- /dev/null
+++ b/spec/factories/external_pull_requests.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :external_pull_request do
+ sequence(:pull_request_iid)
+ project
+ source_branch 'feature'
+ source_repository 'the-repository'
+ source_sha '97de212e80737a608d939f648d959671fb0a0142'
+ target_branch 'master'
+ target_repository 'the-repository'
+ target_sha 'a09386439ca39abe575675ffd4b89ae824fec22f'
+ status :open
+
+ trait(:closed) { status 'closed' }
+ end
+end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index ee5be82cd19..ae3988bdd69 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -271,5 +271,88 @@ ZDXgrA==
auto_ssl_enabled { true }
certificate_source { :gitlab_provided }
end
+
+ trait :explicit_ecdsa do
+ certificate '-----BEGIN CERTIFICATE-----
+MIID1zCCAzkCCQDatOIwBlktwjAKBggqhkjOPQQDAjBPMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCTlkxCzAJBgNVBAcMAk5ZMQswCQYDVQQLDAJJVDEZMBcGA1UEAwwQ
+dGVzdC1jZXJ0aWZpY2F0ZTAeFw0xOTA4MjkxMTE1NDBaFw0yMTA4MjgxMTE1NDBa
+ME8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTELMAkGA1UEBwwCTlkxCzAJBgNV
+BAsMAklUMRkwFwYDVQQDDBB0ZXN0LWNlcnRpZmljYXRlMIICXDCCAc8GByqGSM49
+AgEwggHCAgEBME0GByqGSM49AQECQgH/////////////////////////////////
+/////////////////////////////////////////////////////zCBngRCAf//
+////////////////////////////////////////////////////////////////
+///////////////////8BEFRlT65YY4cmh+SmiGgtoVA7qLacluZsxXzuLSJkY7x
+CeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/AAMVANCeiAApHLhTlsxn
+FzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0QpxkgTkFP7Uh+CivYGtNPbqh
+S1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEYOSlqeJo7wARcil+0LH0b
+2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdhNTxwhqJywkCIvpR2n9Fm
+UAJCAf//////////////////////////////////////////+lGGh4O/L5Zrf8wB
+SPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBVG/4c/hgl36toHj+eGL4pqv7
+l7M+ZKQJ4vz0Y9E6xIx+gvfVaZ58krmbBAP53ikwneQbFdcvw3L/ACPEib/qWjkB
+ogykguy3OwHtKLYNnDWIsfiLumEjElhcBMZVXiXhb5txf11uXAWn5n6Qhey5YKPM
+NjLLqDqaG19efCLCd21A0TcwCgYIKoZIzj0EAwIDgYsAMIGHAkEm68kYFVnN1c2N
+OjSJpIDdFWGVYJHyMDI5WgQyhm4hAioXJ0T22Zab8Wmq+hBYRJNcHoaV894blfqR
+V3ZJgam8EQJCAcnPpJQ0IqoT1pAQkaL3+Ka8ZaaCd6/8RnoDtGvWljisuyH65SRu
+kmYv87bZe1KqOZDoaDBdfVsoxcGbik19lBPV
+-----END CERTIFICATE-----'
+
+ key '-----BEGIN EC PARAMETERS-----
+MIIBwgIBATBNBgcqhkjOPQEBAkIB////////////////////////////////////
+//////////////////////////////////////////////////8wgZ4EQgH/////
+////////////////////////////////////////////////////////////////
+/////////////////ARBUZU+uWGOHJofkpohoLaFQO6i2nJbmbMV87i0iZGO8Qnh
+Vhk5Uex+k3sWUsC9O7G/BzVz34g9LDTx70Uf1GtQPwADFQDQnogAKRy4U5bMZxc5
+MoSqoNpkugSBhQQAxoWOBrcEBOnNnj7LZiOVtEKcZIE5BT+1Ifgor2BrTT26oUte
+d+/nWSj+HcEnov+o3jNIs8GFakKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY
+9URJV5tEaBevvRcnPmYsl+5ymV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlAC
+QgH///////////////////////////////////////////pRhoeDvy+Wa3/MAUj3
+CaXQO7XJuImcR667b7cekThkCQIBAQ==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MIICnQIBAQRCAZZRG4FJO+OK29ygycrNzjxQDB+dp+QPo1Pk6RAl5PcraohyhFnI
+MGUL4ba1efZUxCbAWxjVRSi7QEUNYCCdUPAtoIIBxjCCAcICAQEwTQYHKoZIzj0B
+AQJCAf//////////////////////////////////////////////////////////
+////////////////////////////MIGeBEIB////////////////////////////
+//////////////////////////////////////////////////////////wEQVGV
+PrlhjhyaH5KaIaC2hUDuotpyW5mzFfO4tImRjvEJ4VYZOVHsfpN7FlLAvTuxvwc1
+c9+IPSw08e9FH9RrUD8AAxUA0J6IACkcuFOWzGcXOTKEqqDaZLoEgYUEAMaFjga3
+BATpzZ4+y2YjlbRCnGSBOQU/tSH4KK9ga009uqFLXnfv51ko/h3BJ6L/qN4zSLPB
+hWpCm/l+fjHC5b1mARg5KWp4mjvABFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfu
+cple9CZAxVC5AT+tB2E1PHCGonLCQIi+lHaf0WZQAkIB////////////////////
+///////////////////////6UYaHg78vlmt/zAFI9wml0Du1ybiJnEeuu2+3HpE4
+ZAkCAQGhgYkDgYYABAFUb/hz+GCXfq2geP54Yvimq/uXsz5kpAni/PRj0TrEjH6C
+99VpnnySuZsEA/neKTCd5BsV1y/Dcv8AI8SJv+paOQGiDKSC7Lc7Ae0otg2cNYix
++Iu6YSMSWFwExlVeJeFvm3F/XW5cBafmfpCF7Llgo8w2MsuoOpobX158IsJ3bUDR
+Nw==
+-----END EC PRIVATE KEY-----'
+ end
+
+ trait :ecdsa do
+ certificate '-----BEGIN CERTIFICATE-----
+MIIB8zCCAVUCCQCGKuPQ6SBxUTAKBggqhkjOPQQDAjA+MQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw
+HhcNMTkwOTAyMDkyMDUxWhcNMjEwOTAxMDkyMDUxWjA+MQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw
+gZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAH9Jd7ZWnTasgltZRbIMreihycOh/G4
+TXpkp8tTtEsuD+sh8au3Jywsi89RSZ6vgVoCY7//DQ2vamYnyBZqbL+cTQBsQ7wD
+UEaSyP0R3P4b6Ox347pYzXwSdSOra9Cm4TMQe+prVMesxulqIm7G7CTI+9J8LHlJ
+z0wUDQz/o+tUSYwv6zAKBggqhkjOPQQDAgOBiwAwgYcCQUOlTnn2QP/uYSh1dUSl
+R9WYUg5+PQMg7kS+4K/5+5gonWCvaMcP+2P7hltUcvq41l3uMKKCZRU/x60/FMHc
+1ZXdAkIBuVtm9RJXziNOKS4TcpH9os/FuREW8YQlpec58LDZdlivcHnikHZ4LCri
+T7zu3VY6Rq+V/IKpsQwQjmoTJ0IpCM8=
+-----END CERTIFICATE-----'
+
+ key '-----BEGIN EC PARAMETERS-----
+BgUrgQQAIw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MIHbAgEBBEFa72+eREW25IHbke0TiWFdW1R1ad9Nyqaz7CDtv5Kqdgd6Kcl8V2az
+Lr6z1PS+JSERWzRP+fps7kdFRrtqy/ECpKAHBgUrgQQAI6GBiQOBhgAEAf0l3tla
+dNqyCW1lFsgyt6KHJw6H8bhNemSny1O0Sy4P6yHxq7cnLCyLz1FJnq+BWgJjv/8N
+Da9qZifIFmpsv5xNAGxDvANQRpLI/RHc/hvo7HfjuljNfBJ1I6tr0KbhMxB76mtU
+x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r
+-----END EC PRIVATE KEY-----'
+ end
end
end
diff --git a/spec/features/admin/clusters/applications_spec.rb b/spec/features/admin/clusters/applications_spec.rb
new file mode 100644
index 00000000000..8310811b43d
--- /dev/null
+++ b/spec/features/admin/clusters/applications_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
+
+describe 'Instance-level Cluster Applications', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:user) { create(:admin) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'Installing applications' do
+ include_examples "installing applications on a cluster" do
+ let(:cluster_path) { admin_cluster_path(cluster) }
+ let(:cluster_factory_args) { [:instance] }
+ end
+ end
+end
diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb
index e204e0a515d..6cb345c5066 100644
--- a/spec/features/admin/dashboard_spec.rb
+++ b/spec/features/admin/dashboard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'admin visits dashboard' do
+describe 'admin visits dashboard', :js do
include ProjectForksHelper
before do
diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb
new file mode 100644
index 00000000000..cb8fd8c607c
--- /dev/null
+++ b/spec/features/clusters/installing_applications_shared_examples.rb
@@ -0,0 +1,228 @@
+# frozen_string_literal: true
+
+shared_examples "installing applications on a cluster" do
+ before do
+ visit cluster_path
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, *cluster_factory_args) }
+
+ it 'user is unable to install applications' do
+ expect(page).not_to have_text('Helm')
+ expect(page).not_to have_text('Install')
+ end
+ end
+
+ context 'when cluster is created' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) }
+
+ it 'user can install applications' do
+ wait_for_requests
+
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
+ end
+ end
+
+ context 'when user installs Helm' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+
+ wait_for_requests
+ end
+
+ it 'shows the status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
+ expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
+ expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
+ end
+ end
+
+ context 'when user installs Knative' do
+ before do
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+ end
+
+ context 'on an abac cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, *cluster_factory_args) }
+
+ it 'shows info block and not be installable' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).to have_css('.rbac-notice')
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ end
+ end
+ end
+
+ context 'on an rbac cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) }
+
+ it 'does not show callout block and be installable' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).not_to have_css('p', text: 'You must have an RBAC-enabled cluster', visible: :all)
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+ end
+ end
+
+ describe 'when user clicks install button' do
+ def domainname_form_value
+ page.find('.js-knative-domainname').value
+ end
+
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+
+ page.find('.js-knative-domainname').set("domain.example.org")
+
+ click_button 'Install'
+
+ wait_for_requests
+
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_knative.make_installing!
+ Clusters::Cluster.last.application_knative.make_installed!
+ Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1')
+ end
+ end
+
+ it 'shows status transition' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(domainname_form_value).to eq('domain.example.org')
+ expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster')
+ expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes'
+ end
+
+ it 'can then update the domain' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(ClusterPatchAppWorker).to receive(:perform_async)
+
+ expect(domainname_form_value).to eq('domain.example.org')
+
+ page.find('.js-knative-domainname').set("new.domain.example.org")
+
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(domainname_form_value).to eq('new.domain.example.org')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user installs Cert Manager' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ click_button 'Install'
+ end
+ end
+
+ it 'shows status transition' do
+ def email_form_value
+ page.find('.js-email').value
+ end
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ expect(email_form_value).to eq(cluster.user.email)
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ page.find('.js-email').set("new_email@example.org")
+ Clusters::Cluster.last.application_cert_manager.make_installing!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_cert_manager.make_installed!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
+ end
+ end
+
+ context 'when user installs Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-ingress') do
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+ page.find(:css, '.js-cluster-application-install-button').click
+
+ wait_for_requests
+ end
+ end
+
+ it 'shows the status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ # The application becomes installed but we keep waiting for external IP address
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed')
+ expect(page).to have_selector('.js-no-endpoint-message')
+ expect(page).to have_selector('.js-ingress-ip-loading-icon')
+
+ # We receive the external IP address and display
+ Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
+
+ expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
+ expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
+ expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
+ expect(page).not_to have_css('p', text: 'The endpoint is in the process of being assigned', visible: :all)
+ expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/clusters/applications_spec.rb b/spec/features/groups/clusters/applications_spec.rb
new file mode 100644
index 00000000000..5d48df234eb
--- /dev/null
+++ b/spec/features/groups/clusters/applications_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
+
+describe 'Group-level Cluster Applications', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'Installing applications' do
+ include_examples "installing applications on a cluster" do
+ let(:cluster_path) { group_cluster_path(group, cluster) }
+ let(:cluster_factory_args) { [:group, groups: [group]] }
+ end
+ end
+end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index bcd2b90d3bb..88a7aa51326 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -58,7 +58,7 @@ describe 'Help Pages' do
before do
stub_application_setting(version_check_enabled: true)
- allow(Rails.env).to receive(:production?).and_return(true)
+ stub_rails_env('production')
allow(VersionCheck).to receive(:url).and_return('/version-check-url')
sign_in(create(:user))
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 435b3cd2555..7d89b8e97a6 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
describe 'Merge request > User posts notes', :js do
include NoteInteractionHelpers
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
+
let(:user) { project.creator }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
@@ -33,17 +34,21 @@ describe 'Merge request > User posts notes', :js do
end
describe 'with text' do
+ let(:text) { 'This is awesome' }
+
before do
page.within('.js-main-target-form') do
- fill_in 'note[note]', with: 'This is awesome'
+ fill_in 'note[note]', with: text
end
end
- it 'has enable submit button and preview button' do
+ it 'has enable submit button, preview button and saves content to local storage' do
page.within('.js-main-target-form') do
expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
end
+
+ expect(page.evaluate_script("localStorage['autosave/Note/MergeRequest/#{merge_request.id}']")).to eq(text)
end
end
end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 6262f1ce055..c42eb8560a4 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -64,7 +64,7 @@ describe 'Merge request > User selects branches for new MR', :js do
click_button "Check out branch"
- expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
+ expect(page).to have_content 'git checkout -b "orphaned-branch" "origin/orphaned-branch"'
end
it 'allows filtering multiple dropdowns' do
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 3d15095e2da..ce971b158a3 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
+require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
-describe 'Clusters Applications', :js do
+describe 'Project-level Cluster Applications', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
@@ -14,229 +15,9 @@ describe 'Clusters Applications', :js do
end
describe 'Installing applications' do
- before do
- visit project_cluster_path(project, cluster)
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- it 'user is unable to install applications' do
- expect(page).not_to have_css('.js-cluster-application-row-helm')
- expect(page).not_to have_css('.js-cluster-application-install-button')
- end
- end
-
- context 'when cluster is created' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it 'user can install applications' do
- wait_for_requests
-
- page.within('.js-cluster-application-row-helm') do
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
- end
- end
-
- context 'when user installs Helm' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-helm') do
- page.find(:css, '.js-cluster-application-install-button').click
- end
-
- wait_for_requests
- end
-
- it 'they see status transition' do
- page.within('.js-cluster-application-row-helm') do
- # FE sends request and gets the response, then the buttons is "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_helm.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_helm.make_installed!
-
- expect(page).not_to have_css('.js-cluster-application-install-button')
- expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
- end
- end
-
- context 'when user installs Knative' do
- before do
- create(:clusters_applications_helm, :installed, cluster: cluster)
- end
-
- context 'on an abac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) }
-
- it 'shows info block and not be installable' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.rbac-notice')
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- end
- end
- end
-
- context 'on an rbac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it 'does not show callout block and be installable' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).not_to have_css('.rbac-notice')
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
- end
- end
-
- describe 'when user clicks install button' do
- def domainname_form_value
- page.find('.js-knative-domainname').value
- end
-
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
-
- page.find('.js-knative-domainname').set("domain.example.org")
-
- click_button 'Install'
-
- wait_for_requests
-
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_knative.make_installing!
- Clusters::Cluster.last.application_knative.make_installed!
- Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1')
- end
- end
-
- it 'shows status transition' do
- page.within('.js-cluster-application-row-knative') do
- expect(domainname_form_value).to eq('domain.example.org')
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster')
- expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes'
- end
-
- it 'can then update the domain' do
- page.within('.js-cluster-application-row-knative') do
- expect(ClusterPatchAppWorker).to receive(:perform_async)
-
- expect(domainname_form_value).to eq('domain.example.org')
-
- page.find('.js-knative-domainname').set("new.domain.example.org")
-
- click_button 'Save changes'
-
- wait_for_requests
-
- expect(domainname_form_value).to eq('new.domain.example.org')
- end
- end
- end
- end
- end
-
- context 'when user installs Cert Manager' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- create(:clusters_applications_helm, :installed, cluster: cluster)
-
- page.within('.js-cluster-application-row-cert_manager') do
- click_button 'Install'
- end
- end
-
- it 'shows status transition' do
- def email_form_value
- page.find('.js-email').value
- end
-
- page.within('.js-cluster-application-row-cert_manager') do
- expect(email_form_value).to eq(cluster.user.email)
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- page.find('.js-email').set("new_email@example.org")
- Clusters::Cluster.last.application_cert_manager.make_installing!
-
- expect(email_form_value).to eq('new_email@example.org')
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_cert_manager.make_installed!
-
- expect(email_form_value).to eq('new_email@example.org')
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
- end
- end
-
- context 'when user installs Ingress' do
- context 'when user installs application: Ingress' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- create(:clusters_applications_helm, :installed, cluster: cluster)
-
- page.within('.js-cluster-application-row-ingress') do
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
- page.find(:css, '.js-cluster-application-install-button').click
-
- wait_for_requests
- end
- end
-
- it 'they see status transition' do
- page.within('.js-cluster-application-row-ingress') do
- # FE sends request and gets the response, then the buttons is "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_ingress.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- # The application becomes installed but we keep waiting for external IP address
- Clusters::Cluster.last.application_ingress.make_installed!
-
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed')
- expect(page).to have_selector('.js-no-endpoint-message')
- expect(page).to have_selector('.js-ingress-ip-loading-icon')
-
- # We receive the external IP address and display
- Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
-
- expect(page).not_to have_css('.js-cluster-application-install-button')
- expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
- expect(page).not_to have_selector('.js-no-endpoint-message')
- expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
- end
-
- expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
- end
- end
- end
+ include_examples "installing applications on a cluster" do
+ let(:cluster_path) { project_cluster_path(project, cluster) }
+ let(:cluster_factory_args) { [projects: [project]] }
end
end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 820ce48e52c..a11237db508 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -18,6 +18,8 @@ describe 'Gcp Cluster', :js do
let(:project_id) { 'test-project-1234' }
before do
+ stub_feature_flags(create_eks_clusters: false)
+
allow_any_instance_of(Projects::ClustersController)
.to receive(:token_in_session).and_return('token')
allow_any_instance_of(Projects::ClustersController)
@@ -147,6 +149,7 @@ describe 'Gcp Cluster', :js do
context 'when user has not signed with Google' do
before do
+ stub_feature_flags(create_eks_clusters: false)
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index ce382c19fc1..d1cd19dff2d 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -51,6 +51,7 @@ describe 'Clusters', :js do
context 'when user has not signed in Google' do
before do
+ stub_feature_flags(create_eks_clusters: false)
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
@@ -62,4 +63,27 @@ describe 'Clusters', :js do
expect(page).to have_link('Google account')
end
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when user access create cluster page' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add Kubernetes cluster'
+ click_link 'Create new Cluster on GKE'
+ end
+
+ it 'user sees a link to create a GKE cluster' do
+ expect(page).to have_link('Google GKE')
+ end
+
+ it 'user sees a link to create an EKS cluster' do
+ expect(page).to have_link('Amazon EKS')
+ end
+ end
+ end
end
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index 154e948015f..8b3f193f418 100644
--- a/spec/features/user_can_display_performance_bar_spec.rb
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -37,7 +37,7 @@ describe 'User can display performance bar', :js do
shared_examples 'performance bar is enabled by default in development' do
before do
- allow(Rails.env).to receive(:development?).and_return(true)
+ stub_rails_env('development')
end
it 'shows the performance bar by default' do
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 879ff01f294..ef8749be0be 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -42,6 +42,24 @@ describe IssuesFinder do
end
end
+ context 'filtering by projects' do
+ context 'when projects are passed in a list of ids' do
+ let(:params) { { projects: [project1.id] } }
+
+ it 'returns the issue belonging to the projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'when projects are passed in a subquery' do
+ let(:params) { { projects: Project.id_in(project1.id) } }
+
+ it 'returns the issue belonging to the projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+ end
+
context 'filtering by group_id' do
let(:params) { { group_id: group.id } }
@@ -49,6 +67,30 @@ describe IssuesFinder do
it 'returns all group issues' do
expect(issues).to contain_exactly(issue1)
end
+
+ context 'when projects outside the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id] } }
+
+ it 'returns no issues' do
+ expect(issues).to be_empty
+ end
+ end
+
+ context 'when projects of the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project1.id] } }
+
+ it 'returns the issue within the group and projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'when projects of the group are passed as a subquery' do
+ let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } }
+
+ it 'returns the issue within the group and projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
end
context 'when include_subgroup param is true' do
@@ -59,6 +101,14 @@ describe IssuesFinder do
it 'returns all group and subgroup issues' do
expect(issues).to contain_exactly(issue1, issue4)
end
+
+ context 'when mixed projects are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } }
+
+ it 'returns the issue within the group and projects' do
+ expect(issues).to contain_exactly(issue4)
+ end
+ end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 78224f0b9da..6c0bbeff4f4 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5)
end
- it 'filters by project' do
+ it 'filters by project_id' do
params = { project_id: project1.id, scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute
@@ -21,6 +21,14 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1)
end
+ it 'filters by projects' do
+ params = { projects: [project2.id, project3.id] }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3, merge_request4)
+ end
+
it 'filters by commit sha' do
merge_requests = described_class.new(
user,
@@ -49,6 +57,16 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request5)
end
+
+ it 'filters by group projects including subgroups' do
+ # project3 is not in the group, so it should not return merge_request4
+ projects = [project3.id, project4.id]
+ params = { group_id: group.id, include_subgroups: true, projects: projects }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request5)
+ end
end
it 'filters by non_archived' do
diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
index 88b0fecc24c..d37f5b864d7 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
@@ -1,6 +1,10 @@
{
"type": "object",
"properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": "string" },
"merge_params": { "type": ["object", "null"] },
"state": { "type": "string" },
"source_branch": { "type": "string" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json
index 5713ea1f526..bb0190955f0 100644
--- a/spec/fixtures/api/schemas/public_api/v4/tag.json
+++ b/spec/fixtures/api/schemas/public_api/v4/tag.json
@@ -16,7 +16,8 @@
{ "type": "null" },
{ "$ref": "release/tag_release.json" }
]
- }
+ },
+ "protected": { "type": "boolean" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/statistics.json b/spec/fixtures/api/schemas/statistics.json
new file mode 100644
index 00000000000..ef2f39aad9d
--- /dev/null
+++ b/spec/fixtures/api/schemas/statistics.json
@@ -0,0 +1,29 @@
+{
+ "type": "object",
+ "required" : [
+ "forks",
+ "issues",
+ "merge_requests",
+ "notes",
+ "snippets",
+ "ssh_keys",
+ "milestones",
+ "users",
+ "projects",
+ "groups",
+ "active_users"
+ ],
+ "properties" : {
+ "forks": { "type": "string" },
+ "issues'": { "type": "string" },
+ "merge_requests'": { "type": "string" },
+ "notes'": { "type": "string" },
+ "snippets'": { "type": "string" },
+ "ssh_keys'": { "type": "string" },
+ "milestones'": { "type": "string" },
+ "users'": { "type": "string" },
+ "projects'": { "type": "string" },
+ "groups'": { "type": "string" },
+ "active_users'": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/valid.po b/spec/fixtures/valid.po
index 155b6cbb95d..28826f05595 100644
--- a/spec/fixtures/valid.po
+++ b/spec/fixtures/valid.po
@@ -1128,3 +1128,8 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "padre"
msgstr[1] "padres"
+
+msgid "CycleAnalytics|%{stageName}"
+msgid_plural "CycleAnalytics|%d stages selected"
+msgstr[0] "%{stageName}"
+msgstr[1] "%d stages selected"
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
new file mode 100644
index 00000000000..25b1d432e2d
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -0,0 +1,73 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import StatisticsPanelApp from '~/admin/statistics_panel/components/app.vue';
+import statisticsLabels from '~/admin/statistics_panel/constants';
+import createStore from '~/admin/statistics_panel/store';
+import { GlLoadingIcon } from '@gitlab/ui';
+import mockStatistics from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Admin statistics app', () => {
+ let wrapper;
+ let store;
+ let axiosMock;
+
+ const createComponent = () => {
+ wrapper = shallowMount(StatisticsPanelApp, {
+ localVue,
+ store,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(200);
+ store = createStore();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findStats = idx => wrapper.findAll('.js-stats').at(idx);
+
+ describe('template', () => {
+ describe('when app is loading', () => {
+ it('renders a loading indicator', () => {
+ store.dispatch('requestStatistics');
+ createComponent();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when app has finished loading', () => {
+ const statistics = convertObjectPropsToCamelCase(mockStatistics, { deep: true });
+
+ it.each`
+ statistic | count | index
+ ${'forks'} | ${12} | ${0}
+ ${'issues'} | ${180} | ${1}
+ ${'mergeRequests'} | ${31} | ${2}
+ ${'notes'} | ${986} | ${3}
+ ${'snippets'} | ${50} | ${4}
+ ${'sshKeys'} | ${10} | ${5}
+ ${'milestones'} | ${40} | ${6}
+ ${'activeUsers'} | ${50} | ${7}
+ `('renders the count for the $statistic statistic', ({ statistic, count, index }) => {
+ const label = statisticsLabels[statistic];
+ store.dispatch('receiveStatisticsSuccess', statistics);
+ createComponent();
+
+ expect(findStats(index).text()).toContain(label);
+ expect(findStats(index).text()).toContain(count);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/statistics_panel/mock_data.js b/spec/frontend/admin/statistics_panel/mock_data.js
new file mode 100644
index 00000000000..6d861059dfd
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/mock_data.js
@@ -0,0 +1,15 @@
+const mockStatistics = {
+ forks: 12,
+ issues: 180,
+ merge_requests: 31,
+ notes: 986,
+ snippets: 50,
+ ssh_keys: 10,
+ milestones: 40,
+ users: 50,
+ projects: 29,
+ groups: 9,
+ active_users: 50,
+};
+
+export default mockStatistics;
diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js
new file mode 100644
index 00000000000..9b18b1aebda
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js
@@ -0,0 +1,115 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as actions from '~/admin/statistics_panel/store/actions';
+import * as types from '~/admin/statistics_panel/store/mutation_types';
+import getInitialState from '~/admin/statistics_panel/store/state';
+import mockStatistics from '../mock_data';
+
+describe('Admin statistics panel actions', () => {
+ let mock;
+ let state;
+
+ beforeEach(() => {
+ state = getInitialState();
+ mock = new MockAdapter(axios);
+ });
+
+ describe('fetchStatistics', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(200, mockStatistics);
+ });
+
+ it('dispatches success with received data', done =>
+ testAction(
+ actions.fetchStatistics,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestStatistics' },
+ {
+ type: 'receiveStatisticsSuccess',
+ payload: expect.objectContaining(
+ convertObjectPropsToCamelCase(mockStatistics, { deep: true }),
+ ),
+ },
+ ],
+ done,
+ ));
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(500);
+ });
+
+ it('dispatches error', done =>
+ testAction(
+ actions.fetchStatistics,
+ null,
+ state,
+ [],
+ [
+ {
+ type: 'requestStatistics',
+ },
+ {
+ type: 'receiveStatisticsError',
+ payload: new Error('Request failed with status code 500'),
+ },
+ ],
+ done,
+ ));
+ });
+ });
+
+ describe('requestStatistic', () => {
+ it('should commit the request mutation', done =>
+ testAction(
+ actions.requestStatistics,
+ null,
+ state,
+ [{ type: types.REQUEST_STATISTICS }],
+ [],
+ done,
+ ));
+ });
+
+ describe('receiveStatisticsSuccess', () => {
+ it('should commit received data', done =>
+ testAction(
+ actions.receiveStatisticsSuccess,
+ mockStatistics,
+ state,
+ [
+ {
+ type: types.RECEIVE_STATISTICS_SUCCESS,
+ payload: mockStatistics,
+ },
+ ],
+ [],
+ done,
+ ));
+ });
+
+ describe('receiveStatisticsError', () => {
+ it('should commit error', done => {
+ testAction(
+ actions.receiveStatisticsError,
+ 500,
+ state,
+ [
+ {
+ type: types.RECEIVE_STATISTICS_ERROR,
+ payload: 500,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/statistics_panel/store/getters_spec.js b/spec/frontend/admin/statistics_panel/store/getters_spec.js
new file mode 100644
index 00000000000..152d82531ed
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/store/getters_spec.js
@@ -0,0 +1,48 @@
+import createState from '~/admin/statistics_panel/store/state';
+import * as getters from '~/admin/statistics_panel/store/getters';
+
+describe('Admin statistics panel getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('getStatistics', () => {
+ describe('when statistics data exists', () => {
+ it('returns an array of statistics objects with key, label and value', () => {
+ state.statistics = { forks: 10, issues: 20 };
+
+ const statisticsLabels = {
+ forks: 'Forks',
+ issues: 'Issues',
+ };
+
+ const statisticsData = [
+ { key: 'forks', label: 'Forks', value: 10 },
+ { key: 'issues', label: 'Issues', value: 20 },
+ ];
+
+ expect(getters.getStatistics(state)(statisticsLabels)).toEqual(statisticsData);
+ });
+ });
+
+ describe('when no statistics data exists', () => {
+ it('returns an array of statistics objects with key, label and sets value to null', () => {
+ state.statistics = null;
+
+ const statisticsLabels = {
+ forks: 'Forks',
+ issues: 'Issues',
+ };
+
+ const statisticsData = [
+ { key: 'forks', label: 'Forks', value: null },
+ { key: 'issues', label: 'Issues', value: null },
+ ];
+
+ expect(getters.getStatistics(state)(statisticsLabels)).toEqual(statisticsData);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
new file mode 100644
index 00000000000..179f38d2bc5
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
@@ -0,0 +1,41 @@
+import mutations from '~/admin/statistics_panel/store/mutations';
+import * as types from '~/admin/statistics_panel/store/mutation_types';
+import getInitialState from '~/admin/statistics_panel/store/state';
+import mockStatistics from '../mock_data';
+
+describe('Admin statistics panel mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = getInitialState();
+ });
+
+ describe(`${types.REQUEST_STATISTICS}`, () => {
+ it('sets isLoading to true', () => {
+ mutations[types.REQUEST_STATISTICS](state);
+
+ expect(state.isLoading).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_STATISTICS_SUCCESS}`, () => {
+ it('updates the store with the with statistics', () => {
+ mutations[types.RECEIVE_STATISTICS_SUCCESS](state, mockStatistics);
+
+ expect(state.isLoading).toBe(false);
+ expect(state.error).toBe(null);
+ expect(state.statistics).toEqual(mockStatistics);
+ });
+ });
+
+ describe(`${types.RECEIVE_STATISTICS_ERROR}`, () => {
+ it('sets error and clears data', () => {
+ const error = 500;
+ mutations[types.RECEIVE_STATISTICS_ERROR](state, error);
+
+ expect(state.isLoading).toBe(false);
+ expect(state.error).toBe(error);
+ expect(state.statistics).toEqual(null);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 221ebb143be..fbcab078993 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -85,11 +85,48 @@ describe('Applications', () => {
});
it('renders a row for Jupyter', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
+ });
+ });
+
+ describe('Instance cluster applications', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ type: CLUSTER_TYPE.INSTANCE,
+ applications: APPLICATIONS_MOCK_STATE,
+ });
+ });
+
+ it('renders a row for Helm Tiller', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-helm')).not.toBeNull();
+ });
+
+ it('renders a row for Ingress', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-ingress')).not.toBeNull();
+ });
+
+ it('renders a row for Cert-Manager', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
+ });
+
+ it('renders a row for Prometheus', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
+ });
+
+ it('renders a row for GitLab Runner', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
+ });
+
+ it('renders a row for Jupyter', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBeNull();
+ });
+
+ it('renders a row for Knative', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 67e5dc399ac..ce8b8908026 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -11,19 +11,24 @@ describe('ErrorTrackingList', () => {
let wrapper;
let actions;
- function mountComponent({ errorTrackingEnabled = true } = {}) {
+ function mountComponent({
+ errorTrackingEnabled = true,
+ userCanEnableErrorTracking = true,
+ stubs = {
+ 'gl-link': GlLink,
+ },
+ } = {}) {
wrapper = shallowMount(ErrorTrackingList, {
localVue,
store,
propsData: {
indexPath: '/path',
enableErrorTrackingLink: '/link',
+ userCanEnableErrorTracking,
errorTrackingEnabled,
illustrationPath: 'illustration/path',
},
- stubs: {
- 'gl-link': GlLink,
- },
+ stubs,
});
}
@@ -115,4 +120,23 @@ describe('ErrorTrackingList', () => {
expect(wrapper.find(GlButton).exists()).toBeFalsy();
});
});
+
+ describe('When error tracking is disabled and user is not allowed to enable it', () => {
+ beforeEach(() => {
+ mountComponent({
+ errorTrackingEnabled: false,
+ userCanEnableErrorTracking: false,
+ stubs: {
+ 'gl-link': GlLink,
+ 'gl-empty-state': GlEmptyState,
+ },
+ });
+ });
+
+ it('shows empty state', () => {
+ expect(wrapper.find('a').attributes('href')).toBe(
+ '/help/user/project/operations/error_tracking.html',
+ );
+ });
+ });
});
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
new file mode 100644
index 00000000000..dfc76628d0c
--- /dev/null
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -0,0 +1,318 @@
+import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import smooshpack from 'smooshpack';
+import Clientside from '~/ide/components/preview/clientside.vue';
+
+jest.mock('smooshpack', () => ({
+ Manager: jest.fn(),
+}));
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const dummyPackageJson = () => ({
+ raw: JSON.stringify({
+ main: 'index.js',
+ }),
+});
+
+describe('IDE clientside preview', () => {
+ let wrapper;
+ let store;
+ const storeActions = {
+ getFileData: jest.fn().mockReturnValue(Promise.resolve({})),
+ getRawFileData: jest.fn().mockReturnValue(Promise.resolve('')),
+ };
+
+ const waitForCalls = () => new Promise(setImmediate);
+
+ const createComponent = ({ state, getters } = {}) => {
+ store = new Vuex.Store({
+ state: {
+ entries: {},
+ links: {},
+ ...state,
+ },
+ getters: {
+ packageJson: () => '',
+ currentProject: () => ({
+ visibility: 'public',
+ }),
+ ...getters,
+ },
+ actions: storeActions,
+ });
+
+ wrapper = shallowMount(Clientside, {
+ sync: false,
+ store,
+ localVue,
+ });
+ };
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('without main entry', () => {
+ it('creates sandpack manager', () => {
+ createComponent();
+ expect(smooshpack.Manager).not.toHaveBeenCalled();
+ });
+ });
+ describe('with main entry', () => {
+ beforeEach(() => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ return wrapper.vm.initPreview();
+ });
+
+ it('creates sandpack manager', () => {
+ expect(smooshpack.Manager).toHaveBeenCalledWith(
+ '#ide-preview',
+ {
+ files: {},
+ entry: '/index.js',
+ showOpenInCodeSandbox: true,
+ },
+ {
+ fileResolver: {
+ isFile: expect.any(Function),
+ readFile: expect.any(Function),
+ },
+ },
+ );
+ });
+ });
+
+ describe('computed', () => {
+ describe('normalizedEntries', () => {
+ it('returns flattened list of blobs with content', () => {
+ createComponent({
+ state: {
+ entries: {
+ 'index.js': { type: 'blob', raw: 'test' },
+ 'index2.js': { type: 'blob', content: 'content' },
+ tree: { type: 'tree' },
+ empty: { type: 'blob' },
+ },
+ },
+ });
+
+ expect(wrapper.vm.normalizedEntries).toEqual({
+ '/index.js': {
+ code: 'test',
+ },
+ '/index2.js': {
+ code: 'content',
+ },
+ });
+ });
+ });
+
+ describe('mainEntry', () => {
+ it('returns false when package.json is empty', () => {
+ createComponent();
+ expect(wrapper.vm.mainEntry).toBe(false);
+ });
+
+ it('returns main key from package.json', () => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ expect(wrapper.vm.mainEntry).toBe('index.js');
+ });
+ });
+
+ describe('showPreview', () => {
+ it('returns false if no mainEntry', () => {
+ createComponent();
+ expect(wrapper.vm.showPreview).toBe(false);
+ });
+
+ it('returns false if loading and mainEntry exists', () => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ wrapper.setData({ loading: true });
+
+ expect(wrapper.vm.showPreview).toBe(false);
+ });
+
+ it('returns true if not loading and mainEntry exists', () => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ wrapper.setData({ loading: false });
+
+ expect(wrapper.vm.showPreview).toBe(true);
+ });
+ });
+
+ describe('showEmptyState', () => {
+ it('returns true if no mainEntry exists', () => {
+ createComponent();
+ wrapper.setData({ loading: false });
+ expect(wrapper.vm.showEmptyState).toBe(true);
+ });
+
+ it('returns false if loading', () => {
+ createComponent();
+ wrapper.setData({ loading: true });
+
+ expect(wrapper.vm.showEmptyState).toBe(false);
+ });
+
+ it('returns false if not loading and mainEntry exists', () => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ wrapper.setData({ loading: false });
+
+ expect(wrapper.vm.showEmptyState).toBe(false);
+ });
+ });
+
+ describe('showOpenInCodeSandbox', () => {
+ it('returns true when visiblity is public', () => {
+ createComponent({ getters: { currentProject: () => ({ visibility: 'public' }) } });
+
+ expect(wrapper.vm.showOpenInCodeSandbox).toBe(true);
+ });
+
+ it('returns false when visiblity is private', () => {
+ createComponent({ getters: { currentProject: () => ({ visibility: 'private' }) } });
+
+ expect(wrapper.vm.showOpenInCodeSandbox).toBe(false);
+ });
+ });
+
+ describe('sandboxOpts', () => {
+ beforeEach(() => {
+ createComponent({
+ state: {
+ entries: {
+ 'index.js': { type: 'blob', raw: 'test' },
+ 'package.json': dummyPackageJson(),
+ },
+ },
+ getters: {
+ packageJson: dummyPackageJson,
+ },
+ });
+ });
+
+ it('returns sandbox options', () => {
+ expect(wrapper.vm.sandboxOpts).toEqual({
+ files: {
+ '/index.js': {
+ code: 'test',
+ },
+ '/package.json': {
+ code: '{"main":"index.js"}',
+ },
+ },
+ entry: '/index.js',
+ showOpenInCodeSandbox: true,
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('loadFileContent', () => {
+ beforeEach(() => {
+ createComponent();
+ return wrapper.vm.loadFileContent('package.json');
+ });
+
+ it('calls getFileData', () => {
+ expect(storeActions.getFileData).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ path: 'package.json',
+ makeFileActive: false,
+ },
+ undefined, // vuex callback
+ );
+ });
+
+ it('calls getRawFileData', () => {
+ expect(storeActions.getRawFileData).toHaveBeenCalledWith(
+ expect.any(Object),
+ {
+ path: 'package.json',
+ },
+ undefined, // vuex callback
+ );
+ });
+ });
+
+ describe('update', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.setData({ sandpackReady: true });
+ });
+
+ it('initializes manager if manager is empty', () => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ wrapper.setData({ sandpackReady: true });
+ wrapper.vm.update();
+
+ jest.advanceTimersByTime(250);
+
+ return waitForCalls().then(() => {
+ expect(smooshpack.Manager).toHaveBeenCalled();
+ });
+ });
+
+ it('calls updatePreview', () => {
+ wrapper.setData({
+ manager: {
+ listener: jest.fn(),
+ updatePreview: jest.fn(),
+ },
+ });
+ wrapper.vm.update();
+
+ jest.advanceTimersByTime(250);
+ expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders ide-preview element when showPreview is true', () => {
+ createComponent({ getters: { packageJson: dummyPackageJson } });
+ wrapper.setData({ loading: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('#ide-preview').exists()).toBe(true);
+ });
+ });
+
+ it('renders empty state', () => {
+ createComponent();
+ wrapper.setData({ loading: false });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain(
+ 'Preview your web application using Web IDE client-side evaluation.',
+ );
+ });
+ });
+
+ it('renders loading icon', () => {
+ createComponent();
+ wrapper.setData({ loading: true });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/axios_utils_spec.js b/spec/frontend/lib/utils/axios_utils_spec.js
new file mode 100644
index 00000000000..d5c39567f06
--- /dev/null
+++ b/spec/frontend/lib/utils/axios_utils_spec.js
@@ -0,0 +1,45 @@
+/* eslint-disable promise/catch-or-return */
+
+import AxiosMockAdapter from 'axios-mock-adapter';
+
+import axios from '~/lib/utils/axios_utils';
+
+describe('axios_utils', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+ mock.onAny('/ok').reply(200);
+ mock.onAny('/err').reply(500);
+ expect(axios.countActiveRequests()).toBe(0);
+ });
+
+ afterEach(() => axios.waitForAll().finally(() => mock.restore()));
+
+ describe('waitForAll', () => {
+ it('resolves if there are no requests', () => axios.waitForAll());
+
+ it('waits for all requests to finish', () => {
+ const handler = jest.fn();
+ axios.get('/ok').then(handler);
+ axios.get('/err').catch(handler);
+
+ return axios.waitForAll().finally(() => {
+ expect(handler).toHaveBeenCalledTimes(2);
+ expect(handler.mock.calls[0][0].status).toBe(200);
+ expect(handler.mock.calls[1][0].response.status).toBe(500);
+ });
+ });
+ });
+
+ describe('waitFor', () => {
+ it('waits for requests on a specific URL', () => {
+ const handler = jest.fn();
+ axios.get('/ok').finally(handler);
+ axios.waitFor('/err').finally(() => {
+ throw new Error('waitFor on /err should not be called');
+ });
+ return axios.waitFor('/ok');
+ });
+ });
+});
diff --git a/spec/frontend/mocks/ce/lib/utils/axios_utils.js b/spec/frontend/mocks/ce/lib/utils/axios_utils.js
index a3783b91f95..85fad231d28 100644
--- a/spec/frontend/mocks/ce/lib/utils/axios_utils.js
+++ b/spec/frontend/mocks/ce/lib/utils/axios_utils.js
@@ -1,3 +1,5 @@
+import EventEmitter from 'events';
+
const axios = jest.requireActual('~/lib/utils/axios_utils').default;
axios.isMock = true;
@@ -13,4 +15,64 @@ axios.defaults.adapter = config => {
throw error;
};
+// Count active requests and provide a way to wait for them
+let activeRequests = 0;
+const events = new EventEmitter();
+const onRequest = () => {
+ activeRequests += 1;
+};
+
+// Use setImmediate to alloow the response interceptor to finish
+const onResponse = config => {
+ activeRequests -= 1;
+ setImmediate(() => {
+ events.emit('response', config);
+ });
+};
+
+const subscribeToResponse = (predicate = () => true) =>
+ new Promise(resolve => {
+ const listener = (config = {}) => {
+ if (predicate(config)) {
+ events.off('response', listener);
+ resolve(config);
+ }
+ };
+
+ events.on('response', listener);
+
+ // If a request has been made synchronously, setImmediate waits for it to be
+ // processed and the counter incremented.
+ setImmediate(listener);
+ });
+
+/**
+ * Registers a callback function to be run after a request to the given URL finishes.
+ */
+axios.waitFor = url => subscribeToResponse(({ url: configUrl }) => configUrl === url);
+
+/**
+ * Registers a callback function to be run after all requests have finished. If there are no requests waiting, the callback is executed immediately.
+ */
+axios.waitForAll = () => subscribeToResponse(() => activeRequests === 0);
+
+axios.countActiveRequests = () => activeRequests;
+
+axios.interceptors.request.use(config => {
+ onRequest();
+ return config;
+});
+
+// Remove the global counter
+axios.interceptors.response.use(
+ response => {
+ onResponse(response.config);
+ return response;
+ },
+ err => {
+ onResponse(err.config);
+ return Promise.reject(err);
+ },
+);
+
export default axios;
diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js
index b57041cf4d1..96133c601aa 100644
--- a/spec/frontend/notes/old_notes_spec.js
+++ b/spec/frontend/notes/old_notes_spec.js
@@ -49,17 +49,12 @@ describe('Old Notes (~/notes.js)', () => {
setTestTimeoutOnce(4000);
});
- afterEach(done => {
+ afterEach(() => {
// The Notes component sets a polling interval. Clear it after every run.
// Make sure to use jest.runOnlyPendingTimers() instead of runAllTimers().
jest.clearAllTimers();
- setImmediate(() => {
- // Wait for any requests to resolve, otherwise we get failures about
- // unmocked requests.
- mockAxios.restore();
- done();
- });
+ return axios.waitForAll().finally(() => mockAxios.restore());
});
it('loads the Notes class into the DOM', () => {
diff --git a/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
new file mode 100644
index 00000000000..f076c45e56c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
@@ -0,0 +1,115 @@
+import Vuex from 'vuex';
+import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
+import { GlToggle } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('GlToggleVuex component', () => {
+ let wrapper;
+ let store;
+
+ const findButton = () => wrapper.find('button');
+
+ const createWrapper = (props = {}) => {
+ wrapper = mount(GlToggleVuex, {
+ localVue,
+ store,
+ propsData: {
+ stateProperty: 'toggleState',
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ toggleState: false,
+ },
+ actions: {
+ setToggleState: ({ commit }, { key, value }) => commit('setToggleState', { key, value }),
+ },
+ mutations: {
+ setToggleState: (state, { key, value }) => {
+ state[key] = value;
+ },
+ },
+ });
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders gl-toggle', () => {
+ expect(wrapper.find(GlToggle).exists()).toBe(true);
+ });
+
+ it('properly computes default value for setAction', () => {
+ expect(wrapper.props('setAction')).toBe('setToggleState');
+ });
+
+ describe('without a store module', () => {
+ it('calls action with new value when value changes', () => {
+ jest.spyOn(store, 'dispatch');
+
+ findButton().trigger('click');
+ expect(store.dispatch).toHaveBeenCalledWith('setToggleState', {
+ key: 'toggleState',
+ value: true,
+ });
+ });
+
+ it('updates store property when value changes', () => {
+ findButton().trigger('click');
+ expect(store.state.toggleState).toBe(true);
+ });
+ });
+
+ describe('with a store module', () => {
+ beforeEach(() => {
+ store = new Vuex.Store({
+ modules: {
+ someModule: {
+ namespaced: true,
+ state: {
+ toggleState: false,
+ },
+ actions: {
+ setToggleState: ({ commit }, { key, value }) =>
+ commit('setToggleState', { key, value }),
+ },
+ mutations: {
+ setToggleState: (state, { key, value }) => {
+ state[key] = value;
+ },
+ },
+ },
+ },
+ });
+
+ createWrapper({
+ storeModule: 'someModule',
+ });
+ });
+
+ it('calls action with new value when value changes', () => {
+ jest.spyOn(store, 'dispatch');
+
+ findButton().trigger('click');
+ expect(store.dispatch).toHaveBeenCalledWith('someModule/setToggleState', {
+ key: 'toggleState',
+ value: true,
+ });
+ });
+
+ it('updates store property when value changes', () => {
+ findButton().trigger('click');
+ expect(store.state.someModule.toggleState).toBe(true);
+ });
+ });
+});
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 3d15306d4d2..e062c841717 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -27,7 +27,7 @@ describe EventsHelper do
end
describe '#event_feed_url' do
- let(:event) { create(:event) }
+ let(:event) { create(:event).present }
let(:project) { create(:project, :public, :repository) }
context 'issue' do
diff --git a/spec/helpers/external_link_helper_spec.rb b/spec/helpers/external_link_helper_spec.rb
new file mode 100644
index 00000000000..7fc4ef18731
--- /dev/null
+++ b/spec/helpers/external_link_helper_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExternalLinkHelper do
+ include IconsHelper
+
+ it 'returns external link with icon' do
+ expect(external_link('https://gitlab.com', 'https://gitlab.com').to_s)
+ .to eq('<a target="_blank" rel="noopener noreferrer" href="https://gitlab.com">https://gitlab.com <i aria-hidden="true" data-hidden="true" class="fa fa-external-link"></i></a>')
+ end
+
+ it 'allows options when creating external link with icon' do
+ expect(external_link('https://gitlab.com', 'https://gitlab.com', { "data-foo": "bar", class: "externalLink" }).to_s)
+ .to eq('<a target="_blank" rel="noopener noreferrer" data-foo="bar" class="externalLink" href="https://gitlab.com">https://gitlab.com <i aria-hidden="true" data-hidden="true" class="fa fa-external-link"></i></a>')
+ end
+end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index f92b94a9583..950f951e22e 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -60,20 +60,19 @@ describe IconsHelper do
non_existing = 'non_existing_icon_sprite'
it 'raises in development mode' do
- allow(Rails.env).to receive(:development?).and_return(true)
+ stub_rails_env('development')
expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/)
end
it 'raises in test mode' do
- allow(Rails.env).to receive(:test?).and_return(true)
+ stub_rails_env('test')
expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/)
end
it 'does not raise in production mode' do
- allow(Rails.env).to receive(:test?).and_return(false)
- allow(Rails.env).to receive(:development?).and_return(false)
+ stub_rails_env('production')
expect { sprite_icon(non_existing) }.not_to raise_error
end
diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb
index 7516a636c93..064b3ad21cb 100644
--- a/spec/helpers/projects/error_tracking_helper_spec.rb
+++ b/spec/helpers/projects/error_tracking_helper_spec.rb
@@ -6,21 +6,31 @@ describe Projects::ErrorTrackingHelper do
include Gitlab::Routing.url_helpers
set(:project) { create(:project) }
+ set(:current_user) { create(:user) }
describe '#error_tracking_data' do
+ let(:can_enable_error_tracking) { true }
let(:setting_path) { project_settings_operations_path(project) }
let(:index_path) do
project_error_tracking_index_path(project, format: :json)
end
+ before do
+ allow(helper)
+ .to receive(:can?)
+ .with(current_user, :admin_operations, project)
+ .and_return(can_enable_error_tracking)
+ end
+
context 'without error_tracking_setting' do
it 'returns frontend configuration' do
- expect(error_tracking_data(project)).to eq(
+ expect(helper.error_tracking_data(current_user, project)).to match(
'index-path' => index_path,
+ 'user-can-enable-error-tracking' => 'true',
'enable-error-tracking-link' => setting_path,
'error-tracking-enabled' => 'false',
- "illustration-path" => "/images/illustrations/cluster_popover.svg"
+ 'illustration-path' => match_asset_path('/assets/illustrations/cluster_popover.svg')
)
end
end
@@ -36,7 +46,7 @@ describe Projects::ErrorTrackingHelper do
end
it 'show error tracking enabled' do
- expect(error_tracking_data(project)).to include(
+ expect(helper.error_tracking_data(current_user, project)).to include(
'error-tracking-enabled' => 'true'
)
end
@@ -48,11 +58,21 @@ describe Projects::ErrorTrackingHelper do
end
it 'show error tracking not enabled' do
- expect(error_tracking_data(project)).to include(
+ expect(helper.error_tracking_data(current_user, project)).to include(
'error-tracking-enabled' => 'false'
)
end
end
end
+
+ context 'when user is not maintainer' do
+ let(:can_enable_error_tracking) { false }
+
+ it 'shows error tracking enablement as disabled' do
+ expect(helper.error_tracking_data(current_user, project)).to include(
+ 'user-can-enable-error-tracking' => 'false'
+ )
+ end
+ end
end
end
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
new file mode 100644
index 00000000000..ff820b3cc95
--- /dev/null
+++ b/spec/helpers/releases_helper_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ReleasesHelper do
+ describe '#illustration' do
+ it 'returns the correct image path' do
+ expect(helper.illustration).to match(/illustrations\/releases-(\w+)\.svg/)
+ end
+ end
+
+ describe '#help_page' do
+ it 'returns the correct link to the help page' do
+ expect(helper.help_page).to include('user/project/releases/index')
+ end
+ end
+
+ context 'url helpers' do
+ let(:project) { build(:project, namespace: create(:group)) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ describe '#url_for_merge_requests' do
+ it 'returns the the correct link with the correct parameters' do
+ path = "#{project.group.path}/#{project.path}/merge_requests?scope=all&state=opened"
+ expect(helper.url_for_merge_requests).to include(path)
+ end
+ end
+
+ describe '#url_for_issues' do
+ it 'returns the the correct link with the correct parameters' do
+ path = "#{project.group.path}/#{project.path}/issues?scope=all&state=opened"
+ expect(helper.url_for_issues).to include(path)
+ end
+ end
+
+ describe '#data_for_releases_page' do
+ it 'has the needed data to display release blocks' do
+ keys = %i(project_id illustration_path documentation_path merge_requests_url issues_url)
+ expect(helper.data_for_releases_page.keys).to eq(keys)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index e384e2bf9a0..edc0d64d031 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe VersionCheckHelper do
describe '#version_status_badge' do
it 'returns nil if not dev environment and not enabled' do
- allow(Rails.env).to receive(:production?) { false }
+ stub_rails_env('development')
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
expect(helper.version_status_badge).to be(nil)
@@ -11,7 +11,7 @@ describe VersionCheckHelper do
context 'when production and enabled' do
before do
- allow(Rails.env).to receive(:production?) { true }
+ stub_rails_env('production')
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true }
allow(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
end
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index 721d0b8172d..76675a78db2 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -171,6 +171,32 @@ describe('Issue boards new issue form', () => {
.then(done)
.catch(done.fail);
});
+
+ it('sets detail weight after submit', done => {
+ boardsStore.weightFeatureAvailable = true;
+ vm.title = 'submit issue';
+
+ Vue.nextTick()
+ .then(submitIssue)
+ .then(() => {
+ expect(boardsStore.detail.list.weight).toBe(list.weight);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not set detail weight after submit', done => {
+ boardsStore.weightFeatureAvailable = false;
+ vm.title = 'submit issue';
+
+ Vue.nextTick()
+ .then(submitIssue)
+ .then(() => {
+ expect(boardsStore.detail.list.weight).toBe(list.weight);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('submit error', () => {
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index ea22ae5c4e7..50ad1442873 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -12,6 +12,7 @@ export const listObj = {
position: 0,
title: 'Test',
list_type: 'label',
+ weight: 3,
label: {
id: 5000,
title: 'Testing',
@@ -26,6 +27,7 @@ export const listObjDuplicate = {
position: 1,
title: 'Test',
list_type: 'label',
+ weight: 3,
label: {
id: listObj.label.id,
title: 'Testing',
diff --git a/spec/javascripts/ide/components/preview/clientside_spec.js b/spec/javascripts/ide/components/preview/clientside_spec.js
deleted file mode 100644
index b9bf5c51ffe..00000000000
--- a/spec/javascripts/ide/components/preview/clientside_spec.js
+++ /dev/null
@@ -1,363 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
-import Clientside from '~/ide/components/preview/clientside.vue';
-import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
-import { resetStore, file } from '../../helpers';
-
-describe('IDE clientside preview', () => {
- let vm;
- let Component;
-
- beforeAll(() => {
- Component = Vue.extend(Clientside);
- });
-
- beforeEach(done => {
- const store = createStore();
-
- Vue.set(store.state.entries, 'package.json', {
- ...file('package.json'),
- });
- Vue.set(store.state, 'currentProjectId', 'gitlab-ce');
- Vue.set(store.state.projects, 'gitlab-ce', {
- visibility: 'public',
- });
-
- vm = createComponentWithStore(Component, store);
-
- spyOn(vm, 'getFileData').and.returnValue(Promise.resolve());
- spyOn(vm, 'getRawFileData').and.returnValue(Promise.resolve(''));
- spyOn(vm, 'initManager');
-
- vm.$mount();
-
- timeoutPromise()
- .then(() => vm.$nextTick())
- .then(done)
- .catch(done.fail);
- });
-
- afterEach(() => {
- vm.$destroy();
- resetStore(vm.$store);
- });
-
- describe('without main entry', () => {
- it('creates sandpack manager', () => {
- expect(vm.initManager).not.toHaveBeenCalled();
- });
- });
-
- describe('with main entry', () => {
- beforeEach(done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
-
- vm.$nextTick()
- .then(() => vm.initPreview())
- .then(vm.$nextTick)
- .then(done)
- .catch(done.fail);
- });
-
- it('creates sandpack manager', () => {
- expect(vm.initManager).toHaveBeenCalledWith(
- '#ide-preview',
- {
- files: jasmine.any(Object),
- entry: '/index.js',
- showOpenInCodeSandbox: true,
- },
- {
- fileResolver: {
- isFile: jasmine.any(Function),
- readFile: jasmine.any(Function),
- },
- },
- );
- });
- });
-
- describe('computed', () => {
- describe('normalizedEntries', () => {
- beforeEach(done => {
- vm.$store.state.entries['index.js'] = {
- ...file('index.js'),
- type: 'blob',
- raw: 'test',
- };
- vm.$store.state.entries['index2.js'] = {
- ...file('index2.js'),
- type: 'blob',
- content: 'content',
- };
- vm.$store.state.entries.tree = {
- ...file('tree'),
- type: 'tree',
- };
- vm.$store.state.entries.empty = {
- ...file('empty'),
- type: 'blob',
- };
-
- vm.$nextTick(done);
- });
-
- it('returns flattened list of blobs with content', () => {
- expect(vm.normalizedEntries).toEqual({
- '/index.js': {
- code: 'test',
- },
- '/index2.js': {
- code: 'content',
- },
- });
- });
- });
-
- describe('mainEntry', () => {
- it('returns false when package.json is empty', () => {
- expect(vm.mainEntry).toBe(false);
- });
-
- it('returns main key from package.json', done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
-
- vm.$nextTick(() => {
- expect(vm.mainEntry).toBe('index.js');
-
- done();
- });
- });
- });
-
- describe('showPreview', () => {
- it('returns false if no mainEntry', () => {
- expect(vm.showPreview).toBe(false);
- });
-
- it('returns false if loading', done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
- vm.loading = true;
-
- vm.$nextTick(() => {
- expect(vm.showPreview).toBe(false);
-
- done();
- });
- });
-
- it('returns true if not loading and mainEntry exists', done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
- vm.loading = false;
-
- vm.$nextTick(() => {
- expect(vm.showPreview).toBe(true);
-
- done();
- });
- });
- });
-
- describe('showEmptyState', () => {
- it('returns true if no mainEnry exists', () => {
- expect(vm.showEmptyState).toBe(true);
- });
-
- it('returns false if loading', done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
- vm.loading = true;
-
- vm.$nextTick(() => {
- expect(vm.showEmptyState).toBe(false);
-
- done();
- });
- });
-
- it('returns false if not loading and mainEntry exists', done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
- vm.loading = false;
-
- vm.$nextTick(() => {
- expect(vm.showEmptyState).toBe(false);
-
- done();
- });
- });
- });
-
- describe('showOpenInCodeSandbox', () => {
- it('returns true when visiblity is public', () => {
- expect(vm.showOpenInCodeSandbox).toBe(true);
- });
-
- it('returns false when visiblity is private', done => {
- vm.$store.state.projects['gitlab-ce'].visibility = 'private';
-
- vm.$nextTick(() => {
- expect(vm.showOpenInCodeSandbox).toBe(false);
-
- done();
- });
- });
- });
-
- describe('sandboxOpts', () => {
- beforeEach(done => {
- vm.$store.state.entries['index.js'] = {
- ...file('index.js'),
- type: 'blob',
- raw: 'test',
- };
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
-
- vm.$nextTick(done);
- });
-
- it('returns sandbox options', () => {
- expect(vm.sandboxOpts).toEqual({
- files: {
- '/index.js': {
- code: 'test',
- },
- '/package.json': {
- code: '{"main":"index.js"}',
- },
- },
- entry: '/index.js',
- showOpenInCodeSandbox: true,
- });
- });
- });
- });
-
- describe('methods', () => {
- describe('loadFileContent', () => {
- it('calls getFileData', () => {
- expect(vm.getFileData).toHaveBeenCalledWith({
- path: 'package.json',
- makeFileActive: false,
- });
- });
-
- it('calls getRawFileData', () => {
- expect(vm.getRawFileData).toHaveBeenCalledWith({ path: 'package.json' });
- });
- });
-
- describe('update', () => {
- beforeEach(() => {
- jasmine.clock().install();
-
- vm.sandpackReady = true;
- vm.manager.updatePreview = jasmine.createSpy('updatePreview');
- vm.manager.listener = jasmine.createSpy('updatePreview');
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- it('calls initPreview if manager is empty', () => {
- spyOn(vm, 'initPreview');
- vm.manager = {};
-
- vm.update();
-
- jasmine.clock().tick(250);
-
- expect(vm.initPreview).toHaveBeenCalled();
- });
-
- it('calls updatePreview', () => {
- vm.update();
-
- jasmine.clock().tick(250);
-
- expect(vm.manager.updatePreview).toHaveBeenCalledWith(vm.sandboxOpts);
- });
- });
- });
-
- describe('template', () => {
- it('renders ide-preview element when showPreview is true', done => {
- Vue.set(
- vm.$store.state.entries['package.json'],
- 'raw',
- JSON.stringify({
- main: 'index.js',
- }),
- );
- vm.loading = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('#ide-preview')).not.toBe(null);
- done();
- });
- });
-
- it('renders empty state', done => {
- vm.loading = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.textContent).toContain(
- 'Preview your web application using Web IDE client-side evaluation.',
- );
-
- done();
- });
- });
-
- it('renders loading icon', done => {
- vm.loading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/monitoring/components/graph_group_spec.js b/spec/javascripts/monitoring/components/graph_group_spec.js
new file mode 100644
index 00000000000..068c4b5302c
--- /dev/null
+++ b/spec/javascripts/monitoring/components/graph_group_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import GraphGroup from '~/monitoring/components/graph_group.vue';
+
+describe('Graph group component', () => {
+ let graphGroup;
+
+ afterEach(() => {
+ graphGroup.destroy();
+ });
+
+ describe('When groups can be collapsed', () => {
+ beforeEach(() => {
+ graphGroup = shallowMount(GraphGroup, {
+ propsData: {
+ name: 'panel',
+ collapseGroup: true,
+ },
+ });
+ });
+
+ it('should show the angle-down caret icon when collapseGroup is true', () => {
+ expect(graphGroup.vm.caretIcon).toBe('angle-down');
+ });
+
+ it('should show the angle-right caret icon when collapseGroup is false', () => {
+ graphGroup.vm.collapse();
+
+ expect(graphGroup.vm.caretIcon).toBe('angle-right');
+ });
+ });
+
+ describe('When groups can not be collapsed', () => {
+ beforeEach(() => {
+ graphGroup = shallowMount(GraphGroup, {
+ propsData: {
+ name: 'panel',
+ collapseGroup: true,
+ showPanels: false,
+ },
+ });
+ });
+
+ it('should not contain a prometheus-graph-group container when showPanels is false', () => {
+ expect(graphGroup.vm.$el.querySelector('.prometheus-graph-group')).toBe(null);
+ });
+ });
+});
diff --git a/spec/lib/api/support/git_access_actor_spec.rb b/spec/lib/api/support/git_access_actor_spec.rb
new file mode 100644
index 00000000000..63f5966faea
--- /dev/null
+++ b/spec/lib/api/support/git_access_actor_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Support::GitAccessActor do
+ let(:user) { nil }
+ let(:key) { nil }
+
+ subject { described_class.new(user: user, key: key) }
+
+ describe '.from_params' do
+ context 'with params that are valid' do
+ it 'returns an instance of API::Support::GitAccessActor' do
+ params = { key_id: create(:key).id }
+
+ expect(described_class.from_params(params)).to be_instance_of(described_class)
+ end
+ end
+
+ context 'with params that are invalid' do
+ it 'returns nil' do
+ expect(described_class.from_params({})).to be_nil
+ end
+ end
+ end
+
+ describe 'attributes' do
+ describe '#user' do
+ context 'when initialized with a User' do
+ let(:user) { create(:user) }
+
+ it 'returns the User' do
+ expect(subject.user).to eq(user)
+ end
+ end
+
+ context 'when initialized with a Key' do
+ let(:user_for_key) { create(:user) }
+ let(:key) { create(:key, user: user_for_key) }
+
+ it 'returns the User associated to the Key' do
+ expect(subject.user).to eq(user_for_key)
+ end
+ end
+ end
+ end
+
+ describe '#key_or_user' do
+ context 'when params contains a :key_id' do
+ it 'is an instance of Key' do
+ key = create(:key)
+ params = { key_id: key.id }
+
+ expect(described_class.from_params(params).key_or_user).to eq(key)
+ end
+ end
+
+ context 'when params contains a :user_id' do
+ it 'is an instance of User' do
+ user = create(:user)
+ params = { user_id: user.id }
+
+ expect(described_class.from_params(params).key_or_user).to eq(user)
+ end
+ end
+
+ context 'when params contains a :username' do
+ it 'is an instance of User (via UserFinder)' do
+ user = create(:user)
+ params = { username: user.username }
+
+ expect(described_class.from_params(params).key_or_user).to eq(user)
+ end
+ end
+ end
+
+ describe '#username' do
+ context 'when initialized with a User' do
+ let(:user) { create(:user) }
+
+ it 'returns the username' do
+ expect(subject.username).to eq(user.username)
+ end
+ end
+
+ context 'when initialized with a Key' do
+ let(:key) { create(:key, user: user_for_key) }
+
+ context 'that has no User associated' do
+ let(:user_for_key) { nil }
+
+ it 'returns nil' do
+ expect(subject.username).to be_nil
+ end
+ end
+
+ context 'that has a User associated' do
+ let(:user_for_key) { create(:user) }
+
+ it 'returns the username of the User associated to the Key' do
+ expect(subject.username).to eq(user_for_key.username)
+ end
+ end
+ end
+ end
+
+ describe '#update_last_used_at!' do
+ context 'when initialized with a User' do
+ let(:user) { create(:user) }
+
+ it 'does nothing' do
+ expect(user).not_to receive(:update_last_used_at)
+
+ subject.update_last_used_at!
+ end
+ end
+
+ context 'when initialized with a Key' do
+ let(:key) { create(:key) }
+
+ it 'updates update_last_used_at' do
+ expect(key).to receive(:update_last_used_at)
+
+ subject.update_last_used_at!
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index b7f45421b2a..0c4ccbf28f4 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -36,6 +36,17 @@ describe Banzai::Filter::AssetProxyFilter do
expect(Gitlab.config.asset_proxy.whitelist).to eq %w(gitlab.com *.mydomain.com)
expect(Gitlab.config.asset_proxy.domain_regexp).to eq /^(gitlab\.com|.*?\.mydomain\.com)$/i
end
+
+ context 'when whitelist is empty' do
+ it 'defaults to the install domain' do
+ stub_application_setting(asset_proxy_enabled: true)
+ stub_application_setting(asset_proxy_whitelist: [])
+
+ described_class.initialize_settings
+
+ expect(Gitlab.config.asset_proxy.whitelist).to eq [Gitlab.config.gitlab.host]
+ end
+ end
end
context 'when properly configured' do
diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb
index a341dfa5636..b9134b8d6ab 100644
--- a/spec/lib/gitlab/checks/diff_check_spec.rb
+++ b/spec/lib/gitlab/checks/diff_check_spec.rb
@@ -20,9 +20,8 @@ describe Gitlab::Checks::DiffCheck do
allow(project).to receive(:lfs_enabled?).and_return(false)
end
- it 'skips the validation' do
- expect(subject).not_to receive(:validate_diff)
- expect(subject).not_to receive(:validate_file_paths)
+ it 'does not invoke :lfs_file_locks_validation' do
+ expect(subject).not_to receive(:lfs_file_locks_validation)
subject.validate!
end
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
index 43c5d3ec980..8fc1e0a4e88 100644
--- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -84,6 +84,20 @@ describe Gitlab::Ci::Build::Policy::Refs do
.not_to be_satisfied_by(pipeline)
end
end
+
+ context 'when source is external_pull_request_event' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :external_pull_request_event) }
+
+ it 'is satisfied with only: external_pull_request' do
+ expect(described_class.new(%w[external_pull_requests]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied with only: external_pull_request_event' do
+ expect(described_class.new(%w[external_pull_request_events]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
end
context 'when matching a ref by a regular expression' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index bf9ff922c05..ba4f841cf43 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -128,4 +128,38 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline.target_sha).to eq(merge_request.target_branch_sha)
end
end
+
+ context 'when pipeline is running for an external pull request' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :external_pull_request_event,
+ origin_ref: 'feature',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ source_sha: external_pull_request.source_sha,
+ target_sha: external_pull_request.target_sha,
+ trigger_request: nil,
+ schedule: nil,
+ external_pull_request: external_pull_request,
+ project: project,
+ current_user: user)
+ end
+
+ let(:external_pull_request) { build(:external_pull_request, project: project) }
+
+ before do
+ step.perform!
+ end
+
+ it 'correctly indicated that this is an external pull request pipeline' do
+ expect(pipeline).to be_external_pull_request_event
+ expect(pipeline.external_pull_request).to eq(external_pull_request)
+ end
+
+ it 'correctly sets source sha and target sha to pipeline' do
+ expect(pipeline.source_sha).to eq(external_pull_request.source_sha)
+ expect(pipeline.target_sha).to eq(external_pull_request.target_sha)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 89431b80be3..023d7530b4b 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
context 'is matched' do
let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
- it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
+ it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) }
end
context 'is not matched' do
@@ -541,7 +541,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to be_included }
it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'delayed', start_in: '1 day')
+ expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' })
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 91c559dcd9b..9d9a9ecda33 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -16,7 +16,10 @@ module Gitlab
let(:config) do
YAML.dump(
before_script: ['pwd'],
- rspec: { script: 'rspec' }
+ rspec: {
+ script: 'rspec',
+ interruptible: true
+ }
)
end
@@ -29,6 +32,7 @@ module Gitlab
before_script: ["pwd"],
script: ["rspec"]
},
+ interruptible: true,
allow_failure: false,
when: "on_success",
yaml_variables: []
@@ -36,6 +40,36 @@ module Gitlab
end
end
+ context 'with job rules' do
+ let(:config) do
+ YAML.dump(
+ rspec: {
+ script: 'rspec',
+ rules: [
+ { if: '$CI_COMMIT_REF_NAME == "master"' },
+ { changes: %w[README.md] }
+ ]
+ }
+ )
+ end
+
+ it 'returns valid build attributes' do
+ expect(subject).to eq({
+ stage: 'test',
+ stage_idx: 1,
+ name: 'rspec',
+ options: { script: ['rspec'] },
+ rules: [
+ { if: '$CI_COMMIT_REF_NAME == "master"' },
+ { changes: %w[README.md] }
+ ],
+ allow_failure: false,
+ when: 'on_success',
+ yaml_variables: []
+ })
+ end
+ end
+
describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
@@ -50,6 +84,32 @@ module Gitlab
end
end
+ describe 'interruptible entry' do
+ describe 'interruptible job' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec', interruptible: true })
+ end
+
+ it { expect(subject[:interruptible]).to be_truthy }
+ end
+
+ describe 'interruptible job with default value' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it { expect(subject).not_to have_key(:interruptible) }
+ end
+
+ describe 'uninterruptible job' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec', interruptible: false })
+ end
+
+ it { expect(subject[:interruptible]).to be_falsy }
+ end
+ end
+
describe 'retry entry' do
context 'when retry count is specified' do
let(:config) do
@@ -1226,7 +1286,7 @@ module Gitlab
end
end
- describe 'rules' do
+ context 'with when/rules conflict' do
subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
let(:config) do
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 2990594c538..1b4d366ce7b 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -11,16 +11,62 @@ describe Gitlab::Danger::Helper do
class FakeDanger
include Gitlab::Danger::Helper
- attr_reader :git
+ attr_reader :git, :gitlab
- def initialize(git:)
+ def initialize(git:, gitlab:)
@git = git
+ @gitlab = gitlab
end
end
let(:fake_git) { double('fake-git') }
- subject(:helper) { FakeDanger.new(git: fake_git) }
+ let(:mr_author) { nil }
+ let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) }
+
+ subject(:helper) { FakeDanger.new(git: fake_git, gitlab: fake_gitlab) }
+
+ describe '#gitlab_helper' do
+ context 'when gitlab helper is not available' do
+ let(:fake_gitlab) { nil }
+
+ it 'returns nil' do
+ expect(helper.gitlab_helper).to be_nil
+ end
+ end
+
+ context 'when gitlab helper is available' do
+ it 'returns the gitlab helper' do
+ expect(helper.gitlab_helper).to eq(fake_gitlab)
+ end
+ end
+ end
+
+ describe '#release_automation?' do
+ context 'when gitlab helper is not available' do
+ it 'returns false' do
+ expect(helper.release_automation?).to be_falsey
+ end
+ end
+
+ context 'when gitlab helper is available' do
+ context "but the MR author isn't the RELEASE_TOOLS_BOT" do
+ let(:mr_author) { 'johnmarston' }
+
+ it 'returns false' do
+ expect(helper.release_automation?).to be_falsey
+ end
+ end
+
+ context 'and the MR author is the RELEASE_TOOLS_BOT' do
+ let(:mr_author) { described_class::RELEASE_TOOLS_BOT }
+
+ it 'returns true' do
+ expect(helper.release_automation?).to be_truthy
+ end
+ end
+ end
+ end
describe '#all_changed_files' do
subject { helper.all_changed_files }
@@ -168,9 +214,13 @@ describe Gitlab::Danger::Helper do
'Gemfile.lock' | :backend
'Procfile' | :backend
'Rakefile' | :backend
- '.gitlab-ci.yml' | :backend
'FOO_VERSION' | :backend
+ '.gitlab-ci.yml' | :engineering_productivity
+ '.gitlab/ci/cng.gitlab-ci.yml' | :engineering_productivity
+ '.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | :engineering_productivity
+ 'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | :backend
+
'ee/FOO_VERSION' | :unknown
'db/schema.rb' | :database
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index afbc3896a70..ca036390bde 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -65,6 +65,18 @@ describe Gitlab::Danger::Teammate do
expect(subject.reviewer?(project, :test, labels)).to be_falsey
end
end
+
+ context 'when role is Backend Engineer, Engineering Productivity' do
+ let(:role) { 'Backend Engineer, Engineering Productivity' }
+
+ it '#reviewer? returns true' do
+ expect(subject.reviewer?(project, :engineering_productivity, labels)).to be_truthy
+ end
+
+ it '#maintainer? returns false' do
+ expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_falsey
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index cff4eb398bf..49f92f14559 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1283,33 +1283,19 @@ describe Gitlab::Database::MigrationHelpers do
describe '#perform_background_migration_inline?' do
it 'returns true in a test environment' do
- allow(Rails.env)
- .to receive(:test?)
- .and_return(true)
+ stub_rails_env('test')
expect(model.perform_background_migration_inline?).to eq(true)
end
it 'returns true in a development environment' do
- allow(Rails.env)
- .to receive(:test?)
- .and_return(false)
-
- allow(Rails.env)
- .to receive(:development?)
- .and_return(true)
+ stub_rails_env('development')
expect(model.perform_background_migration_inline?).to eq(true)
end
it 'returns false in a production environment' do
- allow(Rails.env)
- .to receive(:test?)
- .and_return(false)
-
- allow(Rails.env)
- .to receive(:development?)
- .and_return(false)
+ stub_rails_env('production')
expect(model.perform_background_migration_inline?).to eq(false)
end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 63c26e29d73..d221f39c2ed 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
RSpec.describe Gitlab::Favicon, :request_store do
- include RailsHelpers
-
describe '.main' do
it 'defaults to favicon.png' do
stub_rails_env('production')
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 99d563e03ec..1c5f72a4396 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -265,7 +265,7 @@ describe Gitlab::GitalyClient do
context 'in production and when RequestStore is enabled', :request_store do
before do
- allow(Rails.env).to receive(:production?).and_return(true)
+ stub_rails_env('production')
end
context 'when the maximum number of calls is enforced by a feature flag' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 47ba7eff8ed..e496ab4cd35 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -23,6 +23,7 @@ issues:
- epic_issue
- epic
- designs
+- design_versions
events:
- author
- project
@@ -126,6 +127,8 @@ merge_requests:
- blocks_as_blockee
- blocking_merge_requests
- blocked_merge_requests
+external_pull_requests:
+- project
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -155,6 +158,7 @@ ci_pipelines:
- pipeline_schedule
- merge_requests_as_head_pipeline
- merge_request
+- external_pull_request
- deployments
- environments
- chat_data
@@ -372,6 +376,7 @@ project:
- index_status
- feature_usage
- approval_rules
+- approval_merge_request_rules
- approvers
- approver_users
- pages_domains
@@ -402,6 +407,7 @@ project:
- merge_trains
- designs
- project_aliases
+- external_pull_requests
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index fef84c87509..cc8ca1d87e3 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -12,7 +12,7 @@ describe 'Import/Export attribute configuration' do
let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys }
let(:relation_names) do
- names = names_from_tree(config_hash['project_tree'])
+ names = names_from_tree(config_hash.dig('tree', 'project'))
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
new file mode 100644
index 00000000000..3cbc1375d6e
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::ImportExport::AttributesFinder do
+ describe '#find_root' do
+ subject { described_class.new(config: config).find_root(model_key) }
+
+ let(:test_config) { 'spec/support/import_export/import_export.yml' }
+ let(:config) { Gitlab::ImportExport::Config.new.to_h }
+ let(:model_key) { :project }
+
+ let(:project_tree_hash) do
+ {
+ except: [:id, :created_at],
+ include: [
+ { issues: { include: [] } },
+ { labels: { include: [] } },
+ { merge_requests: {
+ except: [:iid],
+ include: [
+ { merge_request_diff: {
+ include: [],
+ preload: { source_project: nil }
+ } },
+ { merge_request_test: { include: [] } }
+ ],
+ only: [:id],
+ preload: {
+ merge_request_diff: { source_project: nil },
+ merge_request_test: nil
+ }
+ } },
+ { commit_statuses: {
+ include: [{ commit: { include: [] } }],
+ preload: { commit: nil }
+ } },
+ { project_members: {
+ include: [{ user: { include: [],
+ only: [:email] } }],
+ preload: { user: nil }
+ } }
+ ],
+ preload: {
+ commit_statuses: {
+ commit: nil
+ },
+ issues: nil,
+ labels: nil,
+ merge_requests: {
+ merge_request_diff: { source_project: nil },
+ merge_request_test: nil
+ },
+ project_members: {
+ user: nil
+ }
+ }
+ }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
+ end
+
+ it 'generates hash from project tree config' do
+ is_expected.to match(project_tree_hash)
+ end
+
+ context 'individual scenarios' do
+ it 'generates the correct hash for a single project relation' do
+ setup_yaml(tree: { project: [:issues] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a single project feature relation' do
+ setup_yaml(tree: { project: [:project_feature] })
+
+ is_expected.to match(
+ include: [{ project_feature: { include: [] } }],
+ preload: { project_feature: nil }
+ )
+ end
+
+ it 'generates the correct hash for a multiple project relation' do
+ setup_yaml(tree: { project: [:issues, :snippets] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [] } },
+ { snippets: { include: [] } }],
+ preload: { issues: nil, snippets: nil }
+ )
+ end
+
+ it 'generates the correct hash for a single sub-relation' do
+ setup_yaml(tree: { project: [issues: [:notes]] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [{ notes: { include: [] } }],
+ preload: { notes: nil } } }],
+ preload: { issues: { notes: nil } }
+ )
+ end
+
+ it 'generates the correct hash for a multiple sub-relation' do
+ setup_yaml(tree: { project: [merge_requests: [:notes, :merge_request_diff]] })
+
+ is_expected.to match(
+ include: [{ merge_requests:
+ { include: [{ notes: { include: [] } },
+ { merge_request_diff: { include: [] } }],
+ preload: { merge_request_diff: nil, notes: nil } } }],
+ preload: { merge_requests: { merge_request_diff: nil, notes: nil } }
+ )
+ end
+
+ it 'generates the correct hash for a sub-relation with another sub-relation' do
+ setup_yaml(tree: { project: [merge_requests: [notes: [:author]]] })
+
+ is_expected.to match(
+ include: [{ merge_requests: {
+ include: [{ notes: { include: [{ author: { include: [] } }],
+ preload: { author: nil } } }],
+ preload: { notes: { author: nil } }
+ } }],
+ preload: { merge_requests: { notes: { author: nil } } }
+ )
+ end
+
+ it 'generates the correct hash for a relation with included attributes' do
+ setup_yaml(tree: { project: [:issues] },
+ included_attributes: { issues: [:name, :description] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [],
+ only: [:name, :description] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a relation with excluded attributes' do
+ setup_yaml(tree: { project: [:issues] },
+ excluded_attributes: { issues: [:name] })
+
+ is_expected.to match(
+ include: [{ issues: { except: [:name],
+ include: [] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a relation with both excluded and included attributes' do
+ setup_yaml(tree: { project: [:issues] },
+ excluded_attributes: { issues: [:name] },
+ included_attributes: { issues: [:description] })
+
+ is_expected.to match(
+ include: [{ issues: { except: [:name],
+ include: [],
+ only: [:description] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a relation with custom methods' do
+ setup_yaml(tree: { project: [:issues] },
+ methods: { issues: [:name] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [],
+ methods: [:name] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ def setup_yaml(hash)
+ allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
+ end
+ end
+ end
+
+ describe '#find_relations_tree' do
+ subject { described_class.new(config: config).find_relations_tree(model_key) }
+
+ let(:tree) { { project: { issues: {} } } }
+ let(:model_key) { :project }
+
+ context 'when initialized with config including tree' do
+ let(:config) { { tree: tree } }
+
+ context 'when relation is in top-level keys of the tree' do
+ it { is_expected.to eq({ issues: {} }) }
+ end
+
+ context 'when the relation is not in top-level keys' do
+ let(:model_key) { :issues }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when tree is not present in config' do
+ let(:config) { {} }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#find_excluded_keys' do
+ subject { described_class.new(config: config).find_excluded_keys(klass_name) }
+
+ let(:klass_name) { 'project' }
+
+ context 'when initialized with excluded_attributes' do
+ let(:config) { { excluded_attributes: excluded_attributes } }
+ let(:excluded_attributes) { { project: [:name, :path], issues: [:milestone_id] } }
+
+ it { is_expected.to eq(%w[name path]) }
+ end
+
+ context 'when excluded_attributes are not present in config' do
+ let(:config) { {} }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb
index cf396dba382..f09a29b84db 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -1,163 +1,177 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
describe Gitlab::ImportExport::Config do
let(:yaml_file) { described_class.new }
describe '#to_h' do
- context 'when using CE' do
- before do
- allow(yaml_file)
- .to receive(:merge?)
- .and_return(false)
+ subject { yaml_file.to_h }
+
+ context 'when using default config' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ee) do
+ [true, false]
end
- it 'just returns the parsed Hash without the EE section' do
- expected = YAML.load_file(Gitlab::ImportExport.config_file)
- expected.delete('ee')
+ with_them do
+ before do
+ allow(Gitlab).to receive(:ee?) { ee }
+ end
- expect(yaml_file.to_h).to eq(expected)
+ it 'parses default config' do
+ expect { subject }.not_to raise_error
+ expect(subject).to be_a(Hash)
+ expect(subject.keys).to contain_exactly(
+ :tree, :excluded_attributes, :included_attributes, :methods, :preloads)
+ end
end
end
- context 'when using EE' do
- before do
- allow(yaml_file)
- .to receive(:merge?)
- .and_return(true)
- end
+ context 'when using custom config' do
+ let(:config) do
+ <<-EOF.strip_heredoc
+ tree:
+ project:
+ - labels:
+ - :priorities
+ - milestones:
+ - events:
+ - :push_event_payload
- it 'merges the EE project tree into the CE project tree' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [
- {
- 'issues' => [
- :id,
- :title,
- { 'notes' => [:id, :note, { 'author' => [:name] }] }
- ]
- }
- ],
- 'ee' => {
- 'project_tree' => [
- {
- 'issues' => [
- :description,
- { 'notes' => [:date, { 'author' => [:email] }] }
- ]
- },
- { 'foo' => [{ 'bar' => %i[baz] }] }
- ]
- }
- })
+ included_attributes:
+ user:
+ - :id
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [
- {
- 'issues' => [
- :id,
- :title,
- {
- 'notes' => [
- :id,
- :note,
- { 'author' => [:name, :email] },
- :date
- ]
- },
- :description
- ]
- },
- { 'foo' => [{ 'bar' => %i[baz] }] }
- ]
- })
+ excluded_attributes:
+ project:
+ - :name
+
+ methods:
+ labels:
+ - :type
+ events:
+ - :action
+
+ preloads:
+ statuses:
+ project:
+
+ ee:
+ tree:
+ project:
+ protected_branches:
+ - :unprotect_access_levels
+ included_attributes:
+ user:
+ - :name_ee
+ excluded_attributes:
+ project:
+ - :name_without_ee
+ methods:
+ labels:
+ - :type_ee
+ events_ee:
+ - :action_ee
+ preloads:
+ statuses:
+ bridge_ee:
+ EOF
end
- it 'merges the excluded attributes list' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [],
- 'excluded_attributes' => {
- 'project' => %i[id title],
- 'notes' => %i[id]
- },
- 'ee' => {
- 'project_tree' => [],
- 'excluded_attributes' => {
- 'project' => %i[date],
- 'foo' => %i[bar baz]
- }
- }
- })
-
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [],
- 'excluded_attributes' => {
- 'project' => %i[id title date],
- 'notes' => %i[id],
- 'foo' => %i[bar baz]
- }
- })
+ let(:config_hash) { YAML.safe_load(config, [Symbol]) }
+
+ before do
+ allow_any_instance_of(described_class).to receive(:parse_yaml) do
+ config_hash.deep_dup
+ end
end
- it 'merges the included attributes list' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [],
- 'included_attributes' => {
- 'project' => %i[id title],
- 'notes' => %i[id]
- },
- 'ee' => {
- 'project_tree' => [],
- 'included_attributes' => {
- 'project' => %i[date],
- 'foo' => %i[bar baz]
+ context 'when using CE' do
+ before do
+ allow(Gitlab).to receive(:ee?) { false }
+ end
+
+ it 'just returns the normalized Hash' do
+ is_expected.to eq(
+ {
+ tree: {
+ project: {
+ labels: {
+ priorities: {}
+ },
+ milestones: {
+ events: {
+ push_event_payload: {}
+ }
+ }
+ }
+ },
+ included_attributes: {
+ user: [:id]
+ },
+ excluded_attributes: {
+ project: [:name]
+ },
+ methods: {
+ labels: [:type],
+ events: [:action]
+ },
+ preloads: {
+ statuses: {
+ project: nil
+ }
}
}
- })
-
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [],
- 'included_attributes' => {
- 'project' => %i[id title date],
- 'notes' => %i[id],
- 'foo' => %i[bar baz]
- }
- })
+ )
+ end
end
- it 'merges the methods list' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [],
- 'methods' => {
- 'project' => %i[id title],
- 'notes' => %i[id]
- },
- 'ee' => {
- 'project_tree' => [],
- 'methods' => {
- 'project' => %i[date],
- 'foo' => %i[bar baz]
+ context 'when using EE' do
+ before do
+ allow(Gitlab).to receive(:ee?) { true }
+ end
+
+ it 'just returns the normalized Hash' do
+ is_expected.to eq(
+ {
+ tree: {
+ project: {
+ labels: {
+ priorities: {}
+ },
+ milestones: {
+ events: {
+ push_event_payload: {}
+ }
+ },
+ protected_branches: {
+ unprotect_access_levels: {}
+ }
+ }
+ },
+ included_attributes: {
+ user: [:id, :name_ee]
+ },
+ excluded_attributes: {
+ project: [:name, :name_without_ee]
+ },
+ methods: {
+ labels: [:type, :type_ee],
+ events: [:action],
+ events_ee: [:action_ee]
+ },
+ preloads: {
+ statuses: {
+ project: nil,
+ bridge_ee: nil
+ }
}
}
- })
-
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [],
- 'methods' => {
- 'project' => %i[id title date],
- 'notes' => %i[id],
- 'foo' => %i[bar baz]
- }
- })
+ )
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
new file mode 100644
index 00000000000..d23b27c9d8e
--- /dev/null
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -0,0 +1,272 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::FastHashSerializer do
+ subject { described_class.new(project, tree).execute }
+
+ let!(:project) { setup_project }
+ let(:user) { create(:user) }
+ let(:shared) { project.import_export_shared }
+ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
+ let(:tree) { reader.project_tree }
+
+ before do
+ project.add_maintainer(user)
+ allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD')
+ allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA')
+ end
+
+ it 'saves the correct hash' do
+ is_expected.to include({ 'description' => 'description', 'visibility_level' => 20 })
+ end
+
+ it 'has approvals_before_merge set' do
+ expect(subject['approvals_before_merge']).to eq(1)
+ end
+
+ it 'has milestones' do
+ expect(subject['milestones']).not_to be_empty
+ end
+
+ it 'has merge requests' do
+ expect(subject['merge_requests']).not_to be_empty
+ end
+
+ it 'has merge request\'s milestones' do
+ expect(subject['merge_requests'].first['milestone']).not_to be_empty
+ end
+
+ it 'has merge request\'s source branch SHA' do
+ expect(subject['merge_requests'].first['source_branch_sha']).to eq('ABCD')
+ end
+
+ it 'has merge request\'s target branch SHA' do
+ expect(subject['merge_requests'].first['target_branch_sha']).to eq('DCBA')
+ end
+
+ it 'has events' do
+ expect(subject['merge_requests'].first['milestone']['events']).not_to be_empty
+ end
+
+ it 'has snippets' do
+ expect(subject['snippets']).not_to be_empty
+ end
+
+ it 'has snippet notes' do
+ expect(subject['snippets'].first['notes']).not_to be_empty
+ end
+
+ it 'has releases' do
+ expect(subject['releases']).not_to be_empty
+ end
+
+ it 'has no author on releases' do
+ expect(subject['releases'].first['author']).to be_nil
+ end
+
+ it 'has the author ID on releases' do
+ expect(subject['releases'].first['author_id']).not_to be_nil
+ end
+
+ it 'has issues' do
+ expect(subject['issues']).not_to be_empty
+ end
+
+ it 'has issue comments' do
+ notes = subject['issues'].first['notes']
+
+ expect(notes).not_to be_empty
+ expect(notes.first['type']).to eq('DiscussionNote')
+ end
+
+ it 'has issue assignees' do
+ expect(subject['issues'].first['issue_assignees']).not_to be_empty
+ end
+
+ it 'has author on issue comments' do
+ expect(subject['issues'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has project members' do
+ expect(subject['project_members']).not_to be_empty
+ end
+
+ it 'has merge requests diffs' do
+ expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty
+ end
+
+ it 'has merge request diff files' do
+ expect(subject['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty
+ end
+
+ it 'has merge request diff commits' do
+ expect(subject['merge_requests'].first['merge_request_diff']['merge_request_diff_commits']).not_to be_empty
+ end
+
+ it 'has merge requests comments' do
+ expect(subject['merge_requests'].first['notes']).not_to be_empty
+ end
+
+ it 'has author on merge requests comments' do
+ expect(subject['merge_requests'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has pipeline stages' do
+ expect(subject.dig('ci_pipelines', 0, 'stages')).not_to be_empty
+ end
+
+ it 'has pipeline statuses' do
+ expect(subject.dig('ci_pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty
+ end
+
+ it 'has pipeline builds' do
+ builds_count = subject
+ .dig('ci_pipelines', 0, 'stages', 0, 'statuses')
+ .count { |hash| hash['type'] == 'Ci::Build' }
+
+ expect(builds_count).to eq(1)
+ end
+
+ it 'has no when YML attributes but only the DB column' do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:ci_yaml_file)
+ .and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
+
+ expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
+
+ subject
+ end
+
+ it 'has pipeline commits' do
+ expect(subject['ci_pipelines']).not_to be_empty
+ end
+
+ it 'has ci pipeline notes' do
+ expect(subject['ci_pipelines'].first['notes']).not_to be_empty
+ end
+
+ it 'has labels with no associations' do
+ expect(subject['labels']).not_to be_empty
+ end
+
+ it 'has labels associated to records' do
+ expect(subject['issues'].first['label_links'].first['label']).not_to be_empty
+ end
+
+ it 'has project and group labels' do
+ label_types = subject['issues'].first['label_links'].map { |link| link['label']['type'] }
+
+ expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
+ end
+
+ it 'has priorities associated to labels' do
+ priorities = subject['issues'].first['label_links'].flat_map { |link| link['label']['priorities'] }
+
+ expect(priorities).not_to be_empty
+ end
+
+ it 'has issue resource label events' do
+ expect(subject['issues'].first['resource_label_events']).not_to be_empty
+ end
+
+ it 'has merge request resource label events' do
+ expect(subject['merge_requests'].first['resource_label_events']).not_to be_empty
+ end
+
+ it 'saves the correct service type' do
+ expect(subject['services'].first['type']).to eq('CustomIssueTrackerService')
+ end
+
+ it 'saves the properties for a service' do
+ expect(subject['services'].first['properties']).to eq('one' => 'value')
+ end
+
+ it 'has project feature' do
+ project_feature = subject['project_feature']
+ expect(project_feature).not_to be_empty
+ expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED)
+ expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED)
+ expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
+ end
+
+ it 'has custom attributes' do
+ expect(subject['custom_attributes'].count).to eq(2)
+ end
+
+ it 'has badges' do
+ expect(subject['project_badges'].count).to eq(2)
+ end
+
+ it 'does not complain about non UTF-8 characters in MR diff files' do
+ ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
+
+ expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty
+ end
+
+ context 'project attributes' do
+ it 'does not contain the runners token' do
+ expect(subject).not_to include("runners_token" => 'token')
+ end
+ end
+
+ it 'has a board and a list' do
+ expect(subject['boards'].first['lists']).not_to be_empty
+ end
+
+ def setup_project
+ issue = create(:issue, assignees: [user])
+ snippet = create(:project_snippet)
+ release = create(:release)
+ group = create(:group)
+
+ project = create(:project,
+ :public,
+ :repository,
+ :issues_disabled,
+ :wiki_enabled,
+ :builds_private,
+ description: 'description',
+ issues: [issue],
+ snippets: [snippet],
+ releases: [release],
+ group: group,
+ approvals_before_merge: 1
+ )
+ project_label = create(:label, project: project)
+ group_label = create(:group_label, group: group)
+ create(:label_link, label: project_label, target: issue)
+ create(:label_link, label: group_label, target: issue)
+ create(:label_priority, label: group_label, priority: 1)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
+
+ ci_build = create(:ci_build, project: project, when: nil)
+ ci_build.pipeline.update(project: project)
+ create(:commit_status, project: project, pipeline: ci_build.pipeline)
+
+ create(:milestone, project: project)
+ create(:discussion_note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_build.pipeline.sha)
+
+ create(:resource_label_event, label: project_label, issue: issue)
+ create(:resource_label_event, label: group_label, merge_request: merge_request)
+
+ create(:event, :created, target: milestone, project: project, author: user)
+ create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' })
+
+ create(:project_custom_attribute, project: project)
+ create(:project_custom_attribute, project: project)
+
+ create(:project_badge, project: project)
+ create(:project_badge, project: project)
+
+ board = create(:board, project: project, name: 'TestBoard')
+ create(:list, board: board, position: 0, label: project_label)
+
+ project
+ end
+end
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 5ed9fef1597..3442e22c11f 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -8,7 +8,7 @@ describe 'Import/Export model configuration' do
let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys }
let(:model_names) do
- names = names_from_tree(config_hash['project_tree'])
+ names = names_from_tree(config_hash.dig('tree', 'project'))
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 0aef4887c75..87be7857e67 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -512,6 +512,24 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
end
end
+
+ context 'with external authorization classification labels' do
+ it 'converts empty external classification authorization labels to nil' do
+ project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } })
+
+ restored_project_json
+
+ expect(project.external_authorization_classification_label).to be_nil
+ end
+
+ it 'preserves valid external classification authorization labels' do
+ project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } })
+
+ restored_project_json
+
+ expect(project.external_authorization_classification_label).to eq("foobar")
+ end
+ end
end
describe '#restored_project' do
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index fefbed93316..ff46e062a5d 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -23,12 +23,65 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(project_tree_saver.save).to be true
end
+ context ':export_fast_serialize feature flag checks' do
+ before do
+ expect(Gitlab::ImportExport::Reader).to receive(:new).with(shared: shared).and_return(reader)
+ expect(reader).to receive(:project_tree).and_return(project_tree)
+ end
+
+ let(:serializer) { instance_double('Gitlab::ImportExport::FastHashSerializer') }
+ let(:reader) { instance_double('Gitlab::ImportExport::Reader') }
+ let(:project_tree) do
+ {
+ include: [{ issues: { include: [] } }],
+ preload: { issues: nil }
+ }
+ end
+
+ context 'when :export_fast_serialize feature is enabled' do
+ before do
+ stub_feature_flags(export_fast_serialize: true)
+ end
+
+ it 'uses FastHashSerializer' do
+ expect(Gitlab::ImportExport::FastHashSerializer)
+ .to receive(:new)
+ .with(project, project_tree)
+ .and_return(serializer)
+
+ expect(serializer).to receive(:execute)
+
+ project_tree_saver.save
+ end
+ end
+
+ context 'when :export_fast_serialize feature is disabled' do
+ before do
+ stub_feature_flags(export_fast_serialize: false)
+ end
+
+ it 'is serialized via built-in `as_json`' do
+ expect(project).to receive(:as_json).with(project_tree)
+
+ project_tree_saver.save
+ end
+ end
+ end
+
+ # It is mostly duplicated in
+ # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb`
+ # except:
+ # context 'with description override' do
+ # context 'group members' do
+ # ^ These are specific for the ProjectTreeSaver
context 'JSON' do
let(:saved_project_json) do
project_tree_saver.save
project_json(project_tree_saver.full_path)
end
+ # It is not duplicated in
+ # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb`
context 'with description override' do
let(:params) { { description: 'Foo Bar' } }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared, params: params) }
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index f93ff074770..87f665bd995 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -2,96 +2,45 @@ require 'spec_helper'
describe Gitlab::ImportExport::Reader do
let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
- let(:test_config) { 'spec/support/import_export/import_export.yml' }
- let(:project_tree_hash) do
- {
- except: [:id, :created_at],
- include: [:issues, :labels,
- { merge_requests: {
- only: [:id],
- except: [:iid],
- include: [:merge_request_diff, :merge_request_test]
- } },
- { commit_statuses: { include: :commit } },
- { project_members: { include: { user: { only: [:email] } } } }]
- }
- end
-
- before do
- allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
- end
-
- it 'generates hash from project tree config' do
- expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash)
- end
-
- context 'individual scenarios' do
- it 'generates the correct hash for a single project relation' do
- setup_yaml(project_tree: [:issues])
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
- end
-
- it 'generates the correct hash for a single project feature relation' do
- setup_yaml(project_tree: [:project_feature])
- expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature])
- end
+ describe '#project_tree' do
+ subject { described_class.new(shared: shared).project_tree }
- it 'generates the correct hash for a multiple project relation' do
- setup_yaml(project_tree: [:issues, :snippets])
+ it 'delegates to AttributesFinder#find_root' do
+ expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
+ .to receive(:find_root)
+ .with(:project)
- expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets])
+ subject
end
- it 'generates the correct hash for a single sub-relation' do
- setup_yaml(project_tree: [issues: [:notes]])
+ context 'when exception raised' do
+ before do
+ expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
+ .to receive(:find_root)
+ .with(:project)
+ .and_raise(StandardError)
+ end
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }])
- end
-
- it 'generates the correct hash for a multiple sub-relation' do
- setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]])
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }])
- end
+ it { is_expected.to be false }
- it 'generates the correct hash for a sub-relation with another sub-relation' do
- setup_yaml(project_tree: [merge_requests: [notes: :author]])
+ it 'logs the error' do
+ expect(shared).to receive(:error).with(instance_of(StandardError))
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }])
+ subject
+ end
end
+ end
- it 'generates the correct hash for a relation with included attributes' do
- setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }])
- end
-
- it 'generates the correct hash for a relation with excluded attributes' do
- setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }])
- end
-
- it 'generates the correct hash for a relation with both excluded and included attributes' do
- setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }])
- end
-
- it 'generates the correct hash for a relation with custom methods' do
- setup_yaml(project_tree: [:issues], methods: { issues: [:name] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
- end
+ describe '#group_members_tree' do
+ subject { described_class.new(shared: shared).group_members_tree }
- it 'generates the correct hash for group members' do
- expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } })
- end
+ it 'delegates to AttributesFinder#find_root' do
+ expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
+ .to receive(:find_root)
+ .with(:group_members)
- def setup_yaml(hash)
- allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
+ subject
end
end
end
diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
index 15748407f0c..17bb5bcc155 100644
--- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:user) { create(:admin) }
let(:group) { create(:group, :nested) }
- let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let!(:project) { create(:project, :builds_disabled, :issues_disabled, group: group, name: 'project', path: 'project') }
let(:shared) { project.import_export_shared }
before do
@@ -24,7 +24,6 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:import_path) { 'spec/lib/gitlab/import_export' }
let(:file_content) { IO.read("#{import_path}/project.json") }
let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
- let(:tree_hash) { project_tree_restorer.instance_variable_get(:@tree_hash) }
before do
allow(shared).to receive(:export_path).and_return(import_path)
@@ -92,21 +91,25 @@ describe Gitlab::ImportExport::RelationRenameService do
end
context 'when exporting' do
- let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: user, shared: shared) }
- let(:project_tree) { project_tree_saver.send(:project_json) }
+ let(:export_content_path) { project_tree_saver.full_path }
+ let(:export_content_hash) { ActiveSupport::JSON.decode(File.read(export_content_path)) }
+ let(:injected_hash) { renames.values.product([{}]).to_h }
- it 'adds old relationships to the exported file' do
- project_tree.merge!(renames.values.map { |new_name| [new_name, []] }.to_h)
+ let(:project_tree_saver) do
+ Gitlab::ImportExport::ProjectTreeSaver.new(
+ project: project, current_user: user, shared: shared)
+ end
- allow(project_tree_saver).to receive(:save) do |arg|
- project_tree_saver.send(:project_json_tree)
+ it 'adds old relationships to the exported file' do
+ # we inject relations with new names that should be rewritten
+ expect(project_tree_saver).to receive(:serialize_project_tree).and_wrap_original do |method, *args|
+ method.call(*args).merge(injected_hash)
end
- result = project_tree_saver.save
-
- saved_data = ActiveSupport::JSON.decode(result)
+ expect(project_tree_saver.save).to eq(true)
- expect(saved_data.keys).to include(*(renames.keys + renames.values))
+ expect(export_content_hash.keys).to include(*renames.keys)
+ expect(export_content_hash.keys).to include(*renames.values)
end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 516e62c4728..e9750d23c53 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -270,6 +270,7 @@ Ci::Pipeline:
- protected
- iid
- merge_request_id
+- external_pull_request_id
Ci::Stage:
- id
- name
@@ -329,6 +330,7 @@ CommitStatus:
- failure_reason
- scheduled_at
- upstream_pipeline_id
+- interruptible
Ci::Variable:
- id
- project_id
@@ -467,6 +469,7 @@ ProtectedBranch:
- name
- created_at
- updated_at
+- code_owner_approval_required
ProtectedTag:
- id
- project_id
@@ -713,3 +716,16 @@ List:
- updated_at
- milestone_id
- user_id
+ExternalPullRequest:
+- id
+- created_at
+- updated_at
+- project_id
+- pull_request_iid
+- status
+- source_branch
+- target_branch
+- source_repository
+- target_repository
+- source_sha
+- target_sha
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index e70fde09edd..45e74597a2e 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -204,17 +204,17 @@ describe Gitlab::Metrics::Transaction do
end
it 'allows tracking of custom tags' do
- transaction.add_event(:meow, animal: 'cat')
+ transaction.add_event(:bau, animal: 'dog')
- expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ expect(metric.tags).to eq(event: :bau, animal: 'dog')
end
context 'with sensitive tags' do
before do
- transaction.add_event(:meow, **sensitive_tags.merge(sane: 'yes'))
+ transaction.add_event(:baubau, **sensitive_tags.merge(sane: 'yes'))
end
- it_behaves_like 'tag filter', event: :meow, sane: 'yes'
+ it_behaves_like 'tag filter', event: :baubau, sane: 'yes'
end
end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 2b35f07cc0d..21a762dbf25 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -253,11 +253,11 @@ describe Gitlab::Metrics::WebTransaction do
end
it 'allows tracking of custom tags' do
- transaction.add_event(:meow, animal: 'cat')
+ transaction.add_event(:bau, animal: 'dog')
metric = transaction.metrics[0]
- expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ expect(metric.tags).to eq(event: :bau, animal: 'dog')
end
end
end
diff --git a/spec/lib/gitlab/pages_spec.rb b/spec/lib/gitlab/pages_spec.rb
new file mode 100644
index 00000000000..affa2ebab2a
--- /dev/null
+++ b/spec/lib/gitlab/pages_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Pages do
+ let(:pages_shared_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
+
+ before do
+ allow(described_class).to receive(:secret).and_return(pages_shared_secret)
+ end
+
+ describe '.verify_api_request' do
+ let(:payload) { { 'iss' => 'gitlab-pages' } }
+
+ it 'returns false if fails to validate the JWT' do
+ encoded_token = JWT.encode(payload, 'wrongsecret', 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers)).to eq(false)
+ end
+
+ it 'returns the decoded JWT' do
+ encoded_token = JWT.encode(payload, described_class.secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers)).to eq([{ "iss" => "gitlab-pages" }, { "alg" => "HS256" }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/patch/chronic_duration_spec.rb b/spec/lib/gitlab/patch/chronic_duration_spec.rb
new file mode 100644
index 00000000000..541037ec1a2
--- /dev/null
+++ b/spec/lib/gitlab/patch/chronic_duration_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Patch::ChronicDuration do
+ subject { ChronicDuration.parse('1mo') }
+
+ it 'uses default conversions' do
+ expect(subject).to eq(2_592_000)
+ end
+
+ context 'with custom conversions' do
+ before do
+ ChronicDuration.hours_per_day = 8
+ ChronicDuration.days_per_week = 5
+ end
+
+ after do
+ ChronicDuration.hours_per_day = 24
+ ChronicDuration.days_per_week = 7
+ end
+
+ it 'uses custom conversions' do
+ expect(subject).to eq(576_000)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 0dbfcf96124..e0b9581c75c 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -4,6 +4,8 @@
require 'spec_helper'
describe Gitlab::ProjectSearchResults do
+ include SearchHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { 'hello world' }
@@ -31,10 +33,10 @@ describe Gitlab::ProjectSearchResults do
where(:scope, :count_method, :expected) do
'blobs' | :blobs_count | '1234'
- 'notes' | :limited_notes_count | '1000+'
+ 'notes' | :limited_notes_count | max_limited_count
'wiki_blobs' | :wiki_blobs_count | '1234'
'commits' | :commits_count | '1234'
- 'projects' | :limited_projects_count | '1000+'
+ 'projects' | :limited_projects_count | max_limited_count
'unknown' | nil | nil
end
diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb
index 39d5a575efc..4e906314b5a 100644
--- a/spec/lib/gitlab/query_limiting/transaction_spec.rb
+++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb
@@ -86,9 +86,7 @@ describe Gitlab::QueryLimiting::Transaction do
it 'returns false in a production environment' do
transaction = described_class.new
- expect(Rails.env)
- .to receive(:test?)
- .and_return(false)
+ stub_rails_env('production')
expect(transaction.raise_error?).to eq(false)
end
diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb
index f0d0340cd6e..e9c6bbc35a3 100644
--- a/spec/lib/gitlab/query_limiting_spec.rb
+++ b/spec/lib/gitlab/query_limiting_spec.rb
@@ -9,14 +9,14 @@ describe Gitlab::QueryLimiting do
end
it 'returns true in a development environment' do
- allow(Rails.env).to receive(:development?).and_return(true)
+ stub_rails_env('development')
+ stub_rails_env('development')
expect(described_class.enable?).to eq(true)
end
it 'returns false on GitLab.com' do
- expect(Rails.env).to receive(:development?).and_return(false)
- expect(Rails.env).to receive(:test?).and_return(false)
+ stub_rails_env('production')
allow(Gitlab).to receive(:com?).and_return(true)
expect(described_class.enable?).to eq(false)
@@ -24,8 +24,7 @@ describe Gitlab::QueryLimiting do
it 'returns false in a non GitLab.com' do
allow(Gitlab).to receive(:com?).and_return(false)
- expect(Rails.env).to receive(:development?).and_return(false)
- expect(Rails.env).to receive(:test?).and_return(false)
+ stub_rails_env('production')
expect(described_class.enable?).to eq(false)
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 5621c686b8a..26cba53502d 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::SearchResults do
include ProjectForksHelper
+ include SearchHelpers
let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
@@ -35,11 +36,11 @@ describe Gitlab::SearchResults do
using RSpec::Parameterized::TableSyntax
where(:scope, :count_method, :expected) do
- 'projects' | :limited_projects_count | '1000+'
- 'issues' | :limited_issues_count | '1000+'
- 'merge_requests' | :limited_merge_requests_count | '1000+'
- 'milestones' | :limited_milestones_count | '1000+'
- 'users' | :limited_users_count | '1000+'
+ 'projects' | :limited_projects_count | max_limited_count
+ 'issues' | :limited_issues_count | max_limited_count
+ 'merge_requests' | :limited_merge_requests_count | max_limited_count
+ 'milestones' | :limited_milestones_count | max_limited_count
+ 'users' | :limited_users_count | max_limited_count
'unknown' | nil | nil
end
@@ -56,9 +57,9 @@ describe Gitlab::SearchResults do
where(:count, :expected) do
23 | '23'
- 1000 | '1000'
- 1001 | '1000+'
- 1234 | '1000+'
+ 100 | '100'
+ 101 | max_limited_count
+ 1234 | max_limited_count
end
with_them do
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index 89d290aaa81..d3353b76c15 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::SnippetSearchResults do
+ include SearchHelpers
+
let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
let(:results) { described_class.new(Snippet.all, 'foo') }
@@ -25,7 +27,7 @@ describe Gitlab::SnippetSearchResults do
where(:scope, :count_method, :expected) do
'snippet_titles' | :snippet_titles_count | '1234'
'snippet_blobs' | :snippet_blobs_count | '1234'
- 'projects' | :limited_projects_count | '1000+'
+ 'projects' | :limited_projects_count | max_limited_count
'unknown' | nil | nil
end
diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb
index a85d418777f..cfc804c13a7 100644
--- a/spec/lib/gitlab/time_tracking_formatter_spec.rb
+++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb
@@ -17,6 +17,14 @@ describe Gitlab::TimeTrackingFormatter do
it { expect(subject).to eq(-12_000) }
end
+
+ context 'durations with months' do
+ let(:duration_string) { '1mo' }
+
+ it 'uses our custom conversions' do
+ expect(subject).to eq(576_000)
+ end
+ end
end
describe '#output' do
diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb
new file mode 100644
index 00000000000..623ac20fa7c
--- /dev/null
+++ b/spec/lib/gitlab_danger_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabDanger do
+ let(:gitlab_danger_helper) { nil }
+
+ subject { described_class.new(gitlab_danger_helper) }
+
+ describe '.local_warning_message' do
+ it 'returns an informational message with rules that can run' do
+ expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database')
+ end
+ end
+
+ describe '.success_message' do
+ it 'returns an informational success message' do
+ expect(described_class.success_message).to eq('==> No Danger rule violations!')
+ end
+ end
+
+ describe '#rule_names' do
+ context 'when running locally' do
+ it 'returns local only rules' do
+ expect(subject.rule_names).to eq(described_class::LOCAL_RULES)
+ end
+ end
+
+ context 'when running under CI' do
+ let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
+
+ it 'returns all rules' do
+ expect(subject.rule_names).to eq(described_class::LOCAL_RULES | described_class::CI_ONLY_RULES)
+ end
+ end
+ end
+
+ describe '#html_link' do
+ context 'when running locally' do
+ it 'returns the same string' do
+ str = 'something'
+
+ expect(subject.html_link(str)).to eq(str)
+ end
+ end
+
+ context 'when running under CI' do
+ let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
+
+ it 'returns a HTML link formatted version of the string' do
+ str = 'something'
+ html_formatted_str = %Q{<a href="#{str}">#{str}</a>}
+
+ expect(gitlab_danger_helper).to receive(:html_link).with(str).and_return(html_formatted_str)
+
+ expect(subject.html_link(str)).to eq(html_formatted_str)
+ end
+ end
+ end
+
+ describe '#ci?' do
+ context 'when gitlab_danger_helper is not available' do
+ it 'returns false' do
+ expect(subject.ci?).to be_falsey
+ end
+ end
+
+ context 'when gitlab_danger_helper is available' do
+ let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
+
+ it 'returns true' do
+ expect(subject.ci?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 74d4b12a070..589dac61528 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
describe Gitlab do
- include RailsHelpers
-
describe '.root' do
it 'returns the root path of the app' do
expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 63ca383ac4b..146e479adef 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -20,6 +20,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to belong_to(:merge_request) }
+ it { is_expected.to belong_to(:external_pull_request) }
it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) }
@@ -885,6 +886,25 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ context 'when source is external pull request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
+ end
+
+ let(:pull_request) { create(:external_pull_request, project: project) }
+
+ it 'exposes external pull request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
+ )
+ end
+ end
end
describe '#protected_ref?' do
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 1b39328752d..e1eee014567 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -81,27 +81,45 @@ describe Clusters::Applications::Jupyter do
end
describe '#files' do
- let(:application) { create(:clusters_applications_jupyter) }
+ let(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp, :project) }
+ let(:application) { create(:clusters_applications_jupyter, cluster: cluster) }
let(:values) { subject[:'values.yaml'] }
subject { application.files }
- it 'includes valid values' do
- expect(values).to include('ingress')
- expect(values).to include('hub')
- expect(values).to include('rbac')
- expect(values).to include('proxy')
- expect(values).to include('auth')
- expect(values).to include('singleuser')
- expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
- expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
- expect(values).to include("gitlabProjectIdWhitelist:\n - #{application.cluster.project.id}")
- expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
- expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
+ context 'when cluster belongs to a project' do
+ it 'includes valid values' do
+ expect(values).to include('ingress')
+ expect(values).to include('hub')
+ expect(values).to include('rbac')
+ expect(values).to include('proxy')
+ expect(values).to include('auth')
+ expect(values).to include('singleuser')
+ expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
+ expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
+ expect(values).to include("gitlabProjectIdWhitelist:\n - #{application.cluster.project.id}")
+ expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
+ expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
+ expect(values).to match(/GITLAB_CLUSTER_ID: '?#{application.cluster.id}/)
+ end
end
- context 'when cluster belongs to a project' do
- it 'sets GitLab project id' do
+ context 'when cluster belongs to a group' do
+ let(:group) { create(:group) }
+ let(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp, :group, groups: [group]) }
+
+ it 'includes valid values' do
+ expect(values).to include('ingress')
+ expect(values).to include('hub')
+ expect(values).to include('rbac')
+ expect(values).to include('proxy')
+ expect(values).to include('auth')
+ expect(values).to include('singleuser')
+ expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
+ expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
+ expect(values).to include("gitlabGroupWhitelist:\n - #{group.to_param}")
+ expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
+ expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
expect(values).to match(/GITLAB_CLUSTER_ID: '?#{application.cluster.id}/)
end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index da46effe411..d8f940a808e 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -131,7 +131,7 @@ describe CacheableAttributes do
context 'in production environment' do
before do
- expect(Rails.env).to receive(:production?).and_return(true)
+ stub_rails_env('production')
end
it 'returns an uncached record and logs a warning' do
@@ -143,7 +143,7 @@ describe CacheableAttributes do
context 'in other environments' do
before do
- expect(Rails.env).to receive(:production?).and_return(false)
+ stub_rails_env('development')
end
it 'returns an uncached record and logs a warning' do
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index a217dc42537..09fb2fff521 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -261,6 +261,18 @@ describe HasStatus do
end
end
+ describe '.alive_or_scheduled' do
+ subject { CommitStatus.alive_or_scheduled }
+
+ %i[running pending preparing created scheduled].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[failed success canceled skipped].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+
describe '.created_or_pending' do
subject { CommitStatus.created_or_pending }
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index a4a81ae126d..0d4dbfb215e 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -17,7 +17,7 @@ describe ShaAttribute do
describe '#sha_attribute' do
context 'when in non-production' do
before do
- allow(Rails.env).to receive(:production?).and_return(false)
+ stub_rails_env('development')
end
context 'when the table exists' do
@@ -76,7 +76,7 @@ describe ShaAttribute do
context 'when in production' do
before do
- allow(Rails.env).to receive(:production?).and_return(true)
+ stub_rails_env('production')
end
it 'defines a SHA attribute' do
diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/external_pull_request_spec.rb
new file mode 100644
index 00000000000..e85d5b2f6c7
--- /dev/null
+++ b/spec/models/external_pull_request_spec.rb
@@ -0,0 +1,220 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExternalPullRequest do
+ let(:project) { create(:project) }
+ let(:source_branch) { 'the-branch' }
+ let(:status) { :open }
+
+ it { is_expected.to belong_to(:project) }
+
+ shared_examples 'has errors on' do |attribute|
+ it "has errors for #{attribute}" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[attribute]).not_to be_empty
+ end
+ end
+
+ describe 'validations' do
+ context 'when source branch not present' do
+ subject { build(:external_pull_request, source_branch: nil) }
+
+ it_behaves_like 'has errors on', :source_branch
+ end
+
+ context 'when status not present' do
+ subject { build(:external_pull_request, status: nil) }
+
+ it_behaves_like 'has errors on', :status
+ end
+
+ context 'when pull request is from a fork' do
+ subject { build(:external_pull_request, source_repository: 'the-fork', target_repository: 'the-target') }
+
+ it_behaves_like 'has errors on', :base
+ end
+ end
+
+ describe 'create_or_update_from_params' do
+ subject { described_class.create_or_update_from_params(params) }
+
+ context 'when pull request does not exist' do
+ context 'when params are correct' do
+ let(:params) do
+ {
+ project_id: project.id,
+ pull_request_iid: 123,
+ source_branch: 'feature',
+ target_branch: 'master',
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: '97de212e80737a608d939f648d959671fb0a0142',
+ target_sha: 'a09386439ca39abe575675ffd4b89ae824fec22f',
+ status: :open
+ }
+ end
+
+ it 'saves the model successfully and returns it' do
+ expect(subject).to be_persisted
+ expect(subject).to be_valid
+ end
+
+ it 'yields the model' do
+ yielded_value = nil
+
+ result = described_class.create_or_update_from_params(params) do |pull_request|
+ yielded_value = pull_request
+ end
+
+ expect(result).to eq(yielded_value)
+ end
+ end
+
+ context 'when params are not correct' do
+ let(:params) do
+ {
+ pull_request_iid: 123,
+ source_branch: 'feature',
+ target_branch: 'master',
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: nil,
+ target_sha: nil,
+ status: :open
+ }
+ end
+
+ it 'returns an invalid model' do
+ expect(subject).not_to be_persisted
+ expect(subject).not_to be_valid
+ end
+ end
+ end
+
+ context 'when pull request exists' do
+ let!(:pull_request) do
+ create(:external_pull_request,
+ project: project,
+ source_sha: '97de212e80737a608d939f648d959671fb0a0142')
+ end
+
+ context 'when params are correct' do
+ let(:params) do
+ {
+ pull_request_iid: pull_request.pull_request_iid,
+ source_branch: pull_request.source_branch,
+ target_branch: pull_request.target_branch,
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: 'ce84140e8b878ce6e7c4d298c7202ff38170e3ac',
+ target_sha: pull_request.target_sha,
+ status: :open
+ }
+ end
+
+ it 'updates the model successfully and returns it' do
+ expect(subject).to be_valid
+ expect(subject.source_sha).to eq(params[:source_sha])
+ expect(pull_request.reload.source_sha).to eq(params[:source_sha])
+ end
+ end
+
+ context 'when params are not correct' do
+ let(:params) do
+ {
+ pull_request_iid: pull_request.pull_request_iid,
+ source_branch: pull_request.source_branch,
+ target_branch: pull_request.target_branch,
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: nil,
+ target_sha: nil,
+ status: :open
+ }
+ end
+
+ it 'returns an invalid model' do
+ expect(subject).not_to be_valid
+ expect(pull_request.reload.source_sha).not_to be_nil
+ expect(pull_request.target_sha).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#open?' do
+ it 'returns true if status is open' do
+ pull_request = create(:external_pull_request, status: :open)
+
+ expect(pull_request).to be_open
+ end
+
+ it 'returns false if status is not open' do
+ pull_request = create(:external_pull_request, status: :closed)
+
+ expect(pull_request).not_to be_open
+ end
+ end
+
+ describe '#closed?' do
+ it 'returns true if status is closed' do
+ pull_request = build(:external_pull_request, status: :closed)
+
+ expect(pull_request).to be_closed
+ end
+
+ it 'returns false if status is not closed' do
+ pull_request = build(:external_pull_request, status: :open)
+
+ expect(pull_request).not_to be_closed
+ end
+ end
+
+ describe '#actual_branch_head?' do
+ let(:project) { create(:project, :repository) }
+ let(:branch) { project.repository.branches.first }
+ let(:source_branch) { branch.name }
+
+ let(:pull_request) do
+ create(:external_pull_request,
+ project: project,
+ source_branch: source_branch,
+ source_sha: source_sha)
+ end
+
+ context 'when source sha matches the head of the branch' do
+ let(:source_sha) { branch.target }
+
+ it 'returns true' do
+ expect(pull_request).to be_actual_branch_head
+ end
+ end
+
+ context 'when source sha does not match the head of the branch' do
+ let(:source_sha) { project.repository.commit('HEAD').sha }
+
+ it 'returns true' do
+ expect(pull_request).not_to be_actual_branch_head
+ end
+ end
+ end
+
+ describe '#from_fork?' do
+ it 'returns true if source_repository differs from target_repository' do
+ pull_request = build(:external_pull_request,
+ source_repository: 'repository-1',
+ target_repository: 'repository-2')
+
+ expect(pull_request).to be_from_fork
+ end
+
+ it 'returns false if source_repository is the same as target_repository' do
+ pull_request = build(:external_pull_request,
+ source_repository: 'repository-1',
+ target_repository: 'repository-1')
+
+ expect(pull_request).not_to be_from_fork
+ end
+ end
+end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 28630f7d3fe..c73ade3f896 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -106,7 +106,8 @@ describe InternalId do
end
it 'always attempts to generate internal IDs in production mode' do
- allow(Rails.env).to receive(:test?).and_return(false)
+ stub_rails_env('production')
+
val = rand(1..100)
generator = double(generate: val)
expect(InternalId::InternalIdGenerator).to receive(:new).and_return(generator)
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 519c519fbcf..5168064bb84 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -151,6 +151,24 @@ describe PagesDomain do
end
end
end
+
+ context 'with ecdsa certificate' do
+ it "is valid" do
+ domain = build(:pages_domain, :ecdsa)
+
+ expect(domain).to be_valid
+ end
+
+ context 'when curve is set explicitly by parameters' do
+ it 'adds errors to private key' do
+ domain = build(:pages_domain, :explicit_ecdsa)
+
+ expect(domain).to be_invalid
+
+ expect(domain.errors[:key]).not_to be_empty
+ end
+ end
+ end
end
describe 'validations' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index bfbcac60fea..e2a684c42ae 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -99,6 +99,7 @@ describe Project do
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
it { is_expected.to have_many(:cycle_analytics_stages) }
+ it { is_expected.to have_many(:external_pull_requests) }
it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b8c323904b8..6722a3c627d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -32,7 +32,7 @@ describe User do
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
- it { is_expected.to have_many(:events).dependent(:destroy) }
+ it { is_expected.to have_many(:events).dependent(:delete_all) }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
diff --git a/spec/presenters/event_presenter_spec.rb b/spec/presenters/event_presenter_spec.rb
new file mode 100644
index 00000000000..79f5e359141
--- /dev/null
+++ b/spec/presenters/event_presenter_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EventPresenter do
+ include Gitlab::Routing.url_helpers
+
+ set(:group) { create(:group) }
+ set(:project) { create(:project, group: group) }
+ set(:target) { create(:milestone, project: project) }
+ set(:group_event) { create(:event, :created, project: nil, group: group, target: target) }
+ set(:project_event) { create(:event, :created, project: project, target: target) }
+
+ describe '#resource_parent_name' do
+ context 'with group event' do
+ subject { group_event.present.resource_parent_name }
+
+ it { is_expected.to eq(group.full_name) }
+ end
+
+ context 'with project label' do
+ subject { project_event.present.resource_parent_name }
+
+ it { is_expected.to eq(project.full_name) }
+ end
+ end
+
+ describe '#target_link_options' do
+ context 'with group event' do
+ subject { group_event.present.target_link_options }
+
+ it { is_expected.to eq([group, target]) }
+ end
+
+ context 'with project label' do
+ subject { project_event.present.target_link_options }
+
+ it { is_expected.to eq([group.becomes(Namespace), project, target]) }
+ end
+ end
+end
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
new file mode 100644
index 00000000000..0b3c5be9c45
--- /dev/null
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Internal::Pages do
+ describe "GET /internal/pages" do
+ let(:pages_shared_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
+
+ before do
+ allow(Gitlab::Pages).to receive(:secret).and_return(pages_shared_secret)
+ end
+
+ def query_host(host, headers = {})
+ get api("/internal/pages"), headers: headers, params: { host: host }
+ end
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(pages_internal_api: false)
+ end
+
+ it 'responds with 404 Not Found' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'feature flag enabled' do
+ context 'not authenticated' do
+ it 'responds with 401 Unauthorized' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context 'authenticated' do
+ def query_host(host)
+ jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
+ headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
+
+ super(host, headers)
+ end
+
+ it 'responds with 200 OK' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 048d04cdefd..d98b9be726a 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -252,5 +252,43 @@ describe API::Settings, 'Settings' do
expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost'])
end
end
+
+ context 'domain_blacklist settings' do
+ it 'rejects domain_blacklist_enabled when domain_blacklist is empty' do
+ put api('/application/settings', admin),
+ params: {
+ domain_blacklist_enabled: true,
+ domain_blacklist: []
+ }
+
+ expect(response).to have_gitlab_http_status(400)
+ message = json_response["message"]
+ expect(message["domain_blacklist"]).to eq(["Domain blacklist cannot be empty if Blacklist is enabled."])
+ end
+
+ it 'allows array for domain_blacklist' do
+ put api('/application/settings', admin),
+ params: {
+ domain_blacklist_enabled: true,
+ domain_blacklist: ['domain1.com', 'domain2.com']
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['domain_blacklist_enabled']).to be(true)
+ expect(json_response['domain_blacklist']).to eq(['domain1.com', 'domain2.com'])
+ end
+
+ it 'allows a string for domain_blacklist' do
+ put api('/application/settings', admin),
+ params: {
+ domain_blacklist_enabled: true,
+ domain_blacklist: 'domain3.com, *.domain4.com'
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['domain_blacklist_enabled']).to be(true)
+ expect(json_response['domain_blacklist']).to eq(['domain3.com', '*.domain4.com'])
+ end
+ end
end
end
diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb
new file mode 100644
index 00000000000..91fc4d4c123
--- /dev/null
+++ b/spec/requests/api/statistics_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Statistics, 'Statistics' do
+ include ProjectForksHelper
+ TABLES_TO_ANALYZE = %w[
+ projects
+ users
+ namespaces
+ issues
+ merge_requests
+ notes
+ snippets
+ fork_networks
+ fork_network_members
+ keys
+ milestones
+ ].freeze
+
+ let(:path) { "/application/statistics" }
+
+ describe "GET /application/statistics" do
+ context 'when no user' do
+ it "returns authentication error" do
+ get api(path, nil)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context "when not an admin" do
+ let(:user) { create(:user) }
+
+ it "returns forbidden error" do
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ let(:admin) { create(:admin) }
+
+ it 'matches the response schema' do
+ get api(path, admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('statistics')
+ end
+
+ it 'gives the right statistics' do
+ projects = create_list(:project, 4, namespace: create(:namespace, owner: admin))
+ issues = create_list(:issue, 2, project: projects.first, updated_by: admin)
+
+ create_list(:snippet, 2, :public, author: admin)
+ create_list(:note, 2, author: admin, project: projects.first, noteable: issues.first)
+ create_list(:milestone, 3, project: projects.first)
+ create(:key, user: admin)
+ create(:merge_request, source_project: projects.first)
+ fork_project(projects.first, admin)
+
+ # Make sure the reltuples have been updated
+ # to get a correct count on postgresql
+ TABLES_TO_ANALYZE.each do |table|
+ ActiveRecord::Base.connection.execute("ANALYZE #{table}")
+ end
+
+ get api(path, admin)
+
+ expected_statistics = {
+ issues: 2,
+ merge_requests: 1,
+ notes: 2,
+ snippets: 2,
+ forks: 1,
+ ssh_keys: 1,
+ milestones: 3,
+ users: 1,
+ projects: 5,
+ groups: 1,
+ active_users: 1
+ }
+
+ expected_statistics.each do |entity, count|
+ expect(json_response[entity.to_s]).to eq(count.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index d1b58aac104..97de26650db 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -11,6 +11,8 @@ require 'spec_helper'
# because they are 3 edge cases of using wiki pages.
describe API::Wikis do
+ include WorkhorseHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(user) } }
let(:project_wiki) { create(:project_wiki, project: project, user: user) }
@@ -155,7 +157,7 @@ describe API::Wikis do
it 'pushes attachment to the wiki repository' do
allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
- post(api(url, user), params: payload)
+ workhorse_post_with_file(api(url, user), file_key: :file, params: payload)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq result_hash.deep_stringify_keys
@@ -180,6 +182,15 @@ describe API::Wikis do
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is invalid')
end
+
+ it 'is backward compatible with regular multipart uploads' do
+ allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
+
+ post(api(url, user), params: payload)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to eq result_hash.deep_stringify_keys
+ end
end
describe 'GET /projects/:id/wikis' do
diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb
new file mode 100644
index 00000000000..aca4644289d
--- /dev/null
+++ b/spec/requests/projects/uploads_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'File uploads' do
+ include WorkhorseHelpers
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ describe 'POST /:namespace/:project/create/:branch' do
+ let(:branch) { 'master' }
+ let(:create_url) { project_blob_path(project, branch) }
+ let(:blob_url) { project_blob_path(project, "#{branch}/dk.png") }
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: branch,
+ branch_name: branch,
+ file: fixture_file_upload('spec/fixtures/dk.png'),
+ commit_message: 'Add an image'
+ }
+ end
+
+ before do
+ project.add_maintainer(user)
+
+ login_as(user)
+ end
+
+ it 'redirects to blob' do
+ workhorse_post_with_file(create_url, file_key: :file, params: params)
+
+ expect(response).to redirect_to(blob_url)
+ end
+ end
+end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 478f09a7881..cf459ba99c1 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -20,11 +20,6 @@ describe 'Rack Attack global throttles' do
let(:period_in_seconds) { 10000 }
let(:period) { period_in_seconds.seconds }
- let(:url_that_does_not_require_authentication) { '/users/sign_in' }
- let(:url_that_requires_authentication) { '/dashboard/snippets' }
- let(:url_api_internal) { '/api/v4/internal/check' }
- let(:api_partial_url) { '/todos' }
-
around do |example|
# Instead of test environment's :null_store so the throttles can increment
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
@@ -35,112 +30,10 @@ describe 'Rack Attack global throttles' do
Rack::Attack.cache.store = Rails.cache
end
- # Requires let variables:
- # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
- # * get_args
- # * other_user_get_args
- shared_examples_for 'rate-limited token-authenticated requests' do
- before do
- # Set low limits
- settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
- settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
- end
-
- context 'when the throttle is enabled' do
- before do
- settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
- stub_application_setting(settings_to_set)
- end
-
- it 'rejects requests over the rate limit' do
- # At first, allow requests under the rate limit.
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- # the last straw
- expect_rejection { get(*get_args) }
- end
-
- it 'allows requests after throttling and then waiting for the next period' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get(*get_args) }
-
- Timecop.travel(period.from_now) do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get(*get_args) }
- end
- end
-
- it 'counts requests from different users separately, even from the same IP' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- # would be over the limit if this wasn't a different user
- get(*other_user_get_args)
- expect(response).to have_http_status 200
- end
-
- it 'counts all requests from the same user, even via different IPs' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
-
- expect_rejection { get(*get_args) }
- end
-
- it 'logs RackAttack info into structured logs' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- arguments = {
- message: 'Rack_Attack',
- env: :throttle,
- remote_ip: '127.0.0.1',
- request_method: 'GET',
- path: get_args.first,
- user_id: user.id,
- username: user.username
- }
-
- expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
-
- expect_rejection { get(*get_args) }
- end
- end
-
- context 'when the throttle is disabled' do
- before do
- settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
- stub_application_setting(settings_to_set)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
- end
- end
- end
-
describe 'unauthenticated requests' do
+ let(:url_that_does_not_require_authentication) { '/users/sign_in' }
+ let(:url_api_internal) { '/api/v4/internal/check' }
+
before do
# Set low limits
settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
@@ -245,6 +138,7 @@ describe 'Rack Attack global throttles' do
let(:other_user) { create(:user) }
let(:other_user_token) { create(:personal_access_token, user: other_user) }
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+ let(:api_partial_url) { '/todos' }
context 'with the token in the query string' do
let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
@@ -265,10 +159,13 @@ describe 'Rack Attack global throttles' do
let(:user) { create(:user) }
let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
+
let(:other_user) { create(:user) }
let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) }
let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") }
+
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+ let(:api_partial_url) { '/todos' }
context 'with the token in the query string' do
let(:get_args) { [api(api_partial_url, oauth_access_token: token)] }
@@ -299,110 +196,11 @@ describe 'Rack Attack global throttles' do
end
describe 'web requests authenticated with regular login' do
+ let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
let(:user) { create(:user) }
+ let(:url_that_requires_authentication) { '/dashboard/snippets' }
- before do
- login_as(user)
-
- # Set low limits
- settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period
- settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds
- end
-
- context 'when the throttle is enabled' do
- before do
- settings_to_set[:throttle_authenticated_web_enabled] = true
- stub_application_setting(settings_to_set)
- end
-
- it 'rejects requests over the rate limit' do
- # At first, allow requests under the rate limit.
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- # the last straw
- expect_rejection { get url_that_requires_authentication }
- end
-
- it 'allows requests after throttling and then waiting for the next period' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get url_that_requires_authentication }
-
- Timecop.travel(period.from_now) do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get url_that_requires_authentication }
- end
- end
-
- it 'counts requests from different users separately, even from the same IP' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- # would be over the limit if this wasn't a different user
- login_as(create(:user))
-
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- it 'counts all requests from the same user, even via different IPs' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
-
- expect_rejection { get url_that_requires_authentication }
- end
-
- it 'logs RackAttack info into structured logs' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- arguments = {
- message: 'Rack_Attack',
- env: :throttle,
- remote_ip: '127.0.0.1',
- request_method: 'GET',
- path: '/dashboard/snippets',
- user_id: user.id,
- username: user.username
- }
-
- expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
-
- get url_that_requires_authentication
- end
- end
-
- context 'when the throttle is disabled' do
- before do
- settings_to_set[:throttle_authenticated_web_enabled] = false
- stub_application_setting(settings_to_set)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
- end
- end
+ it_behaves_like 'rate-limited web authenticated requests'
end
def api_get_args_with_token_headers(partial_url, token_headers)
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 8a3de2a52fc..7e2d70d6eb5 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -530,15 +530,22 @@ describe 'project routing' do
end
end
- # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/}
+ # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /[^\0]+/, project_id: /[^\/]+/}
describe Projects::BlameController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/blame/master/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blame', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/#{newline_file}" })
end
end
- # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/}
+ # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /[^\0]+/, project_id: /[^\/]+/}
describe Projects::BlobController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
@@ -547,28 +554,56 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
expect(get('/gitlab/gitlabhq/blob/master/blob/index.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/blob/index.js')
expect(get('/gitlab/gitlabhq/blob/blob/master/blob/index.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'blob/master/blob/index.js')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/blob/blob/master/blob/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blob', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "blob/master/blob/#{newline_file}" })
end
end
- # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/}
+ # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /[^\0]+/, project_id: /[^\/]+/}
describe Projects::TreeController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
expect(get('/gitlab/gitlabhq/tree/master/tree/files')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/tree/files')
expect(get('/gitlab/gitlabhq/tree/tree/master/tree/files')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'tree/master/tree/files')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/tree/master/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/tree', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/#{newline_file}" })
end
end
- # project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
+ # project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/[^\0]+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/}
describe Projects::FindFileController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/find_file/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/find_file', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "#{newline_file}" })
end
it 'to #list' do
expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.json')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/files/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/find_file', action: 'list',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "#{newline_file}" })
end
end
@@ -578,6 +613,13 @@ describe 'project routing' do
route_to('projects/blob#edit',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: 'master/app/models/project.rb'))
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/edit/master/docs/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blob', action: 'edit',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/docs/#{newline_file}" })
end
it 'to #preview' do
@@ -585,6 +627,26 @@ describe 'project routing' do
route_to('projects/blob#preview',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: 'master/app/models/project.rb'))
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/edit/master/docs/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blob', action: 'edit',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/docs/#{newline_file}" })
+ end
+ end
+
+ # project_raw GET /:project_id/raw/:id(.:format) raw#show {id: /[^\0]+/, project_id: /[^\/]+/}
+ describe Projects::RawController, 'routing' do
+ it 'to #show' do
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/raw/master/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/raw', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/#{newline_file}" })
end
end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index d1483c3c41e..cf0b8ea9b40 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe MergeRequestSerializer do
- let(:user) { create(:user) }
- let(:resource) { create(:merge_request) }
+ set(:user) { create(:user) }
+ set(:resource) { create(:merge_request, description: "Description") }
+
let(:json_entity) do
described_class.new(current_user: user)
.represent(resource, serializer: serializer)
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index fad865a4811..fe86982af91 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -23,6 +23,7 @@ describe Ci::CreatePipelineService do
trigger_request: nil,
variables_attributes: nil,
merge_request: nil,
+ external_pull_request: nil,
push_options: nil,
source_sha: nil,
target_sha: nil,
@@ -36,8 +37,11 @@ describe Ci::CreatePipelineService do
source_sha: source_sha,
target_sha: target_sha }
- described_class.new(project, user, params).execute(
- source, save_on_errors: save_on_errors, trigger_request: trigger_request, merge_request: merge_request)
+ described_class.new(project, user, params).execute(source,
+ save_on_errors: save_on_errors,
+ trigger_request: trigger_request,
+ merge_request: merge_request,
+ external_pull_request: external_pull_request)
end
# rubocop:enable Metrics/ParameterLists
@@ -220,11 +224,11 @@ describe Ci::CreatePipelineService do
expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: pipeline.id)
end
- it 'does not cancel running outdated pipelines' do
+ it 'cancels running outdated pipelines' do
pipeline_on_previous_commit.run
- execute_service
+ head_pipeline = execute_service
- expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'running', auto_canceled_by_id: nil)
+ expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: head_pipeline.id)
end
it 'cancel created outdated pipelines' do
@@ -243,6 +247,202 @@ describe Ci::CreatePipelineService do
expect(pending_pipeline.reload).to have_attributes(status: 'pending', auto_canceled_by_id: nil)
end
+
+ context 'when the interruptible attribute is' do
+ context 'not defined' do
+ before do
+ config = YAML.dump(rspec: { script: 'echo' })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'is cancelable' do
+ pipeline = execute_service
+
+ expect(pipeline.builds.find_by(name: 'rspec').interruptible).to be_nil
+ end
+ end
+
+ context 'set to true' do
+ before do
+ config = YAML.dump(rspec: { script: 'echo', interruptible: true })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'is cancelable' do
+ pipeline = execute_service
+
+ expect(pipeline.builds.find_by(name: 'rspec').interruptible).to be_truthy
+ end
+ end
+
+ context 'set to false' do
+ before do
+ config = YAML.dump(rspec: { script: 'echo', interruptible: false })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'is not cancelable' do
+ pipeline = execute_service
+
+ expect(pipeline.builds.find_by(name: 'rspec').interruptible).to be_falsy
+ end
+ end
+
+ context 'not defined, but an environment is' do
+ before do
+ config = YAML.dump(rspec: { script: 'echo', environment: { name: "review/$CI_COMMIT_REF_NAME" } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'is not cancelable' do
+ pipeline = execute_service
+
+ expect(pipeline.builds.find_by(name: 'rspec').interruptible).to be_nil
+ end
+ end
+
+ context 'overriding the environment definition' do
+ before do
+ config = YAML.dump(rspec: { script: 'echo', environment: { name: "review/$CI_COMMIT_REF_NAME" }, interruptible: true })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'is cancelable' do
+ pipeline = execute_service
+
+ expect(pipeline.builds.find_by(name: 'rspec').interruptible).to be_truthy
+ end
+ end
+ end
+
+ context 'interruptible builds' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:config) do
+ {
+ stages: %w[stage1 stage2 stage3 stage4],
+
+ build_1_1: {
+ stage: 'stage1',
+ script: 'echo'
+ },
+ build_1_2: {
+ stage: 'stage1',
+ script: 'echo',
+ interruptible: true
+ },
+ build_2_1: {
+ stage: 'stage2',
+ script: 'echo',
+ when: 'delayed',
+ start_in: '10 minutes'
+ },
+ build_3_1: {
+ stage: 'stage3',
+ script: 'echo',
+ interruptible: false
+ },
+ build_4_1: {
+ stage: 'stage4',
+ script: 'echo'
+ }
+ }
+ end
+
+ it 'properly configures interruptible status' do
+ interruptible_status =
+ pipeline_on_previous_commit
+ .builds
+ .joins(:metadata)
+ .pluck(:name, 'ci_builds_metadata.interruptible')
+
+ expect(interruptible_status).to contain_exactly(
+ ['build_1_1', nil],
+ ['build_1_2', true],
+ ['build_2_1', nil],
+ ['build_3_1', false],
+ ['build_4_1', nil]
+ )
+ end
+
+ context 'when only interruptible builds are running' do
+ context 'when build marked explicitly by interruptible is running' do
+ it 'cancels running outdated pipelines' do
+ pipeline_on_previous_commit
+ .builds
+ .find_by_name('build_1_2')
+ .run!
+
+ pipeline
+
+ expect(pipeline_on_previous_commit.reload).to have_attributes(
+ status: 'canceled', auto_canceled_by_id: pipeline.id)
+ end
+ end
+
+ context 'when build that is not marked as interruptible is running' do
+ it 'cancels running outdated pipelines' do
+ pipeline_on_previous_commit
+ .builds
+ .find_by_name('build_2_1')
+ .tap(&:enqueue!)
+ .run!
+
+ pipeline
+
+ expect(pipeline_on_previous_commit.reload).to have_attributes(
+ status: 'canceled', auto_canceled_by_id: pipeline.id)
+ end
+ end
+ end
+
+ context 'when an uninterruptible build is running' do
+ it 'does not cancel running outdated pipelines' do
+ pipeline_on_previous_commit
+ .builds
+ .find_by_name('build_3_1')
+ .tap(&:enqueue!)
+ .run!
+
+ pipeline
+
+ expect(pipeline_on_previous_commit.reload).to have_attributes(
+ status: 'running', auto_canceled_by_id: nil)
+ end
+ end
+
+ context 'when an build is waiting on an interruptible scheduled task' do
+ it 'cancels running outdated pipelines' do
+ allow(Ci::BuildScheduleWorker).to receive(:perform_at)
+
+ pipeline_on_previous_commit
+ .builds
+ .find_by_name('build_2_1')
+ .schedule!
+
+ pipeline
+
+ expect(pipeline_on_previous_commit.reload).to have_attributes(
+ status: 'canceled', auto_canceled_by_id: pipeline.id)
+ end
+ end
+
+ context 'when a uninterruptible build has finished' do
+ it 'does not cancel running outdated pipelines' do
+ pipeline_on_previous_commit
+ .builds
+ .find_by_name('build_3_1')
+ .success!
+
+ pipeline
+
+ expect(pipeline_on_previous_commit.reload).to have_attributes(
+ status: 'running', auto_canceled_by_id: nil)
+ end
+ end
+ end
end
context 'auto-cancel disabled' do
@@ -560,33 +760,32 @@ describe Ci::CreatePipelineService do
end
context 'when builds with auto-retries are configured' do
+ let(:pipeline) { execute_service }
+ let(:rspec_job) { pipeline.builds.find_by(name: 'rspec') }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump({
+ rspec: { script: 'rspec', retry: retry_value }
+ }))
+ end
+
context 'as an integer' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
- stub_ci_pipeline_yaml_file(config)
- end
+ let(:retry_value) { 2 }
it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
-
expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
- expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always']
+ expect(rspec_job.retries_max).to eq 2
+ expect(rspec_job.retry_when).to eq ['always']
end
end
context 'as hash' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } })
- stub_ci_pipeline_yaml_file(config)
- end
+ let(:retry_value) { { max: 2, when: 'runner_system_failure' } }
it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
-
expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
- expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure']
+ expect(rspec_job.retries_max).to eq 2
+ expect(rspec_job.retry_when).to eq ['runner_system_failure']
end
end
end
@@ -773,6 +972,152 @@ describe Ci::CreatePipelineService do
end
end
+ describe 'Pipeline for external pull requests' do
+ let(:pipeline) do
+ execute_service(source: source,
+ external_pull_request: pull_request,
+ ref: ref_name,
+ source_sha: source_sha,
+ target_sha: target_sha)
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:ref_name) { 'refs/heads/feature' }
+ let(:source_sha) { project.commit(ref_name).id }
+ let(:target_sha) { nil }
+
+ context 'when source is external pull request' do
+ let(:source) { :external_pull_request_event }
+
+ context 'when config has external_pull_requests keywords' do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo',
+ only: ['external_pull_requests']
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo',
+ except: ['external_pull_requests']
+ }
+ }
+ end
+
+ context 'when external pull request is specified' do
+ let(:pull_request) { create(:external_pull_request, project: project, source_branch: 'feature', target_branch: 'master') }
+ let(:ref_name) { pull_request.source_ref }
+
+ it 'creates an external pull request pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_external_pull_request_event
+ expect(pipeline.external_pull_request).to eq(pull_request)
+ expect(pipeline.source_sha).to eq(source_sha)
+ expect(pipeline.builds.order(:stage_id)
+ .map(&:name))
+ .to eq(%w[build test])
+ end
+
+ context 'when ref is tag' do
+ let(:ref_name) { 'refs/tags/v1.1.0' }
+
+ it 'does not create an extrnal pull request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:tag]).to eq(["is not included in the list"])
+ end
+ end
+
+ context 'when pull request is created from fork' do
+ it 'does not create an external pull request pipeline'
+ end
+
+ context "when there are no matched jobs" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo',
+ except: ['external_pull_requests']
+ }
+ }
+ end
+
+ it 'does not create a detached merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."])
+ end
+ end
+ end
+
+ context 'when external pull request is not specified' do
+ let(:pull_request) { nil }
+
+ it 'does not create an external pull request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:external_pull_request]).to eq(["can't be blank"])
+ end
+ end
+ end
+
+ context "when config does not have external_pull_requests keywords" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo'
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo'
+ }
+ }
+ end
+
+ context 'when external pull request is specified' do
+ let(:pull_request) do
+ create(:external_pull_request,
+ project: project,
+ source_branch: Gitlab::Git.ref_name(ref_name),
+ target_branch: 'master')
+ end
+
+ it 'creates an external pull request pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_external_pull_request_event
+ expect(pipeline.external_pull_request).to eq(pull_request)
+ expect(pipeline.source_sha).to eq(source_sha)
+ expect(pipeline.builds.order(:stage_id)
+ .map(&:name))
+ .to eq(%w[build test pages])
+ end
+ end
+
+ context 'when external pull request is not specified' do
+ let(:pull_request) { nil }
+
+ it 'does not create an external pull request pipeline' do
+ expect(pipeline).not_to be_persisted
+
+ expect(pipeline.errors[:base])
+ .to eq(['Failed to build the pipeline!'])
+ end
+ end
+ end
+ end
+ end
+
describe 'Pipelines for merge requests' do
let(:pipeline) do
execute_service(source: source,
@@ -828,7 +1173,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to be_persisted
expect(pipeline).to be_merge_request_event
expect(pipeline.merge_request).to eq(merge_request)
- expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test])
+ expect(pipeline.builds.order(:stage_id).pluck(:name)).to eq(%w[test])
end
it 'persists the specified source sha' do
@@ -1093,7 +1438,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to be_persisted
expect(pipeline).to be_web
expect(pipeline.merge_request).to be_nil
- expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[build pages])
+ expect(pipeline.builds.order(:stage_id).pluck(:name)).to eq(%w[build pages])
end
end
end
@@ -1133,7 +1478,7 @@ describe Ci::CreatePipelineService do
it 'creates a pipeline with build_a and test_a' do
expect(pipeline).to be_persisted
- expect(pipeline.builds.map(&:name)).to contain_exactly("build_a", "test_a")
+ expect(pipeline.builds.pluck(:name)).to contain_exactly("build_a", "test_a")
end
end
@@ -1168,7 +1513,303 @@ describe Ci::CreatePipelineService do
it 'does create a pipeline only with deploy' do
expect(pipeline).to be_persisted
- expect(pipeline.builds.map(&:name)).to contain_exactly("deploy")
+ expect(pipeline.builds.pluck(:name)).to contain_exactly("deploy")
+ end
+ end
+ end
+
+ context 'when rules are used' do
+ let(:ref_name) { 'refs/heads/master' }
+ let(:pipeline) { execute_service }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+ let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') }
+ let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') }
+ let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') }
+
+ shared_examples 'rules jobs are excluded' do
+ it 'only persists the job without rules' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_nil
+ expect(delayed_job).to be_nil
+ end
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ end
+
+ context 'with simple if: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ master-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "nonexistant-branch"
+ when: never
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: delayed
+ start_in: 1 hour
+
+ never-job:
+ script: "echo Goodbye, World!"
+ rules:
+ - if: $CI_COMMIT_REF_NAME
+ when: never
+ EOY
+ end
+
+ context 'with matches' do
+ it 'creates a pipeline with the vanilla and manual jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'delayed-job', 'master-job')
+ end
+
+ it 'assigns job:when values to the builds' do
+ expect(pipeline.builds.pluck(:when)).to contain_exactly('on_success', 'delayed', 'manual')
+ end
+
+ it 'assigns start_in for delayed jobs' do
+ expect(delayed_job.options[:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'with no matches' do
+ let(:ref_name) { 'refs/heads/feature' }
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+ end
+
+ context 'with complex if: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+ rules:
+ - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME
+ when: manual
+ EOY
+ end
+
+ it 'matches the first rule' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job')
+ expect(regular_job.when).to eq('manual')
+ end
+ end
+
+ context 'with changes:' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - changes:
+ - README.md
+ when: manual
+ - changes:
+ - app.rb
+ when: on_success
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - changes:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+ EOY
+ end
+
+ context 'and matches' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[README.md])
+ end
+
+ it 'creates two jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names)
+ .to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+ end
+
+ context 'and matches the second rule' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[app.rb])
+ end
+
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ end
+ end
+
+ context 'and does not match' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[useless_script.rb])
+ end
+
+ it_behaves_like 'rules jobs are excluded'
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+ end
+
+ context 'with mixed if: and changes: rules' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - changes:
+ - README.md
+ when: manual
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: on_success
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - changes:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: delayed
+ start_in: 1 hour
+ EOY
+ end
+
+ context 'and changes: matches before if' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[README.md])
+ end
+
+ it 'creates two jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names)
+ .to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+ end
+
+ context 'and if: matches after changes' do
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'and does not match' do
+ let(:ref_name) { 'refs/heads/wip' }
+
+ it_behaves_like 'rules jobs are excluded'
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+ end
+
+ context 'with mixed if: and changes: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ changes: [README.md]
+ when: on_success
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ changes: [app.rb]
+ when: manual
+ EOY
+ end
+
+ context 'with if matches and changes matches' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[app.rb])
+ end
+
+ it 'persists all jobs' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_persisted
+ expect(rules_job.when).to eq('manual')
+ end
+ end
+
+ context 'with if matches and no change matches' do
+ it_behaves_like 'rules jobs are excluded'
+ end
+
+ context 'with change matches and no if matches' do
+ let(:ref_name) { 'refs/heads/feature' }
+
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[README.md])
+ end
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+
+ context 'and no matches' do
+ let(:ref_name) { 'refs/heads/feature' }
+
+ it_behaves_like 'rules jobs are excluded'
end
end
end
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index bb86a742f0e..8dd573c3698 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -147,12 +147,12 @@ describe Clusters::Applications::CreateService do
using RSpec::Parameterized::TableSyntax
- where(:application, :association, :allowed, :pre_create_helm) do
- 'helm' | :application_helm | true | false
- 'ingress' | :application_ingress | true | true
- 'runner' | :application_runner | true | true
- 'prometheus' | :application_prometheus | true | true
- 'jupyter' | :application_jupyter | false | true
+ where(:application, :association, :allowed, :pre_create_helm, :pre_create_ingress) do
+ 'helm' | :application_helm | true | false | false
+ 'ingress' | :application_ingress | true | true | false
+ 'runner' | :application_runner | true | true | false
+ 'prometheus' | :application_prometheus | true | true | false
+ 'jupyter' | :application_jupyter | true | true | true
end
with_them do
@@ -160,6 +160,7 @@ describe Clusters::Applications::CreateService do
klass = "Clusters::Applications::#{application.titleize}"
allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original
create(:clusters_applications_helm, :installed, cluster: cluster) if pre_create_helm
+ create(:clusters_applications_ingress, :installed, cluster: cluster, external_hostname: 'example.com') if pre_create_ingress
end
let(:params) { { application: application } }
diff --git a/spec/services/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/external_pull_requests/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..a4da5b38b97
--- /dev/null
+++ b/spec/services/external_pull_requests/create_pipeline_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExternalPullRequests::CreatePipelineService do
+ describe '#execute' do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:pull_request) { create(:external_pull_request, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ subject { described_class.new(project, user).execute(pull_request) }
+
+ context 'when pull request is open' do
+ before do
+ pull_request.update!(status: :open)
+ end
+
+ context 'when source sha is the head of the source branch' do
+ let(:source_branch) { project.repository.branches.last }
+ let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
+
+ before do
+ pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target)
+ end
+
+ it 'creates a pipeline for external pull request' do
+ expect(subject).to be_valid
+ expect(subject).to be_persisted
+ expect(subject).to be_external_pull_request_event
+ expect(subject).to eq(project.ci_pipelines.last)
+ expect(subject.external_pull_request).to eq(pull_request)
+ expect(subject.user).to eq(user)
+ expect(subject.status).to eq('pending')
+ expect(subject.ref).to eq(pull_request.source_branch)
+ expect(subject.sha).to eq(pull_request.source_sha)
+ expect(subject.source_sha).to eq(pull_request.source_sha)
+ end
+ end
+
+ context 'when source sha is not the head of the source branch (force push upon rebase)' do
+ let(:source_branch) { project.repository.branches.first }
+ let(:commit) { project.repository.commits(source_branch.name, limit: 2).last }
+
+ before do
+ pull_request.update!(source_branch: source_branch.name, source_sha: commit.sha)
+ end
+
+ it 'does nothing' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ context 'when pull request is not opened' do
+ before do
+ pull_request.update!(status: :closed)
+ end
+
+ it 'does nothing' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+
+ expect(subject).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index d9e607cd251..c3a4f3dbe3f 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -99,6 +99,20 @@ describe Git::BranchPushService, services: true do
expect(pipeline).to be_push
expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref)
end
+
+ context 'when pipeline has errors' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'reports an error' do
+ allow(Sidekiq).to receive(:server?).and_return(true)
+ expect(Sidekiq.logger).to receive(:warn)
+
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ end
+ end
end
describe "Updates merge requests" do
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index a27fea0c90f..ff4cdd3e7e2 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -13,6 +13,9 @@ describe MergeRequests::PushOptionsHandlerService do
let(:target_branch) { 'feature' }
let(:title) { 'my title' }
let(:description) { 'my description' }
+ let(:label1) { 'mylabel1' }
+ let(:label2) { 'mylabel2' }
+ let(:label3) { 'mylabel3' }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
@@ -122,6 +125,16 @@ describe MergeRequests::PushOptionsHandlerService do
end
end
+ shared_examples_for 'a service that can change labels of a merge request' do |count|
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'changes label count' do
+ service.execute
+
+ expect(last_mr.label_ids.count).to eq(count)
+ end
+ end
+
shared_examples_for 'a service that does not create a merge request' do
it do
expect { service.execute }.not_to change { MergeRequest.count }
@@ -504,6 +517,138 @@ describe MergeRequests::PushOptionsHandlerService do
end
end
+ describe '`label` push option' do
+ let(:push_options) { { label: { label1 => 1, label2 => 1 } } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 2
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 2
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 2
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`unlabel` push option' do
+ let(:push_options) { { label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 1
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 1
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 1
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
describe 'multiple pushed branches' do
let(:push_options) { { create: true } }
let(:changes) do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 6ca0a3fa448..b65ee16c189 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1140,6 +1140,19 @@ describe QuickActions::InterpretService do
let(:todo_label) { create(:label, project: project, title: 'To Do') }
let(:inreview_label) { create(:label, project: project, title: 'In Review') }
+ it 'is available when the user is a developer' do
+ expect(service.available_commands(issue)).to include(a_hash_including(name: :copy_metadata))
+ end
+
+ context 'when the user does not have permission' do
+ let(:guest) { create(:user) }
+ let(:service) { described_class.new(project, guest) }
+
+ it 'is not available' do
+ expect(service.available_commands(issue)).not_to include(a_hash_including(name: :copy_metadata))
+ end
+ end
+
it_behaves_like 'empty command' do
let(:content) { '/copy_metadata' }
let(:issuable) { issue }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6ce76af556f..47f09bf14d0 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -109,6 +109,7 @@ RSpec.configure do |config|
config.include PolicyHelpers, type: :policy
config.include MemoryUsageHelper
config.include ExpectRequestWithStatus, type: :request
+ config.include RailsHelpers
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index 2cf3f4b83c4..d1d25fbabcd 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -19,4 +19,8 @@ module SearchHelpers
click_link scope
end
end
+
+ def max_limited_count
+ Gitlab::SearchResults::COUNT_LIMIT_MESSAGE
+ end
end
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 4488e5f227e..fdbfe53fa39 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -17,7 +17,36 @@ module WorkhorseHelpers
end
def workhorse_internal_api_request_header
- jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
{ 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
end
+
+ # workhorse_post_with_file will transform file_key inside params as if it was disk accelerated by workhorse
+ def workhorse_post_with_file(url, file_key:, params:)
+ workhorse_params = params.dup
+ file = workhorse_params.delete(file_key)
+
+ workhorse_params.merge!(workhorse_disk_accelerated_file_params(file_key, file))
+
+ post(url,
+ params: workhorse_params,
+ headers: workhorse_rewritten_fields_header('file' => file.path)
+ )
+ end
+
+ private
+
+ def jwt_token(data = {})
+ JWT.encode({ 'iss' => 'gitlab-workhorse' }.merge(data), Gitlab::Workhorse.secret, 'HS256')
+ end
+
+ def workhorse_rewritten_fields_header(fields)
+ { Gitlab::Middleware::Multipart::RACK_ENV_KEY => jwt_token('rewritten_fields' => fields) }
+ end
+
+ def workhorse_disk_accelerated_file_params(key, file)
+ {
+ "#{key}.name" => file.original_filename,
+ "#{key}.path" => file.path
+ }
+ end
end
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 734d6838f4d..116bc8d0b9c 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -1,15 +1,22 @@
# Class relationships to be included in the project import/export
-project_tree:
- - :issues
- - :labels
- - merge_requests:
- - :merge_request_diff
- - :merge_request_test
- - commit_statuses:
- - :commit
- - project_members:
+tree:
+ project:
+ - :issues
+ - :labels
+ - merge_requests:
+ - :merge_request_diff
+ - :merge_request_test
+ - commit_statuses:
+ - :commit
+ - project_members:
+ - :user
+ group_members:
- :user
+preloads:
+ merge_request_diff:
+ source_project:
+
included_attributes:
merge_requests:
- :id
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 76d82649c5f..f2f31e1b7f2 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
shared_examples_for 'multiple issue boards' do
- dropdown_selector = '.js-boards-selector .dropdown-menu'
-
context 'authorized user' do
before do
parent.add_maintainer(user)
@@ -20,18 +18,14 @@ shared_examples_for 'multiple issue boards' do
end
it 'shows a list of boards' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
expect(page).to have_content(board.name)
expect(page).to have_content(board2.name)
end
end
it 'switches current board' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_link board2.name
end
@@ -43,9 +37,7 @@ shared_examples_for 'multiple issue boards' do
end
it 'creates new board without detailed configuration' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_button 'Create new board'
end
@@ -57,28 +49,23 @@ shared_examples_for 'multiple issue boards' do
end
it 'deletes board' do
- click_button board.name
-
- wait_for_requests
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_button 'Delete board'
end
expect(page).to have_content('Are you sure you want to delete this board?')
click_button 'Delete'
- click_button board2.name
- page.within(dropdown_selector) do
+ wait_for_requests
+
+ in_boards_switcher_dropdown do
expect(page).not_to have_content(board.name)
expect(page).to have_content(board2.name)
end
end
it 'adds a list to the none default board' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_link board2.name
end
@@ -100,9 +87,7 @@ shared_examples_for 'multiple issue boards' do
expect(page).to have_selector('.board', count: 3)
- click_button board2.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_link board.name
end
@@ -114,9 +99,9 @@ shared_examples_for 'multiple issue boards' do
it 'maintains sidebar state over board switch' do
assert_boards_nav_active
- find('.boards-switcher').click
- wait_for_requests
- click_link board2.name
+ in_boards_switcher_dropdown do
+ click_link board2.name
+ end
assert_boards_nav_active
end
@@ -129,15 +114,24 @@ shared_examples_for 'multiple issue boards' do
end
it 'does not show action links' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
expect(page).not_to have_content('Create new board')
expect(page).not_to have_content('Delete board')
end
end
end
+ def in_boards_switcher_dropdown
+ find('.boards-switcher').click
+
+ wait_for_requests
+
+ dropdown_selector = '.js-boards-selector .dropdown-menu'
+ page.within(dropdown_selector) do
+ yield
+ end
+ end
+
def assert_boards_nav_active
expect(find('.nav-sidebar .active .active')).to have_selector('a', text: 'Boards')
end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
new file mode 100644
index 00000000000..afc6f59b773
--- /dev/null
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+#
+# Requires let variables:
+# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths"
+# * get_args
+# * other_user_get_args
+# * requests_per_period
+# * period_in_seconds
+# * period
+shared_examples_for 'rate-limited token-authenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get(*get_args) }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ get(*other_user_get_args)
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get(*get_args) }
+ end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ remote_ip: '127.0.0.1',
+ request_method: 'GET',
+ path: get_args.first,
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+end
+
+# Requires let variables:
+# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths"
+# * user
+# * url_that_requires_authentication
+# * requests_per_period
+# * period_in_seconds
+# * period
+shared_examples_for 'rate-limited web authenticated requests' do
+ before do
+ login_as(user)
+
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get url_that_requires_authentication }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ login_as(create(:user))
+
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ remote_ip: '127.0.0.1',
+ request_method: 'GET',
+ path: '/dashboard/snippets',
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
+
+ get url_that_requires_authentication
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index e6e4d9504d9..2f3fc7839c1 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -57,7 +57,7 @@ describe 'gitlab:gitaly namespace rake task' do
stub_env('CI', false)
FileUtils.mkdir_p(clone_path)
expect(Dir).to receive(:chdir).with(clone_path).and_call_original
- allow(Rails.env).to receive(:test?).and_return(false)
+ stub_rails_env('development')
end
context 'gmake is available' do
@@ -93,7 +93,7 @@ describe 'gitlab:gitaly namespace rake task' do
end
before do
- allow(Rails.env).to receive(:test?).and_return(true)
+ stub_rails_env('test')
end
it 'calls make in the gitaly directory with --no-deployment flag for bundle' do
diff --git a/spec/validators/addressable_url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index 387e84b2d04..6927a1f67a1 100644
--- a/spec/validators/addressable_url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -92,6 +92,15 @@ describe AddressableUrlValidator do
expect(badge.errors).to be_empty
expect(badge.link_url).to eq('https://127.0.0.1')
end
+
+ it 'allows urls that cannot be resolved' do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+ badge.link_url = 'http://foobar.x'
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
end
context 'when message is set' do
@@ -312,4 +321,32 @@ describe AddressableUrlValidator do
end
end
end
+
+ context 'when dns_rebind_protection is' do
+ let(:not_resolvable_url) { 'http://foobar.x' }
+ let(:validator) { described_class.new(attributes: [:link_url], dns_rebind_protection: dns_value) }
+
+ before do
+ stub_env('RSPEC_ALLOW_INVALID_URLS', 'false')
+ badge.link_url = not_resolvable_url
+
+ subject
+ end
+
+ context 'true' do
+ let(:dns_value) { true }
+
+ it 'raises error' do
+ expect(badge.errors).to be_present
+ end
+ end
+
+ context 'false' do
+ let(:dns_value) { false }
+
+ it 'allows urls that cannot be resolved' do
+ expect(badge.errors).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/validators/named_ecdsa_key_validator_spec.rb b/spec/validators/named_ecdsa_key_validator_spec.rb
new file mode 100644
index 00000000000..044c5b84a56
--- /dev/null
+++ b/spec/validators/named_ecdsa_key_validator_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe NamedEcdsaKeyValidator do
+ let(:validator) { described_class.new(attributes: [:key]) }
+ let!(:domain) { build(:pages_domain) }
+
+ subject { validator.validate_each(domain, :key, value) }
+
+ context 'with empty value' do
+ let(:value) { nil }
+
+ it 'does not add any error if value is empty' do
+ subject
+
+ expect(domain.errors).to be_empty
+ end
+ end
+
+ shared_examples 'does not add any error' do
+ it 'does not add any error' do
+ expect(value).to be_present
+
+ subject
+
+ expect(domain.errors).to be_empty
+ end
+ end
+
+ context 'when key is not EC' do
+ let(:value) { attributes_for(:pages_domain)[:key] }
+
+ include_examples 'does not add any error'
+ end
+
+ context 'with ECDSA certificate with named curve' do
+ let(:value) { attributes_for(:pages_domain, :ecdsa)[:key] }
+
+ include_examples 'does not add any error'
+ end
+
+ context 'with ECDSA certificate with explicit curve params' do
+ let(:value) { attributes_for(:pages_domain, :explicit_ecdsa)[:key] }
+
+ it 'adds errors' do
+ expect(value).to be_present
+
+ subject
+
+ expect(domain.errors[:key]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/workers/update_external_pull_requests_worker_spec.rb b/spec/workers/update_external_pull_requests_worker_spec.rb
new file mode 100644
index 00000000000..f3956bb3514
--- /dev/null
+++ b/spec/workers/update_external_pull_requests_worker_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe UpdateExternalPullRequestsWorker do
+ describe '#perform' do
+ set(:project) { create(:project, import_source: 'tanuki/repository') }
+ set(:user) { create(:user) }
+ let(:worker) { described_class.new }
+
+ before do
+ create(:external_pull_request,
+ project: project,
+ source_repository: project.import_source,
+ target_repository: project.import_source,
+ source_branch: 'feature-1',
+ target_branch: 'master')
+
+ create(:external_pull_request,
+ project: project,
+ source_repository: project.import_source,
+ target_repository: project.import_source,
+ source_branch: 'feature-1',
+ target_branch: 'develop')
+ end
+
+ subject { worker.perform(project.id, user.id, ref) }
+
+ context 'when ref is a branch' do
+ let(:ref) { 'refs/heads/feature-1' }
+ let(:create_pipeline_service) { instance_double(ExternalPullRequests::CreatePipelineService) }
+
+ it 'runs CreatePipelineService for each pull request matching the source branch and repository' do
+ expect(ExternalPullRequests::CreatePipelineService)
+ .to receive(:new)
+ .and_return(create_pipeline_service)
+ .twice
+ expect(create_pipeline_service).to receive(:execute).twice
+
+ subject
+ end
+ end
+
+ context 'when ref is not a branch' do
+ let(:ref) { 'refs/tags/v1.2.3' }
+
+ it 'does nothing' do
+ expect(ExternalPullRequests::CreatePipelineService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/tmp/.gitignore b/tmp/.gitignore
new file mode 100644
index 00000000000..a383b23fb78
--- /dev/null
+++ b/tmp/.gitignore
@@ -0,0 +1,5 @@
+*
+!*/
+!.gitignore
+!.gitkeep
+/tests/
diff --git a/yarn.lock b/yarn.lock
index 92da409f544..c64c3a6acaa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -991,15 +991,15 @@
dependencies:
vue-eslint-parser "^6.0.4"
-"@gitlab/svgs@^1.71.0":
- version "1.71.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.71.0.tgz#c8e6e8f500ea91e5cbba4ac08df533fb2e622a00"
- integrity sha512-kkeNic/FFwaqKnzwio4NE7whBOZ/toRJ8cS0587DBotajAzSYhph5ij4TCY2GTjPa33zIJ5OUr/k90C0Kr71hQ==
+"@gitlab/svgs@^1.72.0":
+ version "1.72.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.72.0.tgz#78a29fd383a5a2b31ef91670068a6fea05ba234e"
+ integrity sha512-EVKRqrXsCY6tUiVAh+lpFMJAyNXZwfEqv5NeKH5ginhALMlOunRkY5rsDllyNvgZ0DWHJS1KEKJj2oVU1ouwAg==
-"@gitlab/ui@5.21.0":
- version "5.21.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.21.0.tgz#975cf0bca3d16dd080d67ed392b9d24cd64695ac"
- integrity sha512-8TMVM+pJXf7omHgKMMZ1FiltuyMOTwfQ3iFgorQzcuhio9u35DJpWi45S2TF7m6CrlpJi7dMX3BsXLbF7ViSUw==
+"@gitlab/ui@5.21.1":
+ version "5.21.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.21.1.tgz#8215ab3eae4296845596d5b3a987d5460b030569"
+ integrity sha512-TjPVhex9sQGUVwebaJK5XuopDHWx4+Sh9N7yH5u8eXSFWa8vk11voR4qYVt7DZB7powAO/+iiXxYMLLNtXmC/g==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -11815,6 +11815,11 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
+timezone-mock@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.0.8.tgz#1b9f7af13f2bf84b7aa3d3d6e24aa17255b6037d"
+ integrity sha512-7dgx34HJPY8O/c5dbqG+I9S3TVDjrfssXmS8BNqiy8sdYvYDfM7shHpNA6VTDQWcDGyv43bE3El6YuFDQf1X3g==
+
tiny-emitter@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"