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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.yml3
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml26
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml2
-rw-r--r--CHANGELOG-EE.md4
-rw-r--r--CHANGELOG.md4
-rw-r--r--Gemfile11
-rw-r--r--Gemfile.lock34
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue53
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue51
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue11
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue63
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js5
-rw-r--r--app/assets/javascripts/jobs/components/log/log.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/charts/anomaly.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue34
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue14
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js36
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js89
-rw-r--r--app/assets/javascripts/monitoring/utils.js26
-rw-r--r--app/assets/javascripts/mr_tabs_popover/components/popover.vue64
-rw-r--r--app/assets/javascripts/mr_tabs_popover/index.js12
-rw-r--r--app/assets/javascripts/pages/groups/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js7
-rw-r--r--app/assets/javascripts/pages/projects/registry/repositories/index.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/performance_bar/index.js50
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js10
-rw-r--r--app/assets/javascripts/persistent_user_callout.js8
-rw-r--r--app/assets/javascripts/registry/list/components/app.vue (renamed from app/assets/javascripts/registry/components/app.vue)2
-rw-r--r--app/assets/javascripts/registry/list/components/collapsible_container.vue (renamed from app/assets/javascripts/registry/components/collapsible_container.vue)9
-rw-r--r--app/assets/javascripts/registry/list/components/group_empty_state.vue (renamed from app/assets/javascripts/registry/components/group_empty_state.vue)0
-rw-r--r--app/assets/javascripts/registry/list/components/project_empty_state.vue (renamed from app/assets/javascripts/registry/components/project_empty_state.vue)0
-rw-r--r--app/assets/javascripts/registry/list/components/table_registry.vue (renamed from app/assets/javascripts/registry/components/table_registry.vue)5
-rw-r--r--app/assets/javascripts/registry/list/constants.js (renamed from app/assets/javascripts/registry/constants.js)2
-rw-r--r--app/assets/javascripts/registry/list/index.js (renamed from app/assets/javascripts/registry/index.js)2
-rw-r--r--app/assets/javascripts/registry/list/stores/actions.js (renamed from app/assets/javascripts/registry/stores/actions.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/getters.js (renamed from app/assets/javascripts/registry/stores/getters.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/index.js (renamed from app/assets/javascripts/registry/stores/index.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/mutation_types.js (renamed from app/assets/javascripts/registry/stores/mutation_types.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/mutations.js (renamed from app/assets/javascripts/registry/stores/mutations.js)0
-rw-r--r--app/assets/javascripts/registry/list/stores/state.js (renamed from app/assets/javascripts/registry/stores/state.js)0
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart.vue7
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/lists.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss14
-rw-r--r--app/assets/stylesheets/pages/error_details.scss6
-rw-r--r--app/assets/stylesheets/pages/issues.scss48
-rw-r--r--app/assets/stylesheets/pages/tree.scss1
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb1
-rw-r--r--app/controllers/admin/identities_controller.rb6
-rw-r--r--app/controllers/autocomplete_controller.rb16
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/instance_statistics/conversational_development_index_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/error_tracking_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb15
-rw-r--r--app/controllers/snippets_controller.rb10
-rw-r--r--app/controllers/uploads_controller.rb1
-rw-r--r--app/finders/deployments_finder.rb48
-rw-r--r--app/finders/snippets_finder.rb53
-rw-r--r--app/helpers/application_helper.rb19
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/broadcast_messages_helper.rb4
-rw-r--r--app/helpers/dev_ops_score_helper.rb (renamed from app/helpers/conversational_development_index_helper.rb)2
-rw-r--r--app/helpers/diff_helper.rb18
-rw-r--r--app/helpers/gitlab_routing_helper.rb95
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/snippets_helper.rb32
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/models/active_session.rb50
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/application_setting_implementation.rb6
-rw-r--r--app/models/broadcast_message.rb10
-rw-r--r--app/models/ci/build.rb12
-rw-r--r--app/models/ci/persistent_ref.rb2
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/runner.rb3
-rw-r--r--app/models/clusters/applications/knative.rb10
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/models/concerns/ignorable_columns.rb45
-rw-r--r--app/models/concerns/update_project_statistics.rb28
-rw-r--r--app/models/deploy_key.rb3
-rw-r--r--app/models/deployment.rb8
-rw-r--r--app/models/dev_ops_score/card.rb (renamed from app/models/conversational_development_index/card.rb)2
-rw-r--r--app/models/dev_ops_score/idea_to_production_step.rb (renamed from app/models/conversational_development_index/idea_to_production_step.rb)2
-rw-r--r--app/models/dev_ops_score/metric.rb (renamed from app/models/conversational_development_index/metric.rb)2
-rw-r--r--app/models/environment_status.rb24
-rw-r--r--app/models/epic.rb4
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb2
-rw-r--r--app/models/import_failure.rb7
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/merge_request.rb25
-rw-r--r--app/models/merge_request/pipelines.rb60
-rw-r--r--app/models/project.rb25
-rw-r--r--app/models/project_ci_cd_setting.rb5
-rw-r--r--app/models/project_services/unify_circuit_service.rb60
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/snippet.rb12
-rw-r--r--app/models/user_callout_enums.rb3
-rw-r--r--app/policies/global_policy.rb3
-rw-r--r--app/policies/personal_snippet_policy.rb3
-rw-r--r--app/presenters/dev_ops_score/metric_presenter.rb (renamed from app/presenters/conversational_development_index/metric_presenter.rb)2
-rw-r--r--app/serializers/deployment_entity.rb11
-rw-r--r--app/serializers/environment_status_entity.rb4
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb4
-rw-r--r--app/services/error_tracking/list_issues_service.rb47
-rw-r--r--app/services/issuable/bulk_update_service.rb20
-rw-r--r--app/services/issues/base_service.rb2
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb7
-rw-r--r--app/services/projects/overwrite_project_service.rb4
-rw-r--r--app/services/projects/unlink_fork_service.rb61
-rw-r--r--app/services/projects/update_service.rb7
-rw-r--r--app/services/repair_ldap_blocked_user_service.rb19
-rw-r--r--app/services/submit_usage_ping_service.rb2
-rw-r--r--app/services/users/repair_ldap_blocked_service.rb21
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml7
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml3
-rw-r--r--app/views/award_emoji/_awards_block.html.haml1
-rw-r--r--app/views/clusters/clusters/_namespace.html.haml2
-rw-r--r--app/views/layouts/_broadcast.html.haml2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/header/_new_dropdown.haml3
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml28
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_awards_block.html.haml5
-rw-r--r--app/views/projects/merge_requests/_description.html.haml9
-rw-r--r--app/views/projects/merge_requests/_discussion_filter.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml15
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml3
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml13
-rw-r--r--app/views/projects/merge_requests/show.html.haml54
-rw-r--r--app/views/projects/merge_requests/tabs/_pane.html.haml7
-rw-r--r--app/views/projects/merge_requests/tabs/_tab.html.haml7
-rw-r--r--app/views/projects/tags/_tag.html.haml6
-rw-r--r--app/views/projects/tree/_tree_header.html.haml21
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml4
-rw-r--r--app/views/snippets/_actions.html.haml12
-rw-r--r--changelogs/unreleased/13979-dashboard-empty-state.yml5
-rw-r--r--changelogs/unreleased/17580-enable-etag-caching-notes-for-mrs.yml5
-rw-r--r--changelogs/unreleased/31611-limit-the-number-of-stored-sessions-per-user.yml5
-rw-r--r--changelogs/unreleased/31830-limit-mr-target-branches.yml5
-rw-r--r--changelogs/unreleased/32959-dismissal-ux-improvement.yml5
-rw-r--r--changelogs/unreleased/33318-make-internal-projects-poolable.yml5
-rw-r--r--changelogs/unreleased/33482-allow-text-wrapping-on-repository-tags-page.yml5
-rw-r--r--changelogs/unreleased/33718-add-new-dep-scanning-flag.yml5
-rw-r--r--changelogs/unreleased/34377-design-view-download-single-issue-design-image.yml5
-rw-r--r--changelogs/unreleased/34685-Pages-template-jekyll-outdated-and-not-working-as-expected.yml5
-rw-r--r--changelogs/unreleased/35458-expose-manual-actions-retry.yml5
-rw-r--r--changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml5
-rw-r--r--changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml5
-rw-r--r--changelogs/unreleased/36955-snowplow-custom-events-for-monitor-apm-add-metric-button-fe.yml5
-rw-r--r--changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml5
-rw-r--r--changelogs/unreleased/37057-record-import-failures.yml5
-rw-r--r--changelogs/unreleased/37313-scroll-to-bottom.yml5
-rw-r--r--changelogs/unreleased/37680-tree-control-buttons-misbehave-on-small-viewports.yml5
-rw-r--r--changelogs/unreleased/7603-make-it-easy-to-generate-and-share-the-maven-xml-for-a-library.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_assignee_select_spec-js.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_board_card_spec-js.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_board_list_common_spec-js.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_board_new_issue_spec-js.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml5
-rw-r--r--changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml5
-rw-r--r--changelogs/unreleased/Updated-hexo-project_template.yml5
-rw-r--r--changelogs/unreleased/Updated-hugo-project_template.yml5
-rw-r--r--changelogs/unreleased/add_body_data_elements_for_page_type_id_project_id_and_namespace_id.yml5
-rw-r--r--changelogs/unreleased/bvl-cache-repository-ancestor.yml5
-rw-r--r--changelogs/unreleased/chore-admin-mode-rack-attack-default-paths-migration.yml5
-rw-r--r--changelogs/unreleased/ci_template_cluster_applications.yml5
-rw-r--r--changelogs/unreleased/cleanup-monitoring-dashboard-unused-methods.yml5
-rw-r--r--changelogs/unreleased/dz-move-operations-routes.yml5
-rw-r--r--changelogs/unreleased/feat-circuit-project-service.yml5
-rw-r--r--changelogs/unreleased/filter-for-project-and-group-audit-events.yml5
-rw-r--r--changelogs/unreleased/fix-fork-link-display-bug.yml5
-rw-r--r--changelogs/unreleased/fixes-35624.yml5
-rw-r--r--changelogs/unreleased/fj-31133-snippet-content-size-limit.yml5
-rw-r--r--changelogs/unreleased/fj-37436-fix-create-personal-snippet-ability.yml5
-rw-r--r--changelogs/unreleased/fj-add-filters-to-snippets-finder.yml5
-rw-r--r--changelogs/unreleased/gitaly-2108-repos-gc-after-move.yml5
-rw-r--r--changelogs/unreleased/helm_values_default.yml5
-rw-r--r--changelogs/unreleased/hly-search-by-project-full-path.yml5
-rw-r--r--changelogs/unreleased/id-optimize-query-for-ci-pipelines.yml5
-rw-r--r--changelogs/unreleased/large_imports_rake_task.yml5
-rw-r--r--changelogs/unreleased/nicolasdular-add-target-path-to-broadcast-message.yml5
-rw-r--r--changelogs/unreleased/osw-delete-fork-relation-upon-visibility-change.yml5
-rw-r--r--changelogs/unreleased/ph-33813-moveMergeRequestDescription.yml5
-rw-r--r--changelogs/unreleased/qa-add-email-delivery-tests.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-34366.yml5
-rw-r--r--changelogs/unreleased/sh-remove-feature-flag-diverging-commits.yml5
-rw-r--r--changelogs/unreleased/sy-grafana-fix-uid.yml5
-rw-r--r--changelogs/unreleased/tz-fe-timings-performancebar.yml5
-rw-r--r--changelogs/unreleased/update-managed-namespace-prefix-copy.yml5
-rw-r--r--changelogs/unreleased/update-size-after-commit.yml5
-rw-r--r--config/initializers/sidekiq.rb5
-rw-r--r--config/routes/project.rb114
-rw-r--r--danger/changelog/Dangerfile6
-rw-r--r--db/fixtures/development/21_dev_ops_score_metrics.rb (renamed from db/fixtures/development/21_conversational_development_index_metrics.rb)6
-rw-r--r--db/migrate/20180215181245_users_name_lower_index.rb4
-rw-r--r--db/migrate/20180309121820_reschedule_commits_count_for_merge_request_diff.rb2
-rw-r--r--db/migrate/20180504195842_project_name_lower_index.rb4
-rw-r--r--db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb22
-rw-r--r--db/migrate/20190402150158_backport_enterprise_schema.rb3
-rw-r--r--db/migrate/20191014132931_remove_index_on_snippets_project_id.rb7
-rw-r--r--db/migrate/20191118173522_add_snippet_size_limit_to_application_settings.rb13
-rw-r--r--db/migrate/20191125114345_add_admin_mode_protected_path.rb54
-rw-r--r--db/migrate/20191125133353_add_target_path_to_broadcast_message.rb9
-rw-r--r--db/migrate/20191125140458_create_import_failures.rb17
-rw-r--r--db/optional_migrations/composite_primary_keys.rb4
-rw-r--r--db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb16
-rw-r--r--db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb2
-rw-r--r--db/post_migrate/20180706223200_populate_site_statistics.rb4
-rw-r--r--db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb2
-rw-r--r--db/post_migrate/20180826111825_recalculate_site_statistics.rb4
-rw-r--r--db/post_migrate/20191125024005_cleanup_deploy_access_levels_for_removed_groups.rb32
-rw-r--r--db/schema.rb18
-rw-r--r--doc/administration/auth/ldap-ee.md2
-rw-r--r--doc/administration/auth/smartcard.md19
-rw-r--r--doc/administration/custom_hooks.md2
-rw-r--r--doc/administration/gitaly/index.md5
-rw-r--r--doc/administration/gitaly/praefect.md2
-rw-r--r--doc/administration/gitaly/reference.md16
-rw-r--r--doc/administration/high_availability/README.md61
-rw-r--r--doc/administration/index.md6
-rw-r--r--doc/administration/invalidate_markdown_cache.md2
-rw-r--r--doc/administration/monitoring/performance/img/performance_bar.pngbin58439 -> 71551 bytes
-rw-r--r--doc/administration/monitoring/performance/img/performance_bar_frontend.pngbin0 -> 362077 bytes
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md6
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md4
-rw-r--r--doc/administration/packages/container_registry.md39
-rw-r--r--doc/administration/raketasks/uploads/migrate.md8
-rw-r--r--doc/administration/snippets/index.md71
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md2
-rw-r--r--doc/administration/troubleshooting/postgresql.md36
-rw-r--r--doc/api/deployments.md2
-rw-r--r--doc/api/issues.md6
-rw-r--r--doc/api/markdown.md2
-rw-r--r--doc/api/releases/index.md4
-rw-r--r--doc/api/services.md48
-rw-r--r--doc/api/settings.md3
-rw-r--r--doc/api/tags.md4
-rw-r--r--doc/ci/jenkins/index.md2
-rw-r--r--doc/ci/merge_request_pipelines/index.md6
-rw-r--r--doc/ci/variables/README.md6
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/ci/yaml/README.md5
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/api_graphql_styleguide.md96
-rw-r--r--doc/development/contributing/style_guides.md4
-rw-r--r--doc/development/cycle_analytics.md246
-rw-r--r--doc/development/database_review.md1
-rw-r--r--doc/development/documentation/index.md17
-rw-r--r--doc/development/documentation/styleguide.md2
-rw-r--r--doc/development/fe_guide/graphql.md44
-rw-r--r--doc/development/fe_guide/index.md11
-rw-r--r--doc/development/fe_guide/style/html.md53
-rw-r--r--doc/development/fe_guide/style/index.md21
-rw-r--r--doc/development/fe_guide/style/javascript.md275
-rw-r--r--doc/development/fe_guide/style/scss.md285
-rw-r--r--doc/development/fe_guide/style/vue.md418
-rw-r--r--doc/development/fe_guide/style_guide_js.md734
-rw-r--r--doc/development/fe_guide/style_guide_scss.md284
-rw-r--r--doc/development/fe_guide/tooling.md154
-rw-r--r--doc/development/fe_guide/vue.md4
-rw-r--r--doc/development/git_object_deduplication.md2
-rw-r--r--doc/development/mass_insert.md13
-rw-r--r--doc/development/merge_request_performance_guidelines.md48
-rw-r--r--doc/development/new_fe_guide/index.md4
-rw-r--r--doc/development/new_fe_guide/style/html.md56
-rw-r--r--doc/development/new_fe_guide/style/index.md18
-rw-r--r--doc/development/new_fe_guide/style/javascript.md198
-rw-r--r--doc/development/new_fe_guide/style/prettier.md101
-rw-r--r--doc/development/new_fe_guide/style/scss.md3
-rw-r--r--doc/development/new_fe_guide/style/vue.md3
-rw-r--r--doc/development/pipelines.md10
-rw-r--r--doc/development/sql.md4
-rw-r--r--doc/development/testing_guide/end_to_end/best_practices.md10
-rw-r--r--doc/development/testing_guide/end_to_end/quick_start_guide.md12
-rw-r--r--doc/development/verifying_database_capabilities.md12
-rw-r--r--doc/development/what_requires_downtime.md58
-rw-r--r--doc/integration/github.md33
-rw-r--r--doc/policy/maintenance.md16
-rw-r--r--doc/security/rack_attack.md3
-rw-r--r--doc/topics/autodevops/index.md20
-rw-r--r--doc/topics/git/useful_git_commands.md8
-rw-r--r--doc/update/patch_versions.md8
-rw-r--r--doc/user/admin_area/appearance.md8
-rw-r--r--doc/user/admin_area/broadcast_messages.md1
-rw-r--r--doc/user/admin_area/monitoring/health_check.md2
-rw-r--r--doc/user/admin_area/settings/protected_paths.md3
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md7
-rw-r--r--doc/user/application_security/container_scanning/index.md2
-rw-r--r--doc/user/application_security/dast/index.md4
-rw-r--r--doc/user/application_security/dependency_scanning/index.md1
-rw-r--r--doc/user/clusters/applications.md63
-rw-r--r--doc/user/clusters/crossplane.md8
-rw-r--r--doc/user/discussions/index.md2
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/packages/conan_repository/index.md2
-rw-r--r--doc/user/packages/maven_repository/index.md2
-rw-r--r--doc/user/packages/npm_registry/index.md2
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/profile/active_sessions.md3
-rw-r--r--doc/user/project/clusters/serverless/aws.md2
-rw-r--r--doc/user/project/deploy_boards.md2
-rw-r--r--doc/user/project/img/service_desk_disabled.pngbin11708 -> 25013 bytes
-rw-r--r--doc/user/project/img/service_desk_enabled.pngbin21514 -> 59684 bytes
-rw-r--r--doc/user/project/integrations/github.md3
-rw-r--r--doc/user/project/integrations/img/unify_circuit_configuration.pngbin0 -> 274416 bytes
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/prometheus.md8
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md2
-rw-r--r--doc/user/project/integrations/unify_circuit.md27
-rw-r--r--doc/user/project/merge_requests/img/merge_request_tab_position_v12_6.pngbin0 -> 74731 bytes
-rw-r--r--doc/user/project/merge_requests/index.md34
-rw-r--r--doc/user/project/merge_requests/merge_request_dependencies.md6
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md2
-rw-r--r--doc/user/project/milestones/index.md19
-rw-r--r--doc/user/project/operations/error_tracking.md2
-rw-r--r--doc/user/project/pages/getting_started/fork_sample_project.md2
-rw-r--r--doc/user/project/releases/index.md4
-rw-r--r--doc/user/project/repository/index.md2
-rw-r--r--doc/user/project/repository/web_editor.md2
-rw-r--r--doc/user/project/service_desk.md5
-rw-r--r--doc/user/project/settings/index.md2
-rw-r--r--doc/workflow/README.md10
-rw-r--r--lib/api/deployments.rb13
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/helpers/common_helpers.rb2
-rw-r--r--lib/api/helpers/services_helpers.rb17
-rw-r--r--lib/gitaly/server.rb14
-rw-r--r--lib/gitlab/background_migration/migrate_legacy_artifacts.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb7
-rw-r--r--lib/gitlab/ci/ansi2json/result.rb22
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml14
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml2
-rw-r--r--lib/gitlab/database/migration_helpers.rb53
-rw-r--r--lib/gitlab/database/obsolete_ignored_columns.rb9
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb10
-rw-r--r--lib/gitlab/etag_caching/router.rb4
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb26
-rw-r--r--lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb2
-rw-r--r--lib/gitlab/slash_commands/presenters/access.rb4
-rw-r--r--lib/gitlab/slash_commands/presenters/base.rb47
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_base.rb10
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_close.rb32
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_comment.rb24
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_move.rb29
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_new.rb31
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_search.rb10
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_show.rb41
-rw-r--r--lib/gitlab/slash_commands/presenters/note_base.rb10
-rw-r--r--lib/gitlab/sql/pattern.rb10
-rw-r--r--lib/gitlab/url_builder.rb21
-rw-r--r--lib/gitlab/visibility_level.rb12
-rw-r--r--lib/quality/kubernetes_client.rb36
-rw-r--r--lib/quality/test_level.rb8
-rw-r--r--lib/sentry/client.rb36
-rw-r--r--lib/sentry/pagination_parser.rb23
-rw-r--r--lib/tasks/db_obsolete_ignored_columns.rake5
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/import_export/import.rake132
-rw-r--r--locale/gitlab.pot125
-rw-r--r--package.json6
-rw-r--r--qa/Gemfile4
-rw-r--r--qa/Gemfile.lock47
-rw-r--r--qa/qa.rb4
-rw-r--r--qa/qa/flow/project.rb19
-rw-r--r--qa/qa/page/base.rb4
-rw-r--r--qa/qa/page/component/issuable/common.rb1
-rw-r--r--qa/qa/page/group/menu.rb7
-rw-r--r--qa/qa/page/mattermost/main.rb5
-rw-r--r--qa/qa/page/merge_request/show.rb22
-rw-r--r--qa/qa/page/project/issue/show.rb7
-rw-r--r--qa/qa/page/project/pipeline/index.rb6
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb2
-rw-r--r--qa/qa/runtime/browser.rb28
-rw-r--r--qa/qa/runtime/env.rb20
-rw-r--r--qa/qa/runtime/mail_hog.rb15
-rw-r--r--qa/qa/scenario/test/integration/smtp.rb13
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/create_group_with_mattermost_team_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/mail/trigger_mail_notification_spec.rb43
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb3
-rw-r--r--qa/spec/spec_helper.rb2
-rw-r--r--rubocop/cop/ignored_columns.rb20
-rw-r--r--rubocop/cop/put_project_routes_under_scope.rb43
-rw-r--r--rubocop/rubocop.rb2
-rw-r--r--scripts/review_apps/base-config.yaml16
-rwxr-xr-xscripts/review_apps/review-apps.sh22
-rwxr-xr-xscripts/trigger-build2
-rw-r--r--spec/controllers/admin/identities_controller_spec.rb4
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb72
-rw-r--r--spec/controllers/projects/error_tracking_controller_spec.rb32
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb28
-rw-r--r--spec/controllers/snippets_controller_spec.rb24
-rw-r--r--spec/db/schema_spec.rb1
-rw-r--r--spec/factories/dev_ops_score_metrics.rb (renamed from spec/factories/conversational_development_index_metrics.rb)2
-rw-r--r--spec/factories/merge_request_diff_commits.rb10
-rw-r--r--spec/features/admin/admin_broadcast_messages_spec.rb2
-rw-r--r--spec/features/instance_statistics/conversational_development_index_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb47
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb5
-rw-r--r--spec/features/snippets/show_spec.rb17
-rw-r--r--spec/finders/deployments_finder_spec.rb61
-rw-r--r--spec/finders/snippets_finder_spec.rb87
-rw-r--r--spec/fixtures/api/schemas/error_tracking/index.json4
-rw-r--r--spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gzbin0 -> 4352 bytes
-rw-r--r--spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gzbin0 -> 3837 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json38
-rw-r--r--spec/fixtures/project_export.tar.gzbin343091 -> 341315 bytes
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js156
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js1
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js3
-rw-r--r--spec/frontend/diffs/components/edit_button_spec.js3
-rw-r--r--spec/frontend/diffs/mock_data/diff_with_commit.js7
-rw-r--r--spec/frontend/diffs/mock_data/merge_request_diffs.js46
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js66
-rw-r--r--spec/frontend/error_tracking/components/list_mock.json29
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js56
-rw-r--r--spec/frontend/error_tracking/store/details/getters_spec.js14
-rw-r--r--spec/frontend/issuable_suggestions/components/app_spec.js82
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js2
-rw-r--r--spec/frontend/lib/utils/accessor_spec.js (renamed from spec/javascripts/lib/utils/accessor_spec.js)15
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js (renamed from spec/javascripts/lib/utils/dom_utils_spec.js)38
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js (renamed from spec/javascripts/lib/utils/file_upload_spec.js)6
-rw-r--r--spec/frontend/lib/utils/highlight_spec.js (renamed from spec/javascripts/lib/utils/higlight_spec.js)0
-rw-r--r--spec/frontend/lib/utils/icon_utils_spec.js (renamed from spec/javascripts/lib/utils/icon_utils_spec.js)39
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js (renamed from spec/javascripts/lib/utils/text_markdown_spec.js)8
-rw-r--r--spec/frontend/lib/utils/users_cache_spec.js (renamed from spec/javascripts/lib/utils/users_cache_spec.js)49
-rw-r--r--spec/frontend/monitoring/charts/time_series_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js18
-rw-r--r--spec/frontend/monitoring/embed/embed_spec.js2
-rw-r--r--spec/frontend/monitoring/embed/mock_data.js26
-rw-r--r--spec/frontend/monitoring/mock_data.js3
-rw-r--r--spec/frontend/monitoring/panel_type_spec.js4
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js17
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js44
-rw-r--r--spec/frontend/performance_bar/stores/performance_bar_store_spec.js17
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js1
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js28
-rw-r--r--spec/frontend/registry/list/components/__snapshots__/group_empty_state_spec.js.snap (renamed from spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap)0
-rw-r--r--spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap (renamed from spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap)0
-rw-r--r--spec/frontend/registry/list/components/app_spec.js (renamed from spec/frontend/registry/components/app_spec.js)4
-rw-r--r--spec/frontend/registry/list/components/collapsible_container_spec.js (renamed from spec/frontend/registry/components/collapsible_container_spec.js)32
-rw-r--r--spec/frontend/registry/list/components/group_empty_state_spec.js (renamed from spec/frontend/registry/components/group_empty_state_spec.js)2
-rw-r--r--spec/frontend/registry/list/components/project_empty_state_spec.js (renamed from spec/frontend/registry/components/project_empty_state_spec.js)2
-rw-r--r--spec/frontend/registry/list/components/table_registry_spec.js (renamed from spec/frontend/registry/components/table_registry_spec.js)52
-rw-r--r--spec/frontend/registry/list/mock_data.js (renamed from spec/frontend/registry/mock_data.js)0
-rw-r--r--spec/frontend/registry/list/stores/actions_spec.js (renamed from spec/frontend/registry/stores/actions_spec.js)8
-rw-r--r--spec/frontend/registry/list/stores/getters_spec.js (renamed from spec/frontend/registry/stores/getters_spec.js)2
-rw-r--r--spec/frontend/registry/list/stores/mutations_spec.js (renamed from spec/frontend/registry/stores/mutations_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js60
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js113
-rw-r--r--spec/helpers/application_helper_spec.rb84
-rw-r--r--spec/helpers/award_emoji_helper_spec.rb22
-rw-r--r--spec/helpers/diff_helper_spec.rb45
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb94
-rw-r--r--spec/helpers/snippets_helper_spec.rb101
-rw-r--r--spec/javascripts/boards/board_card_spec.js4
-rw-r--r--spec/javascripts/boards/board_list_common_spec.js3
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js3
-rw-r--r--spec/javascripts/boards/components/board_spec.js17
-rw-r--r--spec/javascripts/boards/issue_spec.js4
-rw-r--r--spec/javascripts/boards/mock_data.js14
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js145
-rw-r--r--spec/javascripts/diffs/mock_data/diff_with_commit.js10
-rw-r--r--spec/javascripts/diffs/mock_data/merge_request_diffs.js53
-rw-r--r--spec/javascripts/environments/environment_item_spec.js28
-rw-r--r--spec/javascripts/monitoring/charts/column_spec.js2
-rw-r--r--spec/javascripts/monitoring/mock_data.js30
-rw-r--r--spec/javascripts/monitoring/utils_spec.js26
-rw-r--r--spec/javascripts/user_popovers_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/clipboard_button_spec.js51
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js109
-rw-r--r--spec/lib/gitaly/server_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/ansi2json/result_spec.rb42
-rw-r--r--spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb39
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb186
-rw-r--r--spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb21
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb9
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb44
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb10
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb20
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb24
-rw-r--r--spec/lib/quality/kubernetes_client_spec.rb74
-rw-r--r--spec/lib/quality/test_level_spec.rb26
-rw-r--r--spec/lib/sentry/client_spec.rb58
-rw-r--r--spec/lib/sentry/pagination_parser_spec.rb63
-rw-r--r--spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb50
-rw-r--r--spec/models/active_session_spec.rb82
-rw-r--r--spec/models/application_setting_spec.rb2
-rw-r--r--spec/models/broadcast_message_spec.rb36
-rw-r--r--spec/models/ci/build_runner_session_spec.rb20
-rw-r--r--spec/models/ci/job_artifact_spec.rb20
-rw-r--r--spec/models/ci/persistent_ref_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb11
-rw-r--r--spec/models/concerns/ignorable_columns_spec.rb88
-rw-r--r--spec/models/deployment_spec.rb32
-rw-r--r--spec/models/dev_ops_score/metric_spec.rb (renamed from spec/models/conversational_development_index/metric_spec.rb)4
-rw-r--r--spec/models/environment_status_spec.rb78
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb4
-rw-r--r--spec/models/merge_request/pipelines_spec.rb10
-rw-r--r--spec/models/merge_request_spec.rb67
-rw-r--r--spec/models/note_spec.rb62
-rw-r--r--spec/models/project_services/unify_circuit_service_spec.rb10
-rw-r--r--spec/models/project_spec.rb50
-rw-r--r--spec/models/repository_spec.rb35
-rw-r--r--spec/models/snippet_spec.rb56
-rw-r--r--spec/policies/global_policy_spec.rb18
-rw-r--r--spec/presenters/dev_ops_score/metric_presenter_spec.rb (renamed from spec/presenters/conversational_development_index/metric_presenter_spec.rb)4
-rw-r--r--spec/requests/api/deployments_spec.rb43
-rw-r--r--spec/requests/api/settings_spec.rb5
-rw-r--r--spec/routing/environments_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb24
-rw-r--r--spec/rubocop/cop/ignored_columns_spec.rb22
-rw-r--r--spec/rubocop/cop/put_project_routes_under_scope_spec.rb48
-rw-r--r--spec/serializers/cluster_basic_entity_spec.rb2
-rw-r--r--spec/serializers/deployment_entity_spec.rb30
-rw-r--r--spec/serializers/environment_status_entity_spec.rb5
-rw-r--r--spec/serializers/pipeline_entity_spec.rb23
-rw-r--r--spec/services/error_tracking/list_issues_service_spec.rb11
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb71
-rw-r--r--spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb61
-rw-r--r--spec/services/projects/destroy_service_spec.rb5
-rw-r--r--spec/services/projects/git_deduplication_service_spec.rb59
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb169
-rw-r--r--spec/services/projects/update_service_spec.rb105
-rw-r--r--spec/services/submit_usage_ping_service_spec.rb8
-rw-r--r--spec/services/users/repair_ldap_blocked_service_spec.rb (renamed from spec/services/repair_ldap_blocked_user_service_spec.rb)2
-rw-r--r--spec/support/database_cleaner.rb33
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb2
-rw-r--r--spec/tasks/gitlab/import_export/import_rake_spec.rb112
-rw-r--r--spec/views/layouts/application.html.haml_spec.rb47
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb10
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb3
-rw-r--r--vendor/project_templates/hexo.tar.gzbin547220 -> 547436 bytes
-rw-r--r--vendor/project_templates/hugo.tar.gzbin1047952 -> 1048450 bytes
-rw-r--r--vendor/project_templates/jekyll.tar.gzbin60086 -> 60465 bytes
-rw-r--r--yarn.lock114
595 files changed, 8874 insertions, 3913 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index e131d4c07d1..db03486e9fb 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -30,7 +30,10 @@ rules:
no-else-return:
- error
- allowElseIf: true
+ import/no-cycle: warn
+ import/no-unresolved: warn
import/no-useless-path-segments: off
+ import/order: warn
lines-between-class-members: off
# Disabled for now, to make the plugin-vue 4.5 -> 5.0 update smoother
vue/no-confusing-v-for-v-if: error
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6b76853f56f..8143c9e554a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,14 +1,15 @@
-image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
+image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
stages:
- sync
- prepare
- quick-test
- test
+ - post-test
- review-prepare
- review
- qa
- - post-test
+ - post-qa
- notification
- pages
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 0b72461a9fd..c015fdd027e 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -13,7 +13,7 @@
- .default-before_script
- .assets-compile-cache
- .only:changes-code-backstage-qa
- image: registry.gitlab.com/gitlab-org/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
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-19.03.1
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 78645f48b6f..4ac187e1670 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -92,13 +92,21 @@ setup-test-env:
- .use-pg10
- .only-master
+rspec migration pg9:
+ extends: .rspec-base-pg9
+ parallel: 4
+
+rspec migration pg9-foss:
+ extends: .rspec-base-pg9-foss
+ parallel: 4
+
rspec unit pg9:
extends: .rspec-base-pg9
- parallel: 24
+ parallel: 20
rspec unit pg9-foss:
extends: .rspec-base-pg9-foss
- parallel: 24
+ parallel: 20
rspec integration pg9:
extends: .rspec-base-pg9
@@ -140,9 +148,13 @@ rspec system pg10:
- .only-ee
- .use-pg10-ee
+rspec-ee migration pg9:
+ extends: .rspec-ee-base-pg9
+ parallel: 2
+
rspec-ee unit pg9:
extends: .rspec-ee-base-pg9
- parallel: 7
+ parallel: 5
rspec-ee integration pg9:
extends: .rspec-ee-base-pg9
@@ -152,11 +164,17 @@ rspec-ee system pg9:
extends: .rspec-ee-base-pg9
parallel: 5
+rspec-ee migration pg10:
+ extends:
+ - .rspec-ee-base-pg10
+ - .only-master
+ parallel: 2
+
rspec-ee unit pg10:
extends:
- .rspec-ee-base-pg10
- .only-master
- parallel: 7
+ parallel: 5
rspec-ee integration pg10:
extends:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 142f0e1c9d4..af69fdc239c 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -275,7 +275,7 @@ parallel-spec-reports:
- .only-review
- .only:changes-code-qa
image: ruby:2.6-alpine
- stage: post-test
+ stage: post-qa
dependencies: ["review-qa-all"]
variables:
NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md
index b409dc3df4b..8bf716580a5 100644
--- a/CHANGELOG-EE.md
+++ b/CHANGELOG-EE.md
@@ -98,6 +98,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove IIFEs from jira_connect.js file. !19248 (nuwe1)
+## 12.4.5
+
+- No changes.
+
## 12.4.3
### Fixed (2 changes)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8abf4fc45ee..8ee5ed06417 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -377,6 +377,10 @@ entry.
- Change selects from default browser style to custom style.
+## 12.4.5
+
+- No changes.
+
## 12.4.3
### Fixed (2 changes)
diff --git a/Gemfile b/Gemfile
index 8ed9603856e..19b80dd3bd8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -170,8 +170,8 @@ group :unicorn do
end
group :puma do
- gem 'puma', '~> 3.12', require: false
- gem 'puma_worker_killer', require: false
+ gem 'puma', '~> 4.3.0', require: false
+ gem 'puma_worker_killer', '~> 0.1.1', require: false
gem 'rack-timeout', require: false
end
@@ -312,8 +312,7 @@ gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader', '~> 1.4.0'
# Perf bar
-# https://gitlab.com/gitlab-org/gitlab/issues/13996
-gem 'gitlab-peek', '~> 0.0.1', require: 'peek'
+gem 'peek', '~> 1.1'
# Snowplow events tracking
gem 'snowplow-tracker', '~> 0.6.1'
@@ -381,7 +380,7 @@ group :development, :test do
gem 'knapsack', '~> 1.17'
- gem 'stackprof', '~> 0.2.10', require: false
+ gem 'stackprof', '~> 0.2.13', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
@@ -402,7 +401,7 @@ group :test do
gem 'capybara', '~> 3.22.0'
gem 'capybara-screenshot', '~> 1.0.22'
- gem 'selenium-webdriver', '~> 3.141'
+ gem 'selenium-webdriver', '~> 3.142'
gem 'shoulda-matchers', '~> 4.0.1', require: false
gem 'email_spec', '~> 2.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index ba40ebc6cb5..054a491a019 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -143,8 +143,7 @@ GEM
cause (0.1)
character_set (1.1.2)
charlock_holmes (0.7.6)
- childprocess (0.9.0)
- ffi (~> 1.0, >= 1.0.11)
+ childprocess (3.0.0)
chunky_png (1.3.5)
citrus (3.0.2)
claide (1.0.3)
@@ -286,7 +285,7 @@ GEM
fast_blank (1.0.0)
fast_gettext (1.6.0)
ffaker (2.10.0)
- ffi (1.11.1)
+ ffi (1.11.3)
flipper (0.17.1)
flipper-active_record (0.17.1)
activerecord (>= 4.2, < 7)
@@ -373,8 +372,6 @@ GEM
gitlab-license (1.0.0)
gitlab-markup (1.7.0)
gitlab-net-dns (0.9.1)
- gitlab-peek (0.0.1)
- railties (>= 4.0.0)
gitlab-sidekiq-fetcher (0.5.2)
sidekiq (~> 5)
gitlab-styles (2.8.0)
@@ -724,6 +721,8 @@ GEM
parser (2.6.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
+ peek (1.1.0)
+ railties (>= 4.0.0)
pg (1.1.4)
po_to_json (1.0.1)
json (>= 1.6.0)
@@ -749,10 +748,11 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.1.1)
- puma (3.12.0)
- puma_worker_killer (0.1.0)
+ puma (4.3.0)
+ nio4r (~> 2.0)
+ puma_worker_killer (0.1.1)
get_process_mem (~> 0.2)
- puma (>= 2.7, < 4)
+ puma (>= 2.7, < 5)
pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6)
rack (2.0.7)
@@ -956,9 +956,9 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- selenium-webdriver (3.141.0)
- childprocess (~> 0.5)
- rubyzip (~> 1.2, >= 1.2.2)
+ selenium-webdriver (3.142.6)
+ childprocess (>= 0.5, < 4.0)
+ rubyzip (>= 1.2.2)
sentry-raven (2.9.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
@@ -1002,7 +1002,7 @@ GEM
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sshkey (2.0.0)
- stackprof (0.2.10)
+ stackprof (0.2.13)
state_machines (0.5.0)
state_machines-activemodel (0.7.1)
activemodel (>= 4.1)
@@ -1200,7 +1200,6 @@ DEPENDENCIES
gitlab-license (~> 1.0)
gitlab-markup (~> 1.7.0)
gitlab-net-dns (~> 0.9.1)
- gitlab-peek (~> 0.0.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 2.7)
gitlab_chronic_duration (~> 0.10.6.2)
@@ -1275,13 +1274,14 @@ DEPENDENCIES
omniauth_crowd (~> 2.2.0)
omniauth_openid_connect (~> 0.3.3)
org-ruby (~> 0.9.12)
+ peek (~> 1.1)
pg (~> 1.1)
premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.9.10)
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.4)
- puma (~> 3.12)
- puma_worker_killer
+ puma (~> 4.3.0)
+ puma_worker_killer (~> 0.1.1)
rack (~> 2.0.7)
rack-attack (~> 6.2.0)
rack-cors (~> 1.0.0)
@@ -1325,7 +1325,7 @@ DEPENDENCIES
sassc-rails (~> 2.1.0)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
- selenium-webdriver (~> 3.141)
+ selenium-webdriver (~> 3.142)
sentry-raven (~> 2.9)
settingslogic (~> 2.0.9)
shoulda-matchers (~> 4.0.1)
@@ -1339,7 +1339,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0)
sshkey (~> 2.0)
- stackprof (~> 0.2.10)
+ stackprof (~> 0.2.13)
state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.10.0)
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 922c907bb36..fd45e098758 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -133,7 +133,7 @@ export default {
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
- class="qa-project-deploy-keys"
+ data-qa-selector="project_deploy_keys"
/>
</template>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 748673f05bb..d480984da70 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -43,16 +43,21 @@ export default {
mixins: [environmentItemMixin],
props: {
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
model: {
type: Object,
required: true,
default: () => ({}),
},
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
+ tableData: {
+ type: Object,
+ required: true,
},
},
@@ -447,9 +452,13 @@ export default {
class="gl-responsive-table-row"
role="row"
>
- <div class="table-section section-wrap section-15 text-truncate" role="gridcell">
+ <div
+ class="table-section section-wrap text-truncate"
+ :class="tableData.name.spacing"
+ role="gridcell"
+ >
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader">
- {{ s__('Environments|Environment') }}
+ {{ tableData.name.title }}
</div>
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
@@ -489,7 +498,8 @@ export default {
</div>
<div
- class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
+ class="table-section deployment-column d-none d-sm-none d-md-block"
+ :class="tableData.deploy.spacing"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID" class="text-break-word">
@@ -508,7 +518,11 @@ export default {
</span>
</div>
- <div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell">
+ <div
+ class="table-section d-none d-sm-none d-md-block"
+ :class="tableData.build.spacing"
+ role="gridcell"
+ >
<a v-if="shouldRenderBuildName" :href="buildPath" class="build-link cgray">
<tooltip-on-truncate
:title="buildName"
@@ -522,8 +536,14 @@ export default {
</a>
</div>
- <div v-if="!model.isFolder" class="table-section section-20" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ s__('Environments|Commit') }}</div>
+ <div
+ v-if="!model.isFolder"
+ class="table-section"
+ :class="tableData.commit.spacing"
+ role="gridcell"
+ >
+ <div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
+
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
@@ -539,8 +559,14 @@ export default {
</div>
</div>
- <div v-if="!model.isFolder" class="table-section section-10" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ s__('Environments|Updated') }}</div>
+ <div
+ v-if="!model.isFolder"
+ class="table-section"
+ :class="tableData.date.spacing"
+ role="gridcell"
+ >
+ <div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
+
<span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content">
{{ deployedDate }}
</span>
@@ -548,7 +574,8 @@ export default {
<div
v-if="!model.isFolder && displayEnvironmentActions"
- class="table-section section-30 table-button-footer"
+ class="table-section table-button-footer"
+ :class="tableData.actions.spacing"
role="gridcell"
>
<div class="btn-group table-action-buttons" role="group">
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 4464f5e5578..2d2e09c6190 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -4,6 +4,7 @@
*/
import { GlLoadingIcon } from '@gitlab/ui';
import _ from 'underscore';
+import { s__ } from '~/locale';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import EnvironmentItem from './environment_item.vue';
@@ -41,6 +42,34 @@ export default {
: env,
);
},
+ tableData() {
+ return {
+ // percent spacing for cols, should add up to 100
+ name: {
+ title: s__('Environments|Environment'),
+ spacing: 'section-15',
+ },
+ deploy: {
+ title: s__('Environments|Deployment'),
+ spacing: 'section-10',
+ },
+ build: {
+ title: s__('Environments|Job'),
+ spacing: 'section-15',
+ },
+ commit: {
+ title: s__('Environments|Commit'),
+ spacing: 'section-20',
+ },
+ date: {
+ title: s__('Environments|Updated'),
+ spacing: 'section-10',
+ },
+ actions: {
+ spacing: 'section-30',
+ },
+ };
+ },
},
methods: {
folderUrl(model) {
@@ -79,20 +108,20 @@ export default {
<template>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
- <div class="table-section section-15 environments-name" role="columnheader">
- {{ s__('Environments|Environment') }}
+ <div class="table-section" :class="tableData.name.spacing" role="columnheader">
+ {{ tableData.name.title }}
</div>
- <div class="table-section section-10 environments-deploy" role="columnheader">
- {{ s__('Environments|Deployment') }}
+ <div class="table-section" :class="tableData.deploy.spacing" role="columnheader">
+ {{ tableData.deploy.title }}
</div>
- <div class="table-section section-15 environments-build" role="columnheader">
- {{ s__('Environments|Job') }}
+ <div class="table-section" :class="tableData.build.spacing" role="columnheader">
+ {{ tableData.build.title }}
</div>
- <div class="table-section section-20 environments-commit" role="columnheader">
- {{ s__('Environments|Commit') }}
+ <div class="table-section" :class="tableData.commit.spacing" role="columnheader">
+ {{ tableData.commit.title }}
</div>
- <div class="table-section section-10 environments-date" role="columnheader">
- {{ s__('Environments|Updated') }}
+ <div class="table-section" :class="tableData.date.spacing" role="columnheader">
+ {{ tableData.date.title }}
</div>
</div>
<template v-for="(model, i) in sortedEnvironments" :model="model">
@@ -101,6 +130,7 @@ export default {
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
+ :table-data="tableData"
/>
<div
@@ -132,6 +162,7 @@ export default {
:key="`env-item-${i}-${index}`"
:model="children"
:can-read-environment="canReadEnvironment"
+ :table-data="tableData"
/>
<div :key="`sub-div-${i}`">
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 a001b315d4f..9d8e5396dea 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -8,7 +8,6 @@ import {
GlTable,
GlSearchBoxByClick,
} from '@gitlab/ui';
-import { visitUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
@@ -76,8 +75,8 @@ export default {
this.startPolling(`${this.indexPath}?search_term=${this.errorSearchQuery}`);
},
trackViewInSentryOptions,
- viewDetails(errorId) {
- visitUrl(`error_tracking/${errorId}/details`);
+ getDetailsLink(errorId) {
+ return `error_tracking/${errorId}/details`;
},
},
};
@@ -129,11 +128,7 @@ export default {
</template>
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
- <gl-link
- class="d-flex text-dark"
- target="_blank"
- @click="viewDetails(errors.item.id)"
- >
+ <gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)">
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
</gl-link>
<span class="text-secondary text-truncate">
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace.vue b/app/assets/javascripts/error_tracking/components/stacktrace.vue
index 6b71967624f..f58d54f2933 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace.vue
@@ -27,6 +27,8 @@ export default {
:lines="entry.context"
:file-path="entry.filename"
:error-line="entry.lineNo"
+ :error-fn="entry.function"
+ :error-column="entry.colNo"
:expanded="isFirstEntry(index)"
/>
</div>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index ad542c579a9..9ed5b26a1c2 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,4 +1,5 @@
<script>
+import { __, sprintf } from '~/locale';
import { GlTooltip } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -22,9 +23,20 @@ export default {
type: String,
required: true,
},
+ errorFn: {
+ type: String,
+ required: false,
+ default: '',
+ },
errorLine: {
type: Number,
- required: true,
+ required: false,
+ default: 0,
+ },
+ errorColumn: {
+ type: Number,
+ required: false,
+ default: 0,
},
expanded: {
type: Boolean,
@@ -38,12 +50,23 @@ export default {
};
},
computed: {
- linesLength() {
- return this.lines.length;
+ hasCode() {
+ return Boolean(this.lines.length);
},
collapseIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
+ noCodeFn() {
+ return this.errorFn ? sprintf(__('in %{errorFn} '), { errorFn: this.errorFn }) : '';
+ },
+ noCodeLine() {
+ return this.errorLine
+ ? sprintf(__('at line %{errorLine}%{errorColumn}'), {
+ errorLine: this.errorLine,
+ errorColumn: this.errorColumn ? `:${this.errorColumn}` : '',
+ })
+ : '';
+ },
},
methods: {
isHighlighted(lineNum) {
@@ -66,27 +89,31 @@ export default {
<template>
<div class="file-holder">
<div ref="header" class="file-title file-title-flex-parent">
- <div class="file-header-content ">
- <div class="d-inline-block cursor-pointer" @click="toggle()">
+ <div class="file-header-content d-flex align-content-center">
+ <div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
</div>
- <div class="d-inline-block append-right-4">
- <file-icon
- :file-name="filePath"
- :size="18"
- aria-hidden="true"
- css-classes="append-right-5"
- />
- <strong v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
- {{ filePath }}
- </strong>
- </div>
-
+ <file-icon
+ :file-name="filePath"
+ :size="18"
+ aria-hidden="true"
+ css-classes="append-right-5"
+ />
+ <strong
+ v-gl-tooltip
+ :title="filePath"
+ class="file-title-name d-inline-block overflow-hidden text-truncate"
+ :class="{ 'limited-width': !hasCode }"
+ data-container="body"
+ >
+ {{ filePath }}
+ </strong>
<clipboard-button
:title="__('Copy file path')"
:text="filePath"
- css-class="btn-default btn-transparent btn-clipboard"
+ css-class="btn-default btn-transparent btn-clipboard position-static"
/>
+ <span v-if="!hasCode" class="text-tertiary">{{ noCodeFn }}{{ noCodeLine }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
index 7d13439d721..a36c84dc28c 100644
--- a/app/assets/javascripts/error_tracking/store/details/getters.js
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -1,3 +1,6 @@
-export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse();
+export const stacktrace = state =>
+ state.stacktraceData.stack_trace_entries
+ ? state.stacktraceData.stack_trace_entries.reverse()
+ : [];
export default () => {};
diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue
index 03a697d11ed..eb0de53f36a 100644
--- a/app/assets/javascripts/jobs/components/log/log.vue
+++ b/app/assets/javascripts/jobs/components/log/log.vue
@@ -9,7 +9,12 @@ export default {
LogLine,
},
computed: {
- ...mapState(['traceEndpoint', 'trace', 'isTraceComplete']),
+ ...mapState([
+ 'traceEndpoint',
+ 'trace',
+ 'isTraceComplete',
+ 'isScrolledToBottomBeforeReceivingTrace',
+ ]),
},
updated() {
this.$nextTick(() => {
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
index b8767f48c5f..1df7ca37a98 100644
--- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -29,7 +29,7 @@ const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})
* time series chart, the boundary band shows the normal
* range of values the metric should take.
*
- * This component accepts 3 queries, which contain the
+ * This component accepts 3 metrics, which contain the
* "metric", "upper" limit and "lower" limit.
*
* The upper and lower series are "stacked areas" visually
@@ -62,10 +62,10 @@ export default {
},
computed: {
series() {
- return this.graphData.queries.map(query => {
- const values = query.result[0] ? query.result[0].values : [];
+ return this.graphData.metrics.map(metric => {
+ const values = metric.result && metric.result[0] ? metric.result[0].values : [];
return {
- label: query.label,
+ label: metric.label,
// NaN values may disrupt avg., max. & min. calculations in the legend, filter them out
data: values.filter(([, value]) => !Number.isNaN(value)),
};
@@ -83,7 +83,7 @@ export default {
return min < 0 ? -min : 0;
},
metricData() {
- const originalMetricQuery = this.graphData.queries[0];
+ const originalMetricQuery = this.graphData.metrics[0];
const metricQuery = { ...originalMetricQuery };
metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
@@ -93,7 +93,7 @@ export default {
return {
...this.graphData,
type: 'line-chart',
- queries: [metricQuery],
+ metrics: [metricQuery],
};
},
metricSeriesConfig() {
diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue
index ee6aaeb7dde..eb407ad1d7f 100644
--- a/app/assets/javascripts/monitoring/components/charts/column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/column.vue
@@ -32,8 +32,8 @@ export default {
},
computed: {
chartData() {
- const queryData = this.graphData.queries.reduce((acc, query) => {
- const series = makeDataSeries(query.result, {
+ const queryData = this.graphData.metrics.reduce((acc, query) => {
+ const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
});
@@ -45,13 +45,13 @@ export default {
};
},
xAxisTitle() {
- return this.graphData.queries[0].result[0].x_label !== undefined
- ? this.graphData.queries[0].result[0].x_label
+ return this.graphData.metrics[0].result[0].x_label !== undefined
+ ? this.graphData.metrics[0].result[0].x_label
: '';
},
yAxisTitle() {
- return this.graphData.queries[0].result[0].y_label !== undefined
- ? this.graphData.queries[0].result[0].y_label
+ return this.graphData.metrics[0].result[0].y_label !== undefined
+ ? this.graphData.metrics[0].result[0].y_label
: '';
},
xAxisType() {
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index b8158247e49..6ab5aaeba1d 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -24,7 +24,7 @@ export default {
},
computed: {
chartData() {
- return this.queries.result.reduce(
+ return this.metrics.result.reduce(
(acc, result, i) => [...acc, ...result.values.map((value, j) => [i, j, value[1]])],
[],
);
@@ -36,7 +36,7 @@ export default {
return this.graphData.y_label || '';
},
xAxisLabels() {
- return this.queries.result.map(res => Object.values(res.metric)[0]);
+ return this.metrics.result.map(res => Object.values(res.metric)[0]);
},
yAxisLabels() {
return this.result.values.map(val => {
@@ -46,10 +46,10 @@ export default {
});
},
result() {
- return this.queries.result[0];
+ return this.metrics.result[0];
},
- queries() {
- return this.graphData.queries[0];
+ metrics() {
+ return this.graphData.metrics[0];
},
},
};
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index 076682820e6..e75ddb05808 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -17,7 +17,7 @@ export default {
},
computed: {
queryInfo() {
- return this.graphData.queries[0];
+ return this.graphData.metrics[0];
},
engineeringNotation() {
return `${roundOffFloat(this.queryInfo.result[0].value[1], 1)}${this.queryInfo.unit}`;
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 067a727fe16..ed01d0ee553 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -105,7 +105,7 @@ export default {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
// Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
- return this.graphData.queries.reduce((acc, query) => {
+ return this.graphData.metrics.reduce((acc, query) => {
const { appearance } = query;
const lineType =
appearance && appearance.line && appearance.line.type
@@ -121,7 +121,7 @@ export default {
? appearance.area.opacity
: undefined,
};
- const series = makeDataSeries(query.result, {
+ const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
lineStyle: {
type: lineType,
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 26e2c2568c1..c08b471bd51 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -11,19 +11,19 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
export default {
components: {
@@ -252,14 +252,9 @@ export default {
'setEndpoints',
'setPanelGroupMetrics',
]),
- chartsWithData(charts) {
- return charts.filter(chart =>
- chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
- );
- },
- updateMetrics(key, metrics) {
+ updateMetrics(key, panels) {
this.setPanelGroupMetrics({
- metrics,
+ panels,
key,
});
},
@@ -294,14 +289,18 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
+ chartsWithData(panels) {
+ return panels.filter(panel =>
+ panel.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
+ );
+ },
groupHasData(group) {
- return this.chartsWithData(group.metrics).length > 0;
+ return this.chartsWithData(group.panels).length > 0;
},
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
- downloadCSVOptions,
- generateLinkToChartOptions,
+ getAddMetricTrackingOptions,
},
addMetric: {
title: s__('Metrics|Add metric'),
@@ -393,9 +392,10 @@ export default {
</gl-button>
<gl-button
v-if="addingMetricsAvailable"
+ ref="addMetricBtn"
v-gl-modal="$options.addMetric.modalId"
variant="outline-success"
- class="mr-2 mt-1 js-add-metric-button"
+ class="mr-2 mt-1"
>
{{ $options.addMetric.title }}
</gl-button>
@@ -415,6 +415,8 @@ export default {
<div slot="modal-footer">
<gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button>
<gl-button
+ ref="submitCustomMetricsFormBtn"
+ v-track-event="getAddMetricTrackingOptions()"
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
@@ -457,14 +459,14 @@ export default {
:collapse-group="groupHasData(groupData)"
>
<vue-draggable
- :value="groupData.metrics"
+ :value="groupData.panels"
group="metrics-dashboard"
:component-data="{ attrs: { class: 'row mx-0 w-100' } }"
:disabled="!isRearrangingPanels"
@input="updateMetrics(groupData.key, $event)"
>
<div
- v-for="(graphData, graphIndex) in groupData.metrics"
+ v-for="(graphData, graphIndex) in groupData.panels"
:key="`panel-type-${graphIndex}`"
class="col-12 col-lg-6 px-2 mb-2 draggable"
:class="{ 'draggable-enabled': isRearrangingPanels }"
@@ -473,7 +475,7 @@ export default {
<div
v-if="isRearrangingPanels"
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
- @click="removeGraph(groupData.metrics, graphIndex)"
+ @click="removeGraph(groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index f75839c7c6b..581b2093d44 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -37,11 +37,14 @@ export default {
computed: {
...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']),
charts() {
+ if (!this.dashboard || !this.dashboard.panel_groups) {
+ return [];
+ }
const groupWithMetrics = this.dashboard.panel_groups.find(group =>
- group.metrics.find(chart => this.chartHasData(chart)),
- ) || { metrics: [] };
+ group.panels.find(chart => this.chartHasData(chart)),
+ ) || { panels: [] };
- return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart));
+ return groupWithMetrics.panels.filter(chart => this.chartHasData(chart));
},
isSingleChart() {
return this.charts.length === 1;
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index effcf334cbc..fab1dd0f981 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -54,10 +54,14 @@ export default {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
graphDataHasMetrics() {
- return this.graphData.queries[0].result.length > 0;
+ return (
+ this.graphData.metrics &&
+ this.graphData.metrics[0].result &&
+ this.graphData.metrics[0].result.length > 0
+ );
},
csvText() {
- const chartData = this.graphData.queries[0].result[0].values;
+ const chartData = this.graphData.metrics[0].result[0].values;
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
return chartData.reduce((csv, data) => {
@@ -112,7 +116,7 @@ export default {
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
- :thresholds="getGraphAlertValues(graphData.queries)"
+ :thresholds="getGraphAlertValues(graphData.metrics)"
group-id="panel-type-chart"
>
<div class="d-flex align-items-center">
@@ -120,8 +124,8 @@ export default {
v-if="alertWidgetAvailable && graphData"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.queries"
- :alerts-to-manage="getGraphAlerts(graphData.queries)"
+ :relevant-queries="graphData.metrics"
+ :alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<gl-dropdown
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 696af5aed75..bfa76aa7cea 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,9 +1,13 @@
import Vue from 'vue';
import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
-import { normalizeMetrics, normalizeMetric, normalizeQueryResult } from './utils';
+import { normalizeMetric, normalizeQueryResult } from './utils';
-const normalizePanel = panel => panel.metrics.map(normalizeMetric);
+const normalizePanelMetrics = (metrics, defaultLabel) =>
+ metrics.map(metric => ({
+ ...normalizeMetric(metric),
+ label: metric.label || defaultLabel,
+ }));
export default {
[types.REQUEST_METRICS_DATA](state) {
@@ -13,28 +17,18 @@ export default {
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
state.dashboard.panel_groups = groupData.map((group, i) => {
const key = `${slugify(group.group || 'default')}-${i}`;
- let { metrics = [], panels = [] } = group;
+ let { panels = [] } = group;
// each panel has metric information that needs to be normalized
-
panels = panels.map(panel => ({
...panel,
- metrics: normalizePanel(panel),
- }));
-
- // for backwards compatibility, and to limit Vue template changes:
- // for each group alias panels to metrics
- // for each panel alias metrics to queries
- metrics = panels.map(panel => ({
- ...panel,
- queries: panel.metrics,
+ metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
}));
return {
...group,
panels,
key,
- metrics: normalizeMetrics(metrics),
};
});
@@ -58,6 +52,7 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
state.environments = [];
},
+
[types.SET_QUERY_RESULT](state, { metricId, result }) {
if (!metricId || !result || result.length === 0) {
return;
@@ -65,14 +60,17 @@ export default {
state.showEmptyState = false;
+ /**
+ * Search the dashboard state for a matching id
+ */
state.dashboard.panel_groups.forEach(group => {
- group.metrics.forEach(metric => {
- metric.queries.forEach(query => {
- if (query.metric_id === metricId) {
+ group.panels.forEach(panel => {
+ panel.metrics.forEach(metric => {
+ if (metric.metric_id === metricId) {
state.metricsWithData.push(metricId);
// ensure dates/numbers are correctly formatted for charts
const normalizedResults = result.map(normalizeQueryResult);
- Vue.set(query, 'result', Object.freeze(normalizedResults));
+ Vue.set(metric, 'result', Object.freeze(normalizedResults));
}
});
});
@@ -101,6 +99,6 @@ export default {
},
[types.SET_PANEL_GROUP_METRICS](state, payload) {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
- panelGroup.metrics = payload.metrics;
+ panelGroup.panels = payload.panels;
},
};
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 8a396b15a31..3300d2032d0 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -1,83 +1,21 @@
import _ from 'underscore';
-function checkQueryEmptyData(query) {
- return {
- ...query,
- result: query.result.filter(timeSeries => {
- const newTimeSeries = timeSeries;
- const hasValue = series =>
- !Number.isNaN(series[1]) && (series[1] !== null || series[1] !== undefined);
- const hasNonNullValue = timeSeries.values.find(hasValue);
-
- newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
-
- return newTimeSeries.values.length > 0;
- }),
- };
-}
-
-function removeTimeSeriesNoData(queries) {
- return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
-}
-
-// Metrics and queries are currently stored 1:1, so `queries` is an array of length one.
-// We want to group queries onto a single chart by title & y-axis label.
-// This function will no longer be required when metrics:queries are 1:many,
-// though there is no consequence if the function stays in use.
-// @param metrics [Array<Object>]
-// Ex) [
-// { id: 1, title: 'title', y_label: 'MB', queries: [{ ...query1Attrs }] },
-// { id: 2, title: 'title', y_label: 'MB', queries: [{ ...query2Attrs }] },
-// { id: 3, title: 'new title', y_label: 'MB', queries: [{ ...query3Attrs }] }
-// ]
-// @return [Array<Object>]
-// Ex) [
-// { title: 'title', y_label: 'MB', queries: [{ metricId: 1, ...query1Attrs },
-// { metricId: 2, ...query2Attrs }] },
-// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]}
-// ]
-export function groupQueriesByChartInfo(metrics) {
- const metricsByChart = metrics.reduce((accumulator, metric) => {
- const { queries, ...chart } = metric;
-
- const chartKey = `${chart.title}|${chart.y_label}`;
- accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
-
- queries.forEach(queryAttrs => {
- let metricId;
-
- if (chart.id) {
- metricId = chart.id.toString();
- } else if (queryAttrs.metric_id) {
- metricId = queryAttrs.metric_id.toString();
- } else {
- metricId = null;
- }
-
- accumulator[chartKey].queries.push({ metricId, ...queryAttrs });
- });
-
- return accumulator;
- }, {});
-
- return Object.values(metricsByChart);
-}
-
export const uniqMetricsId = metric => `${metric.metric_id}_${metric.id}`;
/**
- * Not to confuse with normalizeMetrics (plural)
* Metrics loaded from project-defined dashboards do not have a metric_id.
* This method creates a unique ID combining metric_id and id, if either is present.
* This is hopefully a temporary solution until BE processes metrics before passing to fE
* @param {Object} metric - metric
* @returns {Object} - normalized metric with a uniqueID
*/
+
export const normalizeMetric = (metric = {}) =>
_.omit(
{
...metric,
metric_id: uniqMetricsId(metric),
+ metricId: uniqMetricsId(metric),
},
'id',
);
@@ -93,6 +31,11 @@ export const normalizeQueryResult = timeSeries => {
Number(value),
]),
};
+ // Check result for empty data
+ normalizedResult.values = normalizedResult.values.filter(series => {
+ const hasValue = d => !Number.isNaN(d[1]) && (d[1] !== null || d[1] !== undefined);
+ return series.find(hasValue);
+ });
} else if (timeSeries.value) {
normalizedResult = {
...timeSeries,
@@ -102,21 +45,3 @@ export const normalizeQueryResult = timeSeries => {
return normalizedResult;
};
-
-export const normalizeMetrics = metrics => {
- const groupedMetrics = groupQueriesByChartInfo(metrics);
-
- return groupedMetrics.map(metric => {
- const queries = metric.queries.map(query => ({
- ...query,
- // custom metrics do not require a label, so we should ensure this attribute is defined
- label: query.label || metric.y_label,
- result: (query.result || []).map(normalizeQueryResult),
- }));
-
- return {
- ...metric,
- queries: removeTimeSeriesNoData(queries),
- };
- });
-};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 2ae1647011d..c824d6d4ddb 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -72,10 +72,9 @@ export const ISODateToString = date => dateformat(date, dateFormats.dateTimePick
*/
export const graphDataValidatorForValues = (isValues, graphData) => {
const responseValueKeyName = isValues ? 'value' : 'values';
-
return (
- Array.isArray(graphData.queries) &&
- graphData.queries.filter(query => {
+ Array.isArray(graphData.metrics) &&
+ graphData.metrics.filter(query => {
if (Array.isArray(query.result)) {
return (
query.result.filter(res => Array.isArray(res[responseValueKeyName])).length ===
@@ -83,7 +82,7 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
);
}
return false;
- }).length === graphData.queries.length
+ }).length === graphData.metrics.filter(query => query.result).length
);
};
@@ -116,6 +115,7 @@ export const generateLinkToChartOptions = chartLink => {
/**
* Tracks snowplow event when user downloads CSV of cluster metric
* @param {String} chart title that will be sent as a property for the event
+ * @return {Object} config object for event tracking
*/
export const downloadCSVOptions = title => {
const isCLusterHealthBoard = isClusterHealthBoard();
@@ -131,7 +131,19 @@ export const downloadCSVOptions = title => {
};
/**
- * This function validates the graph data contains exactly 3 queries plus
+ * Generate options for snowplow to track adding a new metric via the dashboard
+ * custom metric modal
+ * @return {Object} config object for event tracking
+ */
+export const getAddMetricTrackingOptions = () => ({
+ category: document.body.dataset.page,
+ action: 'click_button',
+ label: 'add_new_metric',
+ property: 'modal',
+});
+
+/**
+ * This function validates the graph data contains exactly 3 metrics plus
* value validations from graphDataValidatorForValues.
* @param {Object} isValues
* @param {Object} graphData the graph data response from a prometheus request
@@ -140,8 +152,8 @@ export const downloadCSVOptions = title => {
export const graphDataValidatorForAnomalyValues = graphData => {
const anomalySeriesCount = 3; // metric, upper, lower
return (
- graphData.queries &&
- graphData.queries.length === anomalySeriesCount &&
+ graphData.metrics &&
+ graphData.metrics.length === anomalySeriesCount &&
graphDataValidatorForValues(false, graphData)
);
};
diff --git a/app/assets/javascripts/mr_tabs_popover/components/popover.vue b/app/assets/javascripts/mr_tabs_popover/components/popover.vue
new file mode 100644
index 00000000000..da1e1e70993
--- /dev/null
+++ b/app/assets/javascripts/mr_tabs_popover/components/popover.vue
@@ -0,0 +1,64 @@
+<script>
+import { GlPopover, GlButton, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlPopover,
+ GlButton,
+ GlLink,
+ Icon,
+ },
+ props: {
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ featureId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showPopover: false,
+ };
+ },
+ mounted() {
+ setTimeout(() => {
+ this.showPopover = true;
+ }, 2000);
+ },
+ methods: {
+ onDismiss() {
+ this.showPopover = false;
+
+ axios.post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-popover target="#diffs-tab" placement="bottom" :show="showPopover">
+ <p class="mb-2">
+ {{
+ __(
+ 'Now you can access the merge request navigation tabs at the top, where they’re easier to find.',
+ )
+ }}
+ </p>
+ <p>
+ <gl-link href="https://gitlab.com/gitlab-org/gitlab/issues/36125" target="_blank">
+ {{ __('More information and share feedback') }}
+ <icon name="external-link" :size="10" />
+ </gl-link>
+ </p>
+ <gl-button variant="primary" size="sm" @click="onDismiss">
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+</template>
diff --git a/app/assets/javascripts/mr_tabs_popover/index.js b/app/assets/javascripts/mr_tabs_popover/index.js
new file mode 100644
index 00000000000..9ee0ba046f0
--- /dev/null
+++ b/app/assets/javascripts/mr_tabs_popover/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import Popover from './components/popover.vue';
+
+export default el =>
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(Popover, {
+ props: { dismissEndpoint: el.dataset.dismissEndpoint, featureId: el.dataset.featureId },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js
index b663defad0e..635513afd95 100644
--- a/app/assets/javascripts/pages/groups/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js
@@ -1,3 +1,3 @@
-import initRegistryImages from '~/registry';
+import initRegistryImages from '~/registry/list';
document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 16034313af2..1f8befc07c8 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -6,6 +6,7 @@ import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph';
+import initPopover from '~/mr_tabs_popover';
import initWidget from '../../../vue_merge_request_widget';
export default function() {
@@ -21,4 +22,10 @@ export default function() {
howToMerge();
initWidget();
initSourcegraph();
+
+ const tabHighlightEl = document.querySelector('.js-tabs-feature-highlight');
+
+ if (tabHighlightEl) {
+ initPopover(tabHighlightEl);
+ }
}
diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js
index 35564754ee0..59310b3f76f 100644
--- a/app/assets/javascripts/pages/projects/registry/repositories/index.js
+++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js
@@ -1,3 +1,3 @@
-import initRegistryImages from '~/registry/index';
+import initRegistryImages from '~/registry/list/index';
document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index a0272b148e3..d17c2f33adc 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -52,6 +52,11 @@ export default {
header: s__('PerformanceBar|Redis calls'),
keys: ['cmd'],
},
+ {
+ metric: 'total',
+ header: s__('PerformanceBar|Frontend resources'),
+ keys: ['name', 'size'],
+ },
],
data() {
return { currentRequestId: '' };
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 735c9d804ee..2ffe07500e0 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
@@ -53,12 +54,61 @@ export default ({ container }) =>
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data);
+
+ if (this.requestId === requestId) this.collectFrontendPerformanceMetrics();
})
.catch(() =>
// eslint-disable-next-line no-console
console.warn(`Error getting performance bar results for ${requestId}`),
);
},
+ collectFrontendPerformanceMetrics() {
+ if (performance) {
+ const navigationEntries = performance.getEntriesByType('navigation');
+ const paintEntries = performance.getEntriesByType('paint');
+ const resourceEntries = performance.getEntriesByType('resource');
+
+ let durationString = '';
+ if (navigationEntries.length > 0) {
+ durationString = `BE ${this.formatMs(navigationEntries[0].responseEnd)} / `;
+ durationString += `FCP ${this.formatMs(paintEntries[1].startTime)} / `;
+ durationString += `DOM ${this.formatMs(navigationEntries[0].domContentLoadedEventEnd)}`;
+ }
+
+ let newEntries = resourceEntries.map(this.transformResourceEntry);
+
+ this.updateFrontendPerformanceMetrics(durationString, newEntries);
+
+ if ('PerformanceObserver' in window) {
+ // We start observing for more incoming timings
+ const observer = new PerformanceObserver(list => {
+ newEntries = newEntries.concat(list.getEntries().map(this.transformResourceEntry));
+ this.updateFrontendPerformanceMetrics(durationString, newEntries);
+ });
+
+ observer.observe({ entryTypes: ['resource'] });
+ }
+ }
+ },
+ updateFrontendPerformanceMetrics(durationString, requestEntries) {
+ this.store.setRequestDetailsData(this.requestId, 'total', {
+ duration: durationString,
+ calls: requestEntries.length,
+ details: requestEntries,
+ });
+ },
+ transformResourceEntry(entry) {
+ const nf = new Intl.NumberFormat();
+ return {
+ name: entry.name.replace(document.location.origin, ''),
+ duration: Math.round(entry.duration),
+ size: entry.transferSize ? `${nf.format(entry.transferSize)} bytes` : 'cached',
+ };
+ },
+ formatMs(msValue) {
+ const nf = new Intl.NumberFormat();
+ return `${nf.format(Math.round(msValue))}ms`;
+ },
},
render(createElement) {
return createElement('performance-bar-app', {
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
index 12d0ee86218..6f443db47ed 100644
--- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -32,6 +32,16 @@ export default class PerformanceBarStore {
return request;
}
+ setRequestDetailsData(requestId, metricKey, requestDetailsData) {
+ const selectedRequest = this.findRequest(requestId);
+ if (selectedRequest) {
+ selectedRequest.details = {
+ ...selectedRequest.details,
+ [metricKey]: requestDetailsData,
+ };
+ }
+ }
+
requestsWithDetails() {
return this.requests.filter(request => request.details);
}
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 8d6a3781048..4598626718c 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -6,8 +6,8 @@ import Flash from './flash';
const DEFERRED_LINK_CLASS = 'deferred-link';
export default class PersistentUserCallout {
- constructor(container) {
- const { dismissEndpoint, featureId, deferLinks } = container.dataset;
+ constructor(container, options = container.dataset) {
+ const { dismissEndpoint, featureId, deferLinks } = options;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
@@ -53,11 +53,11 @@ export default class PersistentUserCallout {
});
}
- static factory(container) {
+ static factory(container, options) {
if (!container) {
return undefined;
}
- return new PersistentUserCallout(container);
+ return new PersistentUserCallout(container, options);
}
}
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/list/components/app.vue
index 11b2c3b7016..c555c2b04d1 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/list/components/app.vue
@@ -5,7 +5,7 @@ import store from '../stores';
import CollapsibleContainer from './collapsible_container.vue';
import ProjectEmptyState from './project_empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
-import { s__, sprintf } from '../../locale';
+import { s__, sprintf } from '~/locale';
export default {
name: 'RegistryListApp',
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/list/components/collapsible_container.vue
index 5a6f9370564..86bb2d8092e 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/list/components/collapsible_container.vue
@@ -31,7 +31,7 @@ export default {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
- mixins: [Tracking.mixin({})],
+ mixins: [Tracking.mixin()],
props: {
repo: {
type: Object,
@@ -43,7 +43,6 @@ export default {
isOpen: false,
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
tracking: {
- category: document.body.dataset.page,
label: 'registry_repository_delete',
},
};
@@ -67,7 +66,7 @@ export default {
}
},
handleDeleteRepository() {
- this.track('confirm_delete', {});
+ this.track('confirm_delete');
return this.deleteItem(this.repo)
.then(() => {
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
@@ -103,7 +102,7 @@ export default {
:aria-label="s__('ContainerRegistry|Remove repository')"
class="js-remove-repo btn-inverted"
variant="danger"
- @click="track('click_button', {})"
+ @click="track('click_button')"
>
<icon name="remove" />
</gl-button>
@@ -132,7 +131,7 @@ export default {
:modal-id="modalId"
ok-variant="danger"
@ok="handleDeleteRepository"
- @cancel="track('cancel_delete', {})"
+ @cancel="track('cancel_delete')"
>
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<p
diff --git a/app/assets/javascripts/registry/components/group_empty_state.vue b/app/assets/javascripts/registry/list/components/group_empty_state.vue
index 7885fd2146d..7885fd2146d 100644
--- a/app/assets/javascripts/registry/components/group_empty_state.vue
+++ b/app/assets/javascripts/registry/list/components/group_empty_state.vue
diff --git a/app/assets/javascripts/registry/components/project_empty_state.vue b/app/assets/javascripts/registry/list/components/project_empty_state.vue
index 80ef31004c8..80ef31004c8 100644
--- a/app/assets/javascripts/registry/components/project_empty_state.vue
+++ b/app/assets/javascripts/registry/list/components/project_empty_state.vue
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/list/components/table_registry.vue
index 682c511a1ae..e682a0e0019 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/list/components/table_registry.vue
@@ -23,7 +23,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [timeagoMixin],
+ mixins: [timeagoMixin, Tracking.mixin()],
props: {
repo: {
type: Object,
@@ -71,9 +71,6 @@ export default {
},
methods: {
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
- track(action) {
- Tracking.event(document.body.dataset.page, action, this.tracking);
- },
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = sprintf(
diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/list/constants.js
index db798fb88ac..e55ea9cc9d9 100644
--- a/app/assets/javascripts/registry/constants.js
+++ b/app/assets/javascripts/registry/list/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '../locale';
+import { __ } from '~/locale';
export const FETCH_REGISTRY_ERROR_MESSAGE = __(
'Something went wrong while fetching the registry list.',
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/list/index.js
index 18fd360f586..3d0ff327b42 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/list/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import registryApp from './components/app.vue';
-import Translate from '../vue_shared/translate';
+import Translate from '~/vue_shared/translate';
Vue.use(Translate);
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/list/stores/actions.js
index 6afba618486..6afba618486 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/list/stores/actions.js
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/list/stores/getters.js
index ac90bde1b2a..ac90bde1b2a 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/list/stores/getters.js
diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/list/stores/index.js
index 1bb06bd6e81..1bb06bd6e81 100644
--- a/app/assets/javascripts/registry/stores/index.js
+++ b/app/assets/javascripts/registry/list/stores/index.js
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/list/stores/mutation_types.js
index 6740bfede1a..6740bfede1a 100644
--- a/app/assets/javascripts/registry/stores/mutation_types.js
+++ b/app/assets/javascripts/registry/list/stores/mutation_types.js
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/list/stores/mutations.js
index 419de848883..419de848883 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/list/stores/mutations.js
diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/list/stores/state.js
index 724c64b4994..724c64b4994 100644
--- a/app/assets/javascripts/registry/stores/state.js
+++ b/app/assets/javascripts/registry/list/stores/state.js
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 7d6a725b30f..157d89a3a40 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -17,6 +17,7 @@ const handleUserPopoverMouseOut = event => {
renderedPopover.$destroy();
renderedPopover = null;
}
+ target.removeAttribute('aria-describedby');
};
/**
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index a2b5a79af36..3daea306fba 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -155,7 +155,7 @@ export default {
{{ cherryPickLabel }}
</a>
</div>
- <section class="mr-info-list">
+ <section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
{{ s__('mrWidget|The changes were merged into') }}
<span class="label-branch">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 2c113770d8b..aa65b16a3c3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -249,9 +249,10 @@ export default {
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
+ data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select merge moment')"
>
- <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
+ <i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeImmediatelyDropdown"
@@ -272,7 +273,8 @@ export default {
</li>
<li>
<a
- class="accept-merge-request qa-merge-immediately-option"
+ class="accept-merge-request"
+ data-qa-selector="merge_immediately_option"
href="#"
@click.prevent="handleMergeButtonClick(false, true)"
>
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
index eabf5d4bf60..25d7bfe515c 100644
--- a/app/assets/javascripts/vue_shared/components/bar_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue
@@ -55,13 +55,13 @@ export default {
vbWidth: 0,
vbHeight: 0,
vpWidth: 0,
- vpHeight: 350,
- preserveAspectRatioType: 'xMidYMid meet',
+ vpHeight: 200,
+ preserveAspectRatioType: 'xMidYMin meet',
containerMargin: {
leftRight: 30,
},
viewBoxMargin: {
- topBottom: 150,
+ topBottom: 100,
},
panX: 0,
xScale: {},
@@ -274,6 +274,7 @@ export default {
<div ref="svgContainer" :class="activateGrabCursor" class="svg-graph-container">
<svg
ref="baseSvg"
+ class="svg-graph overflow-visible pt-5"
:width="vpWidth"
:height="vpHeight"
:viewBox="svgViewBox"
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 31ea59df4c5..9a15505dd25 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -442,6 +442,7 @@ img.emoji {
.ws-normal { white-space: normal; }
.ws-pre-wrap { white-space: pre-wrap; }
.overflow-auto { overflow: auto; }
+.overflow-visible { overflow: visible; }
.d-flex-center {
display: flex;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index ecd32dcd0ce..4aba633e182 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -196,6 +196,11 @@ ul.content-list {
display: flex;
align-items: center;
white-space: nowrap;
+
+ // Override style that allows the flex-row text to wrap.
+ &.allow-wrap {
+ white-space: normal;
+ }
}
.row-main-content {
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index ba126d59eef..977fc8329b6 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -883,6 +883,15 @@ $ide-commit-header-height: 48px;
margin-right: $ide-tree-padding;
border-bottom: 1px solid $white-dark;
+ svg {
+ color: $gray-700;
+
+ &:focus,
+ &:hover {
+ color: $blue-600;
+ }
+ }
+
.ide-new-btn {
margin-left: auto;
}
@@ -899,6 +908,11 @@ $ide-commit-header-height: 48px;
.dropdown-menu-toggle {
svg {
vertical-align: middle;
+ color: $gray-700;
+
+ &:hover {
+ color: $gray-700;
+ }
}
&:hover {
diff --git a/app/assets/stylesheets/pages/error_details.scss b/app/assets/stylesheets/pages/error_details.scss
index 0515db914e9..dcd25c126c4 100644
--- a/app/assets/stylesheets/pages/error_details.scss
+++ b/app/assets/stylesheets/pages/error_details.scss
@@ -12,6 +12,12 @@
}
}
+ .file-title-name {
+ &.limited-width {
+ max-width: 80%;
+ }
+ }
+
.line_content.old::before {
content: none !important;
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index a37cbda8558..61542e89828 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -21,16 +21,11 @@
margin-bottom: 2px;
}
- .issue-labels {
+ .issue-labels,
+ .author-link {
display: inline-block;
}
- .issuable-meta {
- .author-link {
- display: inline-block;
- }
- }
-
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
@@ -53,16 +48,6 @@
margin-right: 15px;
}
-.issues_content {
- .title {
- height: 40px;
- }
-
- form {
- margin: 0;
- }
-}
-
form.edit-issue {
margin: 0;
}
@@ -79,10 +64,6 @@ ul.related-merge-requests > li {
margin-left: 5px;
}
- .row_title {
- vertical-align: bottom;
- }
-
gl-emoji {
font-size: 1em;
}
@@ -93,10 +74,6 @@ ul.related-merge-requests > li {
font-weight: $gl-font-weight-bold;
}
-.merge-request-id {
- display: inline-block;
-}
-
.merge-request-status {
&.merged {
color: $blue-500;
@@ -118,11 +95,7 @@ ul.related-merge-requests > li {
border-color: $issues-today-border;
}
- &.closed {
- background: $gray-light;
- border-color: $border-color;
- }
-
+ &.closed,
&.merged {
background: $gray-light;
border-color: $border-color;
@@ -160,9 +133,12 @@ ul.related-merge-requests > li {
padding-bottom: 37px;
}
-.issues-nav-controls {
+.issues-nav-controls,
+.new-branch-col {
font-size: 0;
+}
+.issues-nav-controls {
.btn-group:empty {
display: none;
}
@@ -198,8 +174,6 @@ ul.related-merge-requests > li {
}
.new-branch-col {
- font-size: 0;
-
.discussion-filter-container {
&:not(:only-child) {
margin-right: $gl-padding-8;
@@ -297,11 +271,11 @@ ul.related-merge-requests > li {
padding-top: 0;
align-self: center;
}
+ }
- .create-mr-dropdown-wrap {
- .btn-group:not(.hidden) {
- display: inline-flex;
- }
+ .create-mr-dropdown-wrap {
+ .btn-group:not(.hidden) {
+ display: inline-flex;
}
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 6e7081aa4ec..79ad0bd7735 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -15,7 +15,6 @@
}
.tree-controls {
- display: flex;
text-align: right;
.btn {
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 6e5dd1a1f55..63fff821871 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -60,6 +60,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
font
message
starts_at
+ target_path
))
end
end
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index 8f2e34a6294..327538f1e93 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -28,7 +28,8 @@ class Admin::IdentitiesController < Admin::ApplicationController
def update
if @identity.update(identity_params)
- RepairLdapBlockedUserService.new(@user).execute
+ ::Users::RepairLdapBlockedService.new(@user).execute
+
redirect_to admin_user_identities_path(@user), notice: _('User identity was successfully updated.')
else
render :edit
@@ -37,7 +38,8 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
- RepairLdapBlockedUserService.new(@user).execute
+ ::Users::RepairLdapBlockedService.new(@user).execute
+
redirect_to admin_user_identities_path(@user), status: :found, notice: _('User identity was successfully removed.')
else
redirect_to admin_user_identities_path(@user), status: :found, alert: _('Failed to remove user identity.')
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 06531932b31..ba8d2d18695 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -40,10 +40,20 @@ class AutocompleteController < ApplicationController
end
def merge_request_target_branches
- merge_requests = MergeRequestsFinder.new(current_user, params).execute
- target_branches = merge_requests.recent_target_branches
+ if target_branch_params.present?
+ merge_requests = MergeRequestsFinder.new(current_user, target_branch_params).execute
+ target_branches = merge_requests.recent_target_branches
+
+ render json: target_branches.map { |target_branch| { title: target_branch } }
+ else
+ render json: { error: _('At least one of group_id or project_id must be specified') }, status: :bad_request
+ end
+ end
+
+ private
- render json: target_branches.map { |target_branch| { title: target_branch } }
+ def target_branch_params
+ params.permit(:group_id, :project_id)
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 6162d006cc7..7b7cfa7a5d3 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -121,7 +121,7 @@ module IssuableActions
end
def bulk_update
- result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name)
+ result = Issuable::BulkUpdateService.new(parent, current_user, bulk_update_params).execute(resource_name)
quantity = result[:count]
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
diff --git a/app/controllers/instance_statistics/conversational_development_index_controller.rb b/app/controllers/instance_statistics/conversational_development_index_controller.rb
index 306c16d559c..f34347b4d22 100644
--- a/app/controllers/instance_statistics/conversational_development_index_controller.rb
+++ b/app/controllers/instance_statistics/conversational_development_index_controller.rb
@@ -3,7 +3,7 @@
class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- @metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present
+ @metric = DevOpsScore::Metric.order(:created_at).last&.present
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 578a3d451a7..a908da08f57 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -133,8 +133,6 @@ class Projects::BranchesController < Projects::ApplicationController
# frontend could omit this set. To prevent excessive I/O, we require
# that a list of names be specified.
def limit_diverging_commit_counts!
- return unless Feature.enabled?(:limit_diverging_commit_counts, default_enabled: true)
-
limit = Kaminari.config.default_per_page
# If we don't have many branches in the repository, then go ahead.
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb
index 78d3854e72d..56a66dd38db 100644
--- a/app/controllers/projects/error_tracking_controller.rb
+++ b/app/controllers/projects/error_tracking_controller.rb
@@ -55,6 +55,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
render json: {
errors: serialize_errors(result[:issues]),
+ pagination: result[:pagination],
external_url: service.external_url
}
end
@@ -111,7 +112,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end
def list_issues_params
- params.permit([:search_term, :sort])
+ params.permit(:search_term, :sort, :cursor)
end
def list_projects_params
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 566a7ed46ca..844f1d04679 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -218,11 +218,16 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def ci_environments_status
- environments = if ci_environments_status_on_merge_result?
- EnvironmentStatus.after_merge_request(@merge_request, current_user)
- else
- EnvironmentStatus.for_merge_request(@merge_request, current_user)
- end
+ environments =
+ if ci_environments_status_on_merge_result?
+ if Feature.enabled?(:deployment_merge_requests_widget, @project)
+ EnvironmentStatus.for_deployed_merge_request(@merge_request, current_user)
+ else
+ EnvironmentStatus.after_merge_request(@merge_request, current_user)
+ end
+ else
+ EnvironmentStatus.for_merge_request(@merge_request, current_user)
+ end
render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments)
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 5805d068e21..54774df5e76 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -15,13 +15,9 @@ class SnippetsController < ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
- # Allow read snippet
+ before_action :authorize_create_snippet!, only: [:new, :create]
before_action :authorize_read_snippet!, only: [:show, :raw]
-
- # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
-
- # Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
@@ -140,6 +136,10 @@ class SnippetsController < ApplicationController
return render_404 unless can?(current_user, :admin_personal_snippet, @snippet)
end
+ def authorize_create_snippet!
+ return render_404 unless can?(current_user, :create_personal_snippet)
+ end
+
def snippet_params
params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description)
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 635db386792..67d33648470 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -20,7 +20,6 @@ class UploadsController < ApplicationController
skip_before_action :authenticate_user!
before_action :upload_mount_satisfied?
- before_action :find_model
before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
new file mode 100644
index 00000000000..085c6a04fa6
--- /dev/null
+++ b/app/finders/deployments_finder.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class DeploymentsFinder
+ attr_reader :project, :params
+
+ ALLOWED_SORT_VALUES = %w[id iid created_at updated_at ref].freeze
+ DEFAULT_SORT_VALUE = 'id'.freeze
+
+ ALLOWED_SORT_DIRECTIONS = %w[asc desc].freeze
+ DEFAULT_SORT_DIRECTION = 'asc'.freeze
+
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ items = init_collection
+ items = by_updated_at(items)
+ sort(items)
+ end
+
+ private
+
+ def init_collection
+ project.deployments
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def sort(items)
+ items.order(sort_params)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def by_updated_at(items)
+ items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
+ items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
+
+ items
+ end
+
+ def sort_params
+ order_by = ALLOWED_SORT_VALUES.include?(params[:order_by]) ? params[:order_by] : DEFAULT_SORT_VALUE
+ order_direction = ALLOWED_SORT_DIRECTIONS.include?(params[:sort]) ? params[:sort] : DEFAULT_SORT_DIRECTION
+
+ { order_by => order_direction }
+ end
+end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index bd6b6190fb5..5819f279eaa 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -40,15 +40,14 @@
# Any other value will be ignored.
class SnippetsFinder < UnionFinder
include FinderMethods
+ include Gitlab::Utils::StrongMemoize
- attr_accessor :current_user, :project, :author, :scope, :explore
+ attr_accessor :current_user, :params
+ delegate :explore, :only_personal, :only_project, :scope, to: :params
def initialize(current_user = nil, params = {})
@current_user = current_user
- @project = params[:project]
- @author = params[:author]
- @scope = params[:scope].to_s
- @explore = params[:explore]
+ @params = OpenStruct.new(params)
if project && author
raise(
@@ -60,8 +59,15 @@ class SnippetsFinder < UnionFinder
end
def execute
- base = init_collection
- base.with_optional_visibility(visibility_from_scope).fresh
+ # The snippet query can be expensive, therefore if the
+ # author or project params have been passed and they don't
+ # exist, it's better to return
+ return Snippet.none if author.nil? && params[:author].present?
+ return Snippet.none if project.nil? && params[:project].present?
+
+ items = init_collection
+ items = by_ids(items)
+ items.with_optional_visibility(visibility_from_scope).fresh
end
private
@@ -69,10 +75,12 @@ class SnippetsFinder < UnionFinder
def init_collection
if explore
snippets_for_explore
+ elsif only_personal
+ personal_snippets
elsif project
snippets_for_a_single_project
else
- snippets_for_multiple_projects
+ snippets_for_personal_and_multiple_projects
end
end
@@ -96,8 +104,9 @@ class SnippetsFinder < UnionFinder
#
# Each collection is constructed in isolation, allowing for greater control
# over the resulting SQL query.
- def snippets_for_multiple_projects
- queries = [personal_snippets]
+ def snippets_for_personal_and_multiple_projects
+ queries = []
+ queries << personal_snippets unless only_project
if Ability.allowed?(current_user, :read_cross_project)
queries << snippets_of_visible_projects
@@ -158,7 +167,7 @@ class SnippetsFinder < UnionFinder
end
def visibility_from_scope
- case scope
+ case scope.to_s
when 'are_private'
Snippet::PRIVATE
when 'are_internal'
@@ -169,6 +178,28 @@ class SnippetsFinder < UnionFinder
nil
end
end
+
+ def by_ids(items)
+ return items unless params[:ids].present?
+
+ items.id_in(params[:ids])
+ end
+
+ def author
+ strong_memoize(:author) do
+ next unless params[:author].present?
+
+ params[:author].is_a?(User) ? params[:author] : User.find_by_id(params[:author])
+ end
+ end
+
+ def project
+ strong_memoize(:project) do
+ next unless params[:project].present?
+
+ params[:project].is_a?(Project) ? params[:project] : Project.find_by_id(params[:project])
+ end
+ end
end
SnippetsFinder.prepend_if_ee('EE::SnippetsFinder')
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 3ae804ff231..8389272fd35 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -94,6 +94,25 @@ module ApplicationHelper
sanitize(str, tags: %w(a span))
end
+ def body_data
+ {
+ page: body_data_page,
+ page_type_id: controller.params[:id],
+ find_file: find_file_path,
+ group: "#{@group&.path}"
+ }.merge(project_data)
+ end
+
+ def project_data
+ return {} unless @project
+
+ {
+ project_id: @project.id,
+ project: @project.path,
+ namespace_id: @project.namespace&.id
+ }
+ end
+
def body_data_page
[*controller.controller_path.split('/'), controller.action_name].compact.join(':')
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index a011209375e..d9416cb10c4 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -301,7 +301,8 @@ module ApplicationSettingsHelper
:snowplow_iglu_registry_url,
:push_event_hooks_limit,
:push_event_activities_limit,
- :custom_http_clone_url_root
+ :custom_http_clone_url_root,
+ :snippet_size_limit
]
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 912f0b61978..659f9778892 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -141,7 +141,7 @@ module BlobHelper
if @build && @entry
raw_project_job_artifacts_url(@project, @build, path: @entry.path, **kwargs)
elsif @snippet
- reliable_raw_snippet_url(@snippet)
+ raw_snippet_url(@snippet)
elsif @blob
project_raw_url(@project, @id, **kwargs)
end
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 495c29d3e24..ec653aed91b 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
module BroadcastMessagesHelper
+ def current_broadcast_messages
+ BroadcastMessage.current(request.path)
+ end
+
def broadcast_message(message)
return unless message.present?
diff --git a/app/helpers/conversational_development_index_helper.rb b/app/helpers/dev_ops_score_helper.rb
index 37e5bb325fb..9a673998149 100644
--- a/app/helpers/conversational_development_index_helper.rb
+++ b/app/helpers/dev_ops_score_helper.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module ConversationalDevelopmentIndexHelper
+module DevOpsScoreHelper
def score_level(score)
if score < 33.33
'low'
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 52ec2eadf5e..acc852d8b9a 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -161,6 +161,18 @@ module DiffHelper
end
end
+ def render_overflow_warning?(diffs_collection)
+ diff_files = diffs_collection.diff_files
+
+ if diff_files.any?(&:too_large?)
+ Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits)
+ end
+
+ diff_files.overflow?.tap do |overflown|
+ Gitlab::Metrics.add_event(:diffs_overflow_collection_limits) if overflown
+ end
+ end
+
private
def diff_btn(title, name, selected)
@@ -203,12 +215,6 @@ module DiffHelper
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end
- def render_overflow_warning?(diffs_collection)
- diffs = @merge_request_diff.presence || diffs_collection.diff_files
-
- diffs.overflow?
- end
-
def diff_file_path_text(diff_file, max: 60)
path = diff_file.new_path
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 404ea7b00d4..38ca12e6f90 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -193,6 +193,101 @@ module GitlabRoutingHelper
project = schedule.project
take_ownership_project_pipeline_schedule_path(project, schedule, *args)
end
+
+ def snippet_path(snippet, *args)
+ if snippet.is_a?(ProjectSnippet)
+ application_url_helpers.project_snippet_path(snippet.project, snippet, *args)
+ else
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.snippet_path(snippet, *new_args)
+ end
+ end
+
+ def snippet_url(snippet, *args)
+ if snippet.is_a?(ProjectSnippet)
+ application_url_helpers.project_snippet_url(snippet.project, snippet, *args)
+ else
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.snippet_url(snippet, *new_args)
+ end
+ end
+
+ def raw_snippet_path(snippet, *args)
+ if snippet.is_a?(ProjectSnippet)
+ application_url_helpers.raw_project_snippet_path(snippet.project, snippet, *args)
+ else
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.raw_snippet_path(snippet, *new_args)
+ end
+ end
+
+ def raw_snippet_url(snippet, *args)
+ if snippet.is_a?(ProjectSnippet)
+ application_url_helpers.raw_project_snippet_url(snippet.project, snippet, *args)
+ else
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.raw_snippet_url(snippet, *new_args)
+ end
+ end
+
+ def snippet_notes_path(snippet, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.snippet_notes_path(snippet, *new_args)
+ end
+
+ def snippet_notes_url(snippet, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.snippet_notes_url(snippet, *new_args)
+ end
+
+ def snippet_note_path(snippet, note, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.snippet_note_path(snippet, note, *new_args)
+ end
+
+ def snippet_note_url(snippet, note, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.snippet_note_url(snippet, note, *new_args)
+ end
+
+ def toggle_award_emoji_snippet_note_path(snippet, note, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.toggle_award_emoji_snippet_note_path(snippet, note, *new_args)
+ end
+
+ def toggle_award_emoji_snippet_note_url(snippet, note, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.toggle_award_emoji_snippet_note_url(snippet, note, *new_args)
+ end
+
+ def toggle_award_emoji_snippet_path(snippet, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.toggle_award_emoji_snippet_path(snippet, *new_args)
+ end
+
+ def toggle_award_emoji_snippet_url(snippet, *args)
+ new_args = snippet_query_params(snippet, *args)
+ application_url_helpers.toggle_award_emoji_snippet_url(snippet, *new_args)
+ end
+
+ private
+
+ def application_url_helpers
+ Gitlab::Routing.url_helpers
+ end
+
+ def snippet_query_params(snippet, *args)
+ opts = case args.last
+ when Hash
+ args.pop
+ when ActionController::Parameters
+ args.pop.to_h
+ else
+ {}
+ end
+
+ args << opts
+ end
end
GitlabRoutingHelper.include_if_ee('EE::GitlabRoutingHelper')
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 3c72f41a4c9..18451bc5273 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -391,6 +391,10 @@ module IssuablesHelper
end
end
+ def issuable_templates_names(issuable)
+ issuable_templates(issuable).map { |template| template[:name] }
+ end
+
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 899ab70d1aa..46db3b78fd0 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module MergeRequestsHelper
+ include Gitlab::Utils::StrongMemoize
+
def new_mr_path_from_push_event(event)
target_project = event.project.default_merge_request_target
project_new_merge_request_path(
@@ -168,6 +170,12 @@ module MergeRequestsHelper
current_user.fork_of(project)
end
end
+
+ def mr_tabs_position_enabled?
+ strong_memoize(:mr_tabs_position_enabled) do
+ Feature.enabled?(:mr_tabs_position, @project, default_enabled: true)
+ end
+ end
end
MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper')
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 466c782fc77..ff013f3f7ec 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -114,8 +114,10 @@ module ProjectsHelper
source = visible_fork_source(project)
if source
- _('This will remove the fork relationship between this project and %{fork_source}.') %
+ msg = _('This will remove the fork relationship between this project and %{fork_source}.') %
{ fork_source: link_to(source.full_name, project_path(source)) }
+
+ msg.html_safe
else
_('This will remove the fork relationship between this project and other projects in the fork network.')
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 10e31fb8888..7d7d8646c25 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -11,33 +11,9 @@ module SnippetsHelper
end
end
- def reliable_snippet_path(snippet, opts = {})
- reliable_snippet_url(snippet, opts.merge(only_path: true))
- end
-
- def reliable_raw_snippet_path(snippet, opts = {})
- reliable_raw_snippet_url(snippet, opts.merge(only_path: true))
- end
-
- def reliable_snippet_url(snippet, opts = {})
- if snippet.project_id?
- project_snippet_url(snippet.project, snippet, nil, opts)
- else
- snippet_url(snippet, nil, opts)
- end
- end
-
- def reliable_raw_snippet_url(snippet, opts = {})
- if snippet.project_id?
- raw_project_snippet_url(snippet.project, snippet, nil, opts)
- else
- raw_snippet_url(snippet, nil, opts)
- end
- end
-
def download_raw_snippet_button(snippet)
link_to(icon('download'),
- reliable_raw_snippet_path(snippet, inline: false),
+ raw_snippet_path(snippet, inline: false),
target: '_blank',
rel: 'noopener noreferrer',
class: "btn btn-sm has-tooltip",
@@ -133,7 +109,7 @@ module SnippetsHelper
end
def snippet_embed_tag(snippet)
- content_tag(:script, nil, src: reliable_snippet_url(snippet, format: :js, only_path: false))
+ content_tag(:script, nil, src: snippet_url(snippet, format: :js))
end
def snippet_badge(snippet)
@@ -158,7 +134,7 @@ module SnippetsHelper
return if blob.empty? || blob.binary? || blob.stored_externally?
link_to(external_snippet_icon('doc-code'),
- reliable_raw_snippet_url(@snippet),
+ raw_snippet_url(@snippet),
class: 'btn',
target: '_blank',
rel: 'noopener noreferrer',
@@ -167,7 +143,7 @@ module SnippetsHelper
def embedded_snippet_download_button
link_to(external_snippet_icon('download'),
- reliable_raw_snippet_url(@snippet, inline: false),
+ raw_snippet_url(@snippet, inline: false),
class: 'btn',
target: '_blank',
title: 'Download',
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index cae3ec5f8d0..11b78b8fd59 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -4,6 +4,7 @@ module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
GCP_SIGNUP_OFFER = 'gcp_signup_offer'
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
+ TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
@@ -25,6 +26,10 @@ module UserCalloutsHelper
!user_dismissed?(SUGGEST_POPOVER_DISMISSED)
end
+ def show_tabs_feature_highlight?
+ !user_dismissed?(TABS_POSITION_HIGHLIGHT) && !Rails.env.test?
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 00192b1da59..a6d5fc1137d 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -4,6 +4,7 @@ class ActiveSession
include ActiveModel::Model
SESSION_BATCH_SIZE = 200
+ ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
attr_accessor :created_at, :updated_at,
:session_id, :ip_address,
@@ -65,21 +66,22 @@ class ActiveSession
def self.destroy(user, session_id)
Gitlab::Redis::SharedState.with do |redis|
- redis.srem(lookup_key_name(user.id), session_id)
+ destroy_sessions(redis, user, [session_id])
+ end
+ end
- deleted_keys = redis.del(key_name(user.id, session_id))
+ def self.destroy_sessions(redis, user, session_ids)
+ key_names = session_ids.map {|session_id| key_name(user.id, session_id) }
+ session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
- # only allow deleting the devise session if we could actually find a
- # related active session. this prevents another user from deleting
- # someone else's session.
- if deleted_keys > 0
- redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
- end
- end
+ redis.srem(lookup_key_name(user.id), session_ids)
+ redis.del(key_names)
+ redis.del(session_names)
end
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
+ clean_up_old_sessions(redis, user)
cleaned_up_lookup_entries(redis, user)
end
end
@@ -118,19 +120,39 @@ class ActiveSession
end
end
- def self.raw_active_session_entries(session_ids, user_id)
+ def self.raw_active_session_entries(redis, session_ids, user_id)
return [] if session_ids.empty?
- Gitlab::Redis::SharedState.with do |redis|
- entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+ entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+
+ redis.mget(entry_keys)
+ end
- redis.mget(entry_keys)
+ def self.active_session_entries(session_ids, user_id, redis)
+ return [] if session_ids.empty?
+
+ entry_keys = raw_active_session_entries(redis, session_ids, user_id)
+
+ entry_keys.compact.map do |raw_session|
+ Marshal.load(raw_session) # rubocop:disable Security/MarshalLoad
end
end
+ def self.clean_up_old_sessions(redis, user)
+ session_ids = session_ids_for_user(user.id)
+
+ return if session_ids.count <= ALLOWED_NUMBER_OF_ACTIVE_SESSIONS
+
+ # remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
+ sessions = active_session_entries(session_ids, user.id, redis)
+ sessions.sort_by! {|session| session.updated_at }.reverse!
+ sessions = sessions[ALLOWED_NUMBER_OF_ACTIVE_SESSIONS..-1].map { |session| session.session_id }
+ destroy_sessions(redis, user, sessions)
+ end
+
def self.cleaned_up_lookup_entries(redis, user)
session_ids = session_ids_for_user(user.id)
- entries = raw_active_session_entries(session_ids, user.id)
+ entries = raw_active_session_entries(redis, session_ids, user.id)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 72605af433f..4ba0317b580 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,12 +5,9 @@ class ApplicationSetting < ApplicationRecord
include CacheMarkdownField
include TokenAuthenticatable
include ChronicDurationAttribute
+ include IgnorableColumns
- # Only remove this >= %12.6 and >= 2019-12-01
- self.ignored_columns += %i[
- pendo_enabled
- pendo_url
- ]
+ ignore_columns :pendo_enabled, :pendo_url, remove_after: '2019-12-01', remove_with: '12.6'
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
@@ -229,6 +226,8 @@ class ApplicationSetting < ApplicationRecord
validates :push_event_activities_limit,
numericality: { greater_than_or_equal_to: 0 }
+ validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 7bb89f0d1e2..e1eb8d429bb 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -26,7 +26,8 @@ module ApplicationSettingImplementation
'/users',
'/users/confirmation',
'/unsubscribes/',
- '/import/github/personal_access_token'
+ '/import/github/personal_access_token',
+ '/admin/session'
].freeze
class_methods do
@@ -139,7 +140,8 @@ module ApplicationSettingImplementation
snowplow_app_id: nil,
snowplow_iglu_registry_url: nil,
custom_http_clone_url_root: nil,
- productivity_analytics_start_date: Time.now
+ productivity_analytics_start_date: Time.now,
+ snippet_size_limit: 50.megabytes
}
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index dfcf28763ee..9c2ae92071d 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -20,7 +20,7 @@ class BroadcastMessage < ApplicationRecord
after_commit :flush_redis_cache
- def self.current
+ def self.current(current_path = nil)
messages = cache.fetch(CACHE_KEY, as: BroadcastMessage, expires_in: cache_expires_in) do
current_and_future_messages
end
@@ -33,7 +33,7 @@ class BroadcastMessage < ApplicationRecord
# cache so we don't keep running this code all the time.
cache.expire(CACHE_KEY) if now_or_future.empty?
- now_or_future.select(&:now?)
+ now_or_future.select(&:now?).select { |message| message.matches_current_path(current_path) }
end
def self.current_and_future_messages
@@ -72,6 +72,12 @@ class BroadcastMessage < ApplicationRecord
now? || future?
end
+ def matches_current_path(current_path)
+ return true if current_path.blank? || target_path.blank?
+
+ current_path.match(Regexp.escape(target_path).gsub('\\*', '.*'))
+ end
+
def flush_redis_cache
self.class.cache.expire(CACHE_KEY)
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7d3cb62e4ee..caa4478c848 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -13,17 +13,11 @@ module Ci
include Importable
include Gitlab::Utils::StrongMemoize
include HasRef
+ include IgnorableColumns
BuildArchivedError = Class.new(StandardError)
- self.ignored_columns += %i[
- artifacts_file
- artifacts_file_store
- artifacts_metadata
- artifacts_metadata_store
- artifacts_size
- commands
- ]
+ ignore_columns :artifacts_file, :artifacts_file_store, :artifacts_metadata, :artifacts_metadata_store, :artifacts_size, :commands, remove_after: '2019-12-15', remove_with: '12.7'
belongs_to :project, inverse_of: :builds
belongs_to :runner
@@ -53,7 +47,7 @@ module Ci
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
- accepts_nested_attributes_for :runner_session
+ accepts_nested_attributes_for :runner_session, update_only: true
accepts_nested_attributes_for :job_variables
delegate :url, to: :runner_session, prefix: true, allow_nil: true
diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb
index 54c3135ac9e..10d7795be2b 100644
--- a/app/models/ci/persistent_ref.rb
+++ b/app/models/ci/persistent_ref.rb
@@ -22,7 +22,7 @@ module Ci
end
def create
- return unless enabled? && !exist?
+ return unless enabled?
create_ref(sha, path)
rescue => e
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index b411bc296c5..5821cc1a1a5 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -299,11 +299,6 @@ module Ci
end
end
- def self.latest_for_shas(shas)
- max_id_per_sha = for_sha(shas).group(:sha).select("max(id)")
- where(id: max_id_per_sha)
- end
-
def self.latest_successful_ids_per_project
success.group(:project_id).select('max(id) as id')
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index c4a4410e8fc..3f409b8bb22 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -8,6 +8,7 @@ module Ci
include ChronicDurationAttribute
include FromUnion
include TokenAuthenticatable
+ include IgnorableColumns
add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
@@ -35,7 +36,7 @@ module Ci
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
- self.ignored_columns += %i[is_shared]
+ ignore_column :is_shared, remove_after: '2019-12-15', remove_with: '12.6'
has_many :builds
has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 6ad086211fb..c15d9edf7b2 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -17,11 +17,11 @@ module Clusters
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
+ alias_method :original_set_initial_status, :set_initial_status
def set_initial_status
- return unless not_installable?
- return unless verify_cluster?
+ return unless cluster&.platform_kubernetes_rbac?
- self.status = status_states[:installable]
+ original_set_initial_status
end
state_machine :status do
@@ -131,10 +131,6 @@ module Clusters
[Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)]
end
-
- def verify_cluster?
- cluster&.application_helm_available? && cluster&.platform_kubernetes_rbac?
- end
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index aae49c36899..95993089426 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -464,8 +464,20 @@ class Commit
"commit:#{sha}"
end
+ def expire_note_etag_cache
+ super
+
+ expire_note_etag_cache_for_related_mrs
+ end
+
private
+ def expire_note_etag_cache_for_related_mrs
+ MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each do |mr|
+ mr.expire_note_etag_cache
+ end
+ end
+
def commit_reference(from, referable_commit_id, full: false)
reference = project.to_reference(from, full: full)
diff --git a/app/models/concerns/ignorable_columns.rb b/app/models/concerns/ignorable_columns.rb
new file mode 100644
index 00000000000..744a1f0b5f3
--- /dev/null
+++ b/app/models/concerns/ignorable_columns.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module IgnorableColumns
+ extend ActiveSupport::Concern
+
+ ColumnIgnore = Struct.new(:remove_after, :remove_with) do
+ def safe_to_remove?
+ Date.today > remove_after
+ end
+
+ def to_s
+ "(#{remove_after}, #{remove_with})"
+ end
+ end
+
+ class_methods do
+ # Ignore database columns in a model
+ #
+ # Indicate the earliest date and release we can stop ignoring the column with +remove_after+ (a date string) and +remove_with+ (a release)
+ def ignore_columns(*columns, remove_after:, remove_with:)
+ raise ArgumentError, 'Please indicate when we can stop ignoring columns with remove_after (date string YYYY-MM-DD), example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_after =~ Gitlab::Regex.utc_date_regex
+ raise ArgumentError, 'Please indicate in which release we can stop ignoring columns with remove_with, example: ignore_columns(:name, remove_after: \'2019-12-01\', remove_with: \'12.6\')' unless remove_with
+
+ self.ignored_columns += columns.flatten # rubocop:disable Cop/IgnoredColumns
+
+ columns.flatten.each do |column|
+ self.ignored_columns_details[column.to_sym] = ColumnIgnore.new(Date.parse(remove_after), remove_with)
+ end
+ end
+
+ alias_method :ignore_column, :ignore_columns
+
+ def ignored_columns_details
+ unless defined?(@ignored_columns_details)
+ IGNORE_COLUMN_MUTEX.synchronize do
+ @ignored_columns_details ||= superclass.try(:ignored_columns_details)&.dup || {}
+ end
+ end
+
+ @ignored_columns_details
+ end
+
+ IGNORE_COLUMN_MUTEX = Mutex.new
+ end
+end
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index 869b3490f3f..a84fb1cf56d 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -49,8 +49,7 @@ module UpdateProjectStatistics
attr = self.class.statistic_attribute
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
- update_project_statistics(delta)
- schedule_namespace_aggregation_worker
+ schedule_update_project_statistic(delta)
end
def update_project_statistics_attribute_changed?
@@ -58,24 +57,35 @@ module UpdateProjectStatistics
end
def update_project_statistics_after_destroy
- update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
+ delta = -read_attribute(self.class.statistic_attribute).to_i
- schedule_namespace_aggregation_worker
+ schedule_update_project_statistic(delta)
end
def project_destroyed?
project.pending_delete?
end
- def update_project_statistics(delta)
- ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
- end
+ def schedule_update_project_statistic(delta)
+ return if delta.zero?
+
+ if Feature.enabled?(:update_project_statistics_after_commit, default_enabled: true)
+ # Update ProjectStatistics after the transaction
+ run_after_commit do
+ ProjectStatistics.increment_statistic(
+ project_id, self.class.project_statistics_name, delta)
+ end
+ else
+ # Use legacy-way to update within transaction
+ ProjectStatistics.increment_statistic(
+ project_id, self.class.project_statistics_name, delta)
+ end
- def schedule_namespace_aggregation_worker
run_after_commit do
next if project.nil?
- Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
+ Namespaces::ScheduleAggregationWorker.perform_async(
+ project.namespace_id)
end
end
end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 22ab326a0ab..19216281e48 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -2,6 +2,7 @@
class DeployKey < Key
include FromUnion
+ include IgnorableColumns
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :deploy_keys_projects
@@ -10,7 +11,7 @@ class DeployKey < Key
scope :are_public, -> { where(public: true) }
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, :namespace] }) }
- self.ignored_columns += %i[can_push]
+ ignore_column :can_push, remove_after: '2019-12-15', remove_with: '12.6'
accepts_nested_attributes_for :deploy_keys_projects
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 4a38912db9b..65f5cbf69c6 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -4,6 +4,8 @@ class Deployment < ApplicationRecord
include AtomicInternalId
include IidRoutes
include AfterCommitQueue
+ include UpdatedAtFilterable
+ include Gitlab::Utils::StrongMemoize
belongs_to :project, required: true
belongs_to :environment, required: true
@@ -125,6 +127,12 @@ class Deployment < ApplicationRecord
@scheduled_actions ||= deployable.try(:other_scheduled_actions)
end
+ def playable_build
+ strong_memoize(:playable_build) do
+ deployable.try(:playable?) ? deployable : nil
+ end
+ end
+
def includes_commit?(commit)
return false unless commit
diff --git a/app/models/conversational_development_index/card.rb b/app/models/dev_ops_score/card.rb
index f9180bdd97b..b1894cf4138 100644
--- a/app/models/conversational_development_index/card.rb
+++ b/app/models/dev_ops_score/card.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module ConversationalDevelopmentIndex
+module DevOpsScore
class Card
attr_accessor :metric, :title, :description, :feature, :blog, :docs
diff --git a/app/models/conversational_development_index/idea_to_production_step.rb b/app/models/dev_ops_score/idea_to_production_step.rb
index e78a734693c..d892793cf97 100644
--- a/app/models/conversational_development_index/idea_to_production_step.rb
+++ b/app/models/dev_ops_score/idea_to_production_step.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module ConversationalDevelopmentIndex
+module DevOpsScore
class IdeaToProductionStep
attr_accessor :metric, :title, :features
diff --git a/app/models/conversational_development_index/metric.rb b/app/models/dev_ops_score/metric.rb
index b91123be87e..a9133128ce9 100644
--- a/app/models/conversational_development_index/metric.rb
+++ b/app/models/dev_ops_score/metric.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module ConversationalDevelopmentIndex
+module DevOpsScore
class Metric < ApplicationRecord
include Presentable
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index d7dc64190d6..5fdb5af2d9b 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -20,6 +20,28 @@ class EnvironmentStatus
build_environments_status(mr, user, mr.merge_pipeline)
end
+ def self.for_deployed_merge_request(mr, user)
+ statuses = []
+
+ mr.recent_visible_deployments.each do |deploy|
+ env = deploy.environment
+
+ next unless Ability.allowed?(user, :read_environment, env)
+
+ statuses <<
+ EnvironmentStatus.new(deploy.project, env, mr, deploy.sha)
+ end
+
+ # Existing projects that used deployments prior to the introduction of
+ # explicitly linked merge requests won't have any data using this new
+ # approach, so we fall back to retrieving deployments based on CI pipelines.
+ if statuses.any?
+ statuses
+ else
+ after_merge_request(mr, user)
+ end
+ end
+
def initialize(project, environment, merge_request, sha)
@project = project
@environment = environment
@@ -78,7 +100,7 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- pipeline.environments.available.map do |environment|
+ pipeline.environments.includes(:project).available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
diff --git a/app/models/epic.rb b/app/models/epic.rb
index 01ef8bd100e..8222bbf9656 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -3,7 +3,9 @@
# Placeholder class for model that is implemented in EE
# It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ApplicationRecord
- self.ignored_columns += %i[milestone_id]
+ include IgnorableColumns
+
+ ignore_column :milestone_id, remove_after: '2019-12-15', remove_with: '12.7'
def self.link_reference_pattern
nil
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 5b5c1b1b56b..3af14d7eef2 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -104,7 +104,7 @@ module ErrorTracking
def calculate_reactive_cache(request, opts)
case request
when 'list_issues'
- { issues: sentry_client.list_issues(**opts.symbolize_keys) }
+ sentry_client.list_issues(**opts.symbolize_keys)
when 'issue_details'
{
issue: sentry_client.issue_details(**opts.symbolize_keys)
diff --git a/app/models/import_failure.rb b/app/models/import_failure.rb
new file mode 100644
index 00000000000..998572853d3
--- /dev/null
+++ b/app/models/import_failure.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class ImportFailure < ApplicationRecord
+ belongs_to :project
+
+ validates :project, presence: true
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7e5a94fc0a1..4a17c93e7a2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -14,6 +14,7 @@ class Issue < ApplicationRecord
include TimeTrackable
include ThrottledTouch
include LabelEventable
+ include IgnorableColumns
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -68,8 +69,7 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
- # Only remove after 2019-12-22 and with %12.7
- self.ignored_columns += %i[state]
+ ignore_column :state, remove_with: '12.7', remove_after: '2019-12-22'
after_commit :expire_etag_cache
after_save :ensure_metrics, unless: :imported?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 16d36d42eac..a25d1ccccca 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -17,6 +17,7 @@ class MergeRequest < ApplicationRecord
include FromUnion
include DeprecatedAssignee
include ShaAttribute
+ include IgnorableColumns
sha_attribute :squash_commit_sha
@@ -73,6 +74,14 @@ class MergeRequest < ApplicationRecord
has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees
+ has_many :deployment_merge_requests
+
+ # These are deployments created after the merge request has been merged, and
+ # the merge request was tracked explicitly (instead of implicitly using a CI
+ # build).
+ has_many :deployments,
+ through: :deployment_merge_requests
+
KNOWN_MERGE_PARAMS = [
:auto_merge_strategy,
:should_remove_source_branch,
@@ -199,6 +208,9 @@ class MergeRequest < ApplicationRecord
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :from_project, ->(project) { where(source_project_id: project.id) }
+ scope :from_and_to_forks, ->(project) do
+ where('source_project_id <> target_project_id AND (source_project_id = ? OR target_project_id = ?)', project.id, project.id)
+ end
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :open_and_closed, -> { with_states(:opened, :closed) }
@@ -228,8 +240,7 @@ class MergeRequest < ApplicationRecord
with_state(:opened).where(auto_merge_enabled: true)
end
- # Only remove after 2019-12-22 and with %12.7
- self.ignored_columns += %i[state]
+ ignore_column :state, remove_with: '12.7', remove_after: '2019-12-22'
after_save :keep_around_commit
@@ -266,7 +277,7 @@ class MergeRequest < ApplicationRecord
def self.recent_target_branches(limit: 100)
group(:target_branch)
.select(:target_branch)
- .reorder('MAX(merge_requests.updated_at) DESC')
+ .reorder(arel_table[:updated_at].maximum.desc)
.limit(limit)
.pluck(:target_branch)
end
@@ -1468,6 +1479,14 @@ class MergeRequest < ApplicationRecord
all_pipelines.for_sha_or_source_sha(diff_head_sha).first
end
+ def etag_caching_enabled?
+ true
+ end
+
+ def recent_visible_deployments
+ deployments.visible.includes(:environment).order(id: :desc).limit(10)
+ end
+
private
def with_rebase_lock
diff --git a/app/models/merge_request/pipelines.rb b/app/models/merge_request/pipelines.rb
index cba38f781a6..c32f29a9304 100644
--- a/app/models/merge_request/pipelines.rb
+++ b/app/models/merge_request/pipelines.rb
@@ -12,15 +12,18 @@ class MergeRequest::Pipelines
attr_reader :merge_request
- delegate :all_commit_shas, :source_project, :source_branch, to: :merge_request
+ delegate :commit_shas, :source_project, :source_branch, to: :merge_request
def all
- return Ci::Pipeline.none unless source_project
-
strong_memoize(:all_pipelines) do
- pipelines = Ci::Pipeline.from_union(
- [source_pipelines, detached_pipelines, triggered_for_branch],
- remove_duplicates: false)
+ next Ci::Pipeline.none unless source_project
+
+ pipelines =
+ if merge_request.persisted?
+ pipelines_using_cte
+ else
+ triggered_for_branch.for_sha(commit_shas)
+ end
sort(pipelines)
end
@@ -28,38 +31,55 @@ class MergeRequest::Pipelines
private
- def triggered_by_merge_request
- source_project.ci_pipelines
- .where(source: :merge_request_event, merge_request: merge_request)
+ def pipelines_using_cte
+ cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha))
+
+ source_pipelines_join = cte.table[:sha].eq(Ci::Pipeline.arel_table[:source_sha])
+ source_pipelines = filter_by(triggered_by_merge_request, cte, source_pipelines_join)
+ detached_pipelines = filter_by_sha(triggered_by_merge_request, cte)
+ pipelines_for_branch = filter_by_sha(triggered_for_branch, cte)
+
+ Ci::Pipeline.with(cte.to_arel)
+ .from_union([source_pipelines, detached_pipelines, pipelines_for_branch])
+ end
+
+ def filter_by_sha(pipelines, cte)
+ hex = Arel::Nodes::SqlLiteral.new("'hex'")
+ string_sha = Arel::Nodes::NamedFunction.new('encode', [cte.table[:sha], hex])
+ join_condition = string_sha.eq(Ci::Pipeline.arel_table[:sha])
+
+ filter_by(pipelines, cte, join_condition)
end
- def detached_pipelines
- triggered_by_merge_request.for_sha(all_commit_shas)
+ def filter_by(pipelines, cte, join_condition)
+ shas_table =
+ Ci::Pipeline.arel_table
+ .join(cte.table, Arel::Nodes::InnerJoin)
+ .on(join_condition)
+ .join_sources
+
+ pipelines.joins(shas_table)
end
- def source_pipelines
- triggered_by_merge_request.for_source_sha(all_commit_shas)
+ def triggered_by_merge_request
+ source_project.ci_pipelines
+ .where(source: :merge_request_event, merge_request: merge_request)
end
def triggered_for_branch
source_project.ci_pipelines
.where(source: branch_pipeline_sources, ref: source_branch, tag: false)
- .for_sha(all_commit_shas)
- end
-
- def sources
- ::Ci::Pipeline.sources
end
def branch_pipeline_sources
strong_memoize(:branch_pipeline_sources) do
- sources.reject { |source| source == EVENT }.values
+ Ci::Pipeline.sources.reject { |source| source == EVENT }.values
end
end
def sort(pipelines)
sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
- query = ApplicationRecord.send(:sanitize_sql_array, [sql, sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend
+ query = ApplicationRecord.send(:sanitize_sql_array, [sql, Ci::Pipeline.sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend
pipelines.order(Arel.sql(query))
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 4cfbb963ddf..14207c48f66 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -31,6 +31,7 @@ class Project < ApplicationRecord
include FeatureGate
include OptionallySearch
include FromUnion
+ include IgnorableColumns
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -64,7 +65,7 @@ class Project < ApplicationRecord
# TODO: remove once GitLab 12.5 is released
# https://gitlab.com/gitlab-org/gitlab/issues/34638
- self.ignored_columns += %i[merge_requests_require_code_owner_approval]
+ ignore_column :merge_requests_require_code_owner_approval, remove_after: '2019-12-01', remove_with: '12.6'
default_value_for :archived, false
default_value_for :resolve_outdated_diff_discussions, false
@@ -169,6 +170,7 @@ class Project < ApplicationRecord
has_one :microsoft_teams_service
has_one :packagist_service
has_one :hangouts_chat_service
+ has_one :unify_circuit_service
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
@@ -179,6 +181,7 @@ class Project < ApplicationRecord
has_one :forked_from_project, through: :fork_network_member
has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id'
has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
+ has_many :fork_network_projects, through: :fork_network, source: :projects
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -295,6 +298,8 @@ class Project < ApplicationRecord
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
+ has_many :import_failures, 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
@@ -538,7 +543,11 @@ class Project < ApplicationRecord
#
# query - The search query as a String.
def search(query)
- fuzzy_search(query, [:path, :name, :description])
+ if Feature.enabled?(:project_search_by_full_path, default_enabled: true)
+ joins(:route).fuzzy_search(query, [Route.arel_table[:path], :name, :description])
+ else
+ fuzzy_search(query, [:path, :name, :description])
+ end
end
def search_by_title(query)
@@ -715,6 +724,10 @@ class Project < ApplicationRecord
Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
end
+ def unlink_forks_upon_visibility_decrease_enabled?
+ Feature.enabled?(:unlink_fork_network_upon_visibility_decrease, self)
+ end
+
def empty_repo?
repository.empty?
end
@@ -1533,6 +1546,7 @@ class Project < ApplicationRecord
# update visibility_level of forks
def update_forks_visibility_level
+ return if unlink_forks_upon_visibility_decrease_enabled?
return unless visibility_level < visibility_level_before_last_save
forks.each do |forked_project|
@@ -2242,12 +2256,13 @@ class Project < ApplicationRecord
# Git objects are only poolable when the project is or has:
# - Hashed storage -> The object pool will have a remote to its members, using relative paths.
# If the repository path changes we would have to update the remote.
- # - Public -> User will be able to fetch Git objects that might not exist
- # in their own repository.
+ # - not private -> The visibility level or repository access level has to be greater than private
+ # to prevent fetching objects that might not exist
# - Repository -> Else the disk path will be empty, and there's nothing to pool
def git_objects_poolable?
hashed_storage?(:repository) &&
- public? &&
+ visibility_level > Gitlab::VisibilityLevel::PRIVATE &&
+ repository_access_level > ProjectFeature::PRIVATE &&
repository_exists? &&
Gitlab::CurrentSettings.hashed_storage_enabled
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index d089a004d3d..b292d39dae7 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
class ProjectCiCdSetting < ApplicationRecord
- # TODO: remove once GitLab 12.7 is released
+ include IgnorableColumns
# https://gitlab.com/gitlab-org/gitlab/issues/36651
- self.ignored_columns += %i[merge_trains_enabled]
+ ignore_column :merge_trains_enabled, remove_with: '12.7', remove_after: '2019-12-22'
+
belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table.
diff --git a/app/models/project_services/unify_circuit_service.rb b/app/models/project_services/unify_circuit_service.rb
new file mode 100644
index 00000000000..06f2d10f83b
--- /dev/null
+++ b/app/models/project_services/unify_circuit_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class UnifyCircuitService < ChatNotificationService
+ def title
+ 'Unify Circuit'
+ end
+
+ def description
+ 'Receive event notifications in Unify Circuit'
+ end
+
+ def self.to_param
+ 'unify_circuit'
+ end
+
+ def help
+ 'This service sends notifications about projects events to a Unify Circuit conversation.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
+ <li>Select events below to enable notifications.</li>
+ </ol>'
+ end
+
+ def event_field(event)
+ end
+
+ def default_channel_placeholder
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { type: 'select', name: 'branches_to_be_notified', choices: BRANCH_CHOICES }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ response = Gitlab::HTTP.post(webhook, body: {
+ subject: message.project_name,
+ text: message.pretext,
+ markdown: true
+ }.to_json)
+
+ response if response.success?
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5e547cf509b..2a67c26d840 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -925,7 +925,22 @@ class Repository
def ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil?
- raw_repository.ancestor?(ancestor_id, descendant_id)
+ counter = Gitlab::Metrics.counter(
+ :repository_ancestor_calls_total,
+ 'The number of times we call Repository#ancestor with valid arguments')
+ cache_hit = true
+
+ cache_key = "ancestor:#{ancestor_id}:#{descendant_id}"
+ result = request_store_cache.fetch(cache_key) do
+ cache.fetch(cache_key) do
+ cache_hit = false
+ raw_repository.ancestor?(ancestor_id, descendant_id)
+ end
+ end
+
+ counter.increment(cache_hit: cache_hit.to_s)
+
+ result
end
def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true)
diff --git a/app/models/service.rb b/app/models/service.rb
index 08936f7fcbd..95b7c6927cf 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -289,6 +289,7 @@ class Service < ApplicationRecord
slack
teamcity
microsoft_teams
+ unify_circuit
]
if Rails.env.development?
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 51ab94e6f4a..b802ea2fd59 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -46,6 +46,18 @@ class Snippet < ApplicationRecord
length: { maximum: 255 }
validates :content, presence: true
+ validates :content,
+ length: {
+ maximum: ->(_) { Gitlab::CurrentSettings.snippet_size_limit },
+ message: -> (_, data) do
+ current_value = ActiveSupport::NumberHelper.number_to_human_size(data[:value].size)
+ max_size = ActiveSupport::NumberHelper.number_to_human_size(Gitlab::CurrentSettings.snippet_size_limit)
+
+ _("is too long (%{current_value}). The maximum size is %{max_size}.") % { current_value: current_value, max_size: max_size }
+ end
+ },
+ if: :content_changed?
+
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
# Scopes
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
index e9f25d833d0..ef0b2407e23 100644
--- a/app/models/user_callout_enums.rb
+++ b/app/models/user_callout_enums.rb
@@ -14,7 +14,8 @@ module UserCalloutEnums
gke_cluster_integration: 1,
gcp_signup_offer: 2,
cluster_security_warning: 3,
- suggest_popover_dismissed: 9
+ suggest_popover_dismissed: 9,
+ tabs_position_highlight: 10
}
end
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index eca73f0a241..f212bb06bc9 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -75,12 +75,15 @@ class GlobalPolicy < BasePolicy
rule { ~anonymous }.policy do
enable :read_instance_metadata
+ enable :create_personal_snippet
end
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
end
+
+ rule { external_user }.prevent :create_personal_snippet
end
GlobalPolicy.prepend_if_ee('EE::GlobalPolicy')
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 03869412d0c..5c62bdd0d95 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -17,9 +17,6 @@ class PersonalSnippetPolicy < BasePolicy
enable :create_note
end
- rule { ~anonymous }.enable :create_personal_snippet
- rule { external_user }.prevent :create_personal_snippet
-
rule { internal_snippet & ~external_user }.policy do
enable :read_personal_snippet
enable :create_note
diff --git a/app/presenters/conversational_development_index/metric_presenter.rb b/app/presenters/dev_ops_score/metric_presenter.rb
index 9639b84cf56..d22beefee54 100644
--- a/app/presenters/conversational_development_index/metric_presenter.rb
+++ b/app/presenters/dev_ops_score/metric_presenter.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-module ConversationalDevelopmentIndex
+module DevOpsScore
class MetricPresenter < Gitlab::View::Presenter::Simple
def cards
[
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index e6421315b34..94773eeebd0 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -37,6 +37,9 @@ class DeploymentEntity < Grape::Entity
expose :commit, using: CommitEntity, if: -> (*) { include_details? }
expose :manual_actions, using: JobEntity, if: -> (*) { include_details? && can_create_deployment? }
expose :scheduled_actions, using: JobEntity, if: -> (*) { include_details? && can_create_deployment? }
+ expose :playable_build, expose_nil: false, if: -> (*) { include_details? && can_create_deployment? } do |deployment, options|
+ JobEntity.represent(deployment.playable_build, options.merge(only: [:play_path, :retry_path]))
+ end
expose :cluster, using: ClusterBasicEntity
@@ -47,7 +50,7 @@ class DeploymentEntity < Grape::Entity
end
def can_create_deployment?
- can?(request.current_user, :create_deployment, request.project)
+ can?(request.current_user, :create_deployment, project)
end
def can_read_deployables?
@@ -56,6 +59,10 @@ class DeploymentEntity < Grape::Entity
# because it triggers a policy evaluation that involves multiple
# Gitaly calls that might not be cached.
#
- can?(request.current_user, :read_build, request.project)
+ can?(request.current_user, :read_build, project)
+ end
+
+ def project
+ request.try(:project) || options[:project]
end
end
diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb
index 811cc2ad5af..40db23c143e 100644
--- a/app/serializers/environment_status_entity.rb
+++ b/app/serializers/environment_status_entity.rb
@@ -37,6 +37,10 @@ class EnvironmentStatusEntity < Grape::Entity
es.deployment.try(:formatted_deployment_time)
end
+ expose :deployment, as: :details do |es, options|
+ DeploymentEntity.represent(es.deployment, options.merge(project: es.project, only: [:playable_build]))
+ end
+
expose :changes
private
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 2a61187a856..028de38e42a 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-class MergeRequestPollWidgetEntity < IssuableEntity
+class MergeRequestPollWidgetEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :auto_merge_strategy
expose :available_auto_merge_strategies do |merge_request|
AutoMergeService.new(merge_request.project, current_user).available_strategies(merge_request) # rubocop: disable CodeReuse/ServiceClass
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 94e8b174f0f..cddb894fd64 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -77,6 +77,10 @@ class PipelineEntity < Grape::Entity
cancel_project_pipeline_path(pipeline.project, pipeline)
end
+ expose :failed_builds, if: -> (*) { can_retry? }, using: JobEntity do |pipeline|
+ pipeline.builds.failed
+ end
+
private
alias_method :pipeline, :object
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
index a2c8e6e6619..132e9dfa7bd 100644
--- a/app/services/error_tracking/list_issues_service.rb
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -6,37 +6,24 @@ module ErrorTracking
DEFAULT_LIMIT = 20
DEFAULT_SORT = 'last_seen'
- def execute
- return error('Error Tracking is not enabled') unless enabled?
- return error('Access denied', :unauthorized) unless can_read?
-
- result = project_error_tracking_setting.list_sentry_issues(
- issue_status: issue_status,
- limit: limit,
- search_term: search_term,
- sort: sort
- )
-
- # our results are not yet ready
- unless result
- return error('Not ready. Try again later', :no_content)
- end
-
- if result[:error].present?
- return error(result[:error], http_status_for(result[:error_type]))
- end
-
- success(issues: result[:issues])
- end
-
def external_url
project_error_tracking_setting&.sentry_external_url
end
private
+ def fetch
+ project_error_tracking_setting.list_sentry_issues(
+ issue_status: issue_status,
+ limit: limit,
+ search_term: params[:search_term].presence,
+ sort: sort,
+ cursor: params[:cursor].presence
+ )
+ end
+
def parse_response(response)
- { issues: response[:issues] }
+ response.slice(:issues, :pagination)
end
def issue_status
@@ -47,18 +34,6 @@ module ErrorTracking
params[:limit] || DEFAULT_LIMIT
end
- def search_term
- params[:search_term].presence
- end
-
- def enabled?
- project_error_tracking_setting&.enabled?
- end
-
- def can_read?
- can?(current_user, :read_sentry_issue, project)
- end
-
def sort
params[:sort] || DEFAULT_SORT
end
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 273a12f386a..bbb3c2ad050 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -4,19 +4,18 @@ module Issuable
class BulkUpdateService
include Gitlab::Allowable
- attr_accessor :current_user, :params
+ attr_accessor :parent, :current_user, :params
- def initialize(user = nil, params = {})
- @current_user, @params = user, params.dup
+ def initialize(parent, user = nil, params = {})
+ @parent, @current_user, @params = parent, user, params.dup
end
- # rubocop: disable CodeReuse/ActiveRecord
def execute(type)
model_class = type.classify.constantize
update_class = type.classify.pluralize.constantize::UpdateService
ids = params.delete(:issuable_ids).split(",")
- items = model_class.where(id: ids)
+ items = find_issuables(parent, model_class, ids)
permitted_attrs(type).each do |key|
params.delete(key) unless params[key].present?
@@ -37,7 +36,6 @@ module Issuable
success: !items.count.zero?
}
end
- # rubocop: enable CodeReuse/ActiveRecord
private
@@ -50,5 +48,15 @@ module Issuable
attrs.push(:assignee_id)
end
end
+
+ def find_issuables(parent, model_class, ids)
+ if parent.is_a?(Project)
+ model_class.id_in(ids).of_projects(parent)
+ elsif parent.is_a?(Group)
+ model_class.id_in(ids).of_projects(parent.all_projects)
+ end
+ end
end
end
+
+Issuable::BulkUpdateService.prepend_if_ee('EE::Issuable::BulkUpdateService')
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 48ed5afbc2a..974f7e598ca 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -36,3 +36,5 @@ module Issues
end
end
end
+
+Issues::BaseService.prepend_if_ee('EE::Issues::BaseService')
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index 60591e9a6f3..44b58ad9729 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -133,7 +133,7 @@ module Metrics
def uid_regex
base_url = @project.grafana_integration.grafana_url.chomp('/')
- %r{(#{Regexp.escape(base_url)}\/d\/(?<uid>\w+)\/)}x
+ %r{^(#{Regexp.escape(base_url)}\/d\/(?<uid>.+)\/)}x
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 90e703e7050..cbed794f92e 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -31,13 +31,6 @@ module Projects
Projects::UnlinkForkService.new(project, current_user).execute
- # The project is not necessarily a fork, so update the fork network originating
- # from this project
- if fork_network = project.root_of_fork_network
- fork_network.update(root_project: nil,
- deleted_root_project_name: project.full_name)
- end
-
attempt_destroy_transaction(project)
system_hook_service.execute_hooks_for(project, :destroy)
diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb
index 696e1b665b2..c5e38f166da 100644
--- a/app/services/projects/overwrite_project_service.rb
+++ b/app/services/projects/overwrite_project_service.rb
@@ -7,7 +7,9 @@ module Projects
Project.transaction do
move_before_destroy_relationships(source_project)
- destroy_old_project(source_project)
+ # Reset is required in order to get the proper
+ # uncached fork network method calls value.
+ destroy_old_project(source_project.reset)
rename_project(source_project.name, source_project.path)
@project
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index 1b8a920268f..e7e0141099e 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -2,34 +2,67 @@
module Projects
class UnlinkForkService < BaseService
- # rubocop: disable CodeReuse/ActiveRecord
+ # If a fork is given, it:
+ #
+ # - Saves LFS objects to the root project
+ # - Close existing MRs coming from it
+ # - Is removed from the fork network
+ #
+ # If a root of fork(s) is given, it does the same,
+ # but not updating LFS objects (there'll be no related root to cache it).
def execute
- return unless @project.forked?
+ fork_network = @project.fork_network
- if fork_source = @project.fork_source
- fork_source.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project unless lfs_object.projects.include?(@project)
- end
+ return unless fork_network
- refresh_forks_count(fork_source)
- end
+ save_lfs_objects
- merge_requests = @project.fork_network
+ merge_requests = fork_network
.merge_requests
.opened
- .where.not(target_project: @project)
- .from_project(@project)
+ .from_and_to_forks(@project)
- merge_requests.each do |mr|
+ merge_requests.find_each do |mr|
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
- @project.fork_network_member.destroy
+ Project.transaction do
+ # Get out of the fork network as a member and
+ # remove references from all its direct forks.
+ @project.fork_network_member.destroy
+ @project.forked_to_members.update_all(forked_from_project_id: nil)
+
+ # The project is not necessarily a fork, so update the fork network originating
+ # from this project
+ if fork_network = @project.root_of_fork_network
+ fork_network.update(root_project: nil, deleted_root_project_name: @project.full_name)
+ end
+ end
+
+ # When the project getting out of the network is a node with parent
+ # and children, both the parent and the node needs a cache refresh.
+ [@project.forked_from_project, @project].compact.each do |project|
+ refresh_forks_count(project)
+ end
end
- # rubocop: enable CodeReuse/ActiveRecord
+
+ private
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
+
+ def save_lfs_objects
+ return unless @project.forked?
+
+ lfs_storage_project = @project.lfs_storage_project
+
+ return unless lfs_storage_project
+ return if lfs_storage_project == @project # that project is being unlinked
+
+ lfs_storage_project.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << @project unless lfs_object.projects.include?(@project)
+ end
+ end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 2dad1d05a2c..aedd7252f63 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -65,7 +65,7 @@ module Projects
)
project_changed_feature_keys = project.project_feature.previous_changes.keys
- if project.previous_changes.include?(:visibility_level) && project.private?
+ if project.visibility_level_previous_changes && project.private?
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
@@ -79,6 +79,11 @@ module Projects
system_hook_service.execute_hooks_for(project, :update)
end
+ if project.visibility_level_decreased? && project.unlink_forks_upon_visibility_decrease_enabled?
+ # It's a system-bounded operation, so no extra authorization check is required.
+ Projects::UnlinkForkService.new(project, current_user).execute
+ end
+
update_pages_config if changing_pages_related_config?
end
diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb
deleted file mode 100644
index 6ed42054ac3..00000000000
--- a/app/services/repair_ldap_blocked_user_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-class RepairLdapBlockedUserService
- attr_accessor :user
-
- def initialize(user)
- @user = user
- end
-
- def execute
- user.block if ldap_hard_blocked?
- end
-
- private
-
- def ldap_hard_blocked?
- user.ldap_blocked? && !user.ldap_user?
- end
-end
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 415a02ab337..7927ab265c5 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -38,7 +38,7 @@ class SubmitUsagePingService
def store_metrics(response)
return unless response['conv_index'].present?
- ConversationalDevelopmentIndex::Metric.create!(
+ DevOpsScore::Metric.create!(
response['conv_index'].slice(*METRICS)
)
end
diff --git a/app/services/users/repair_ldap_blocked_service.rb b/app/services/users/repair_ldap_blocked_service.rb
new file mode 100644
index 00000000000..378145a65b3
--- /dev/null
+++ b/app/services/users/repair_ldap_blocked_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Users
+ class RepairLdapBlockedService
+ attr_accessor :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def execute
+ user.block if ldap_hard_blocked?
+ end
+
+ private
+
+ def ldap_hard_blocked?
+ user.ldap_blocked? && !user.ldap_user?
+ end
+ end
+end
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 962234d3aea..03b7ae76de9 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -40,6 +40,13 @@
= f.color_field :font, class: "form-control text-font-color"
.form-group.row
.col-sm-2.col-form-label
+ = f.label :target_path, _('Target Path')
+ .col-sm-10
+ = f.text_field :target_path, class: "form-control"
+ .form-text.text-muted
+ = _('Paths can contain wildcards, like */welcome')
+ .form-group.row
+ .col-sm-2.col-form-label
= f.label :starts_at, _("Starts at (UTC)")
.col-sm-10.datetime-controls
= f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index eb4dfdf2858..4731421fd9e 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -19,6 +19,7 @@
%th Preview
%th Starts
%th Ends
+ %th Target Path
%th &nbsp;
%tbody
- @broadcast_messages.each do |message|
@@ -32,6 +33,8 @@
%td
= message.ends_at
%td
+ = message.target_path
+ %td
= link_to sprite_icon('pencil-square'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn'
= link_to sprite_icon('remove'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-danger'
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 60ca7e4e267..793ddef2c58 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -17,3 +17,4 @@
%span{ class: "award-control-icon award-control-icon-positive" }= sprite_icon('smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= sprite_icon('smile')
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
+ = yield
diff --git a/app/views/clusters/clusters/_namespace.html.haml b/app/views/clusters/clusters/_namespace.html.haml
index 0c64819ad62..8a86fd90963 100644
--- a/app/views/clusters/clusters/_namespace.html.haml
+++ b/app/views/clusters/clusters/_namespace.html.haml
@@ -1,4 +1,4 @@
-- managed_namespace_help_text = s_('ClusterIntegration|Choose a prefix to be used for your namespaces. Defaults to your project path.')
+- managed_namespace_help_text = s_('ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- non_managed_namespace_help_text = s_('ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, pod logs, and Web terminals.')
- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'gitlab-managed-clusters'), target: '_blank'
diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml
index e2dbdcbb939..ee3ca824342 100644
--- a/app/views/layouts/_broadcast.html.haml
+++ b/app/views/layouts/_broadcast.html.haml
@@ -1,2 +1,2 @@
-- BroadcastMessage.current&.each do |message|
+- current_broadcast_messages&.each do |message|
= broadcast_message(message)
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index c38f96f302a..f4ab491a38e 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
+ %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data }
= render "layouts/init_auto_complete" if @gfm_form
= render "layouts/init_client_detection_flags"
= render 'peek/bar'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index e28efb09be5..30109621515 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -38,4 +38,5 @@
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group?
%li= link_to _('New group'), new_group_path
- %li= link_to _('New snippet'), new_snippet_path, class: 'qa-global-new-snippet-link'
+ - if current_user.can?(:create_personal_snippet)
+ %li= link_to _('New snippet'), new_snippet_path, class: 'qa-global-new-snippet-link'
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index f53bd2b5e4d..1b799477093 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -2,7 +2,7 @@
- hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
- .breadcrumbs-container
+ .breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border && mr_tabs_position_enabled?) }
- if defined?(@left_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only= _("Open sidebar")
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index a6d2c894185..a027dca1b56 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -44,7 +44,7 @@
- if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'analytics#show') do
- = link_to group_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right' } do
+ = link_to group_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right', qa_selector: 'contribution_analytics_link' } do
%span
= _('Contribution Analytics')
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 6a7cb1499c5..7abac2d14e4 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -15,7 +15,7 @@
%li.breadcrumb-item
= link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path)
- .tree-controls
+ .tree-controls<
= link_to download_project_job_artifacts_path(@project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= sprite_icon('download')
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index a4fb5f6ba88..e611df8df2a 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -17,21 +17,19 @@
- else
= link_to title, project_tree_path(@project, tree_join(@ref, path))
- .tree-controls
+ .tree-controls<
= render 'projects/find_file_link'
+ -# only show normal/blame view links for text files
+ - if blob.readable_text?
+ - if blame
+ = link_to 'Normal view', project_blob_path(@project, @id),
+ class: 'btn'
+ - else
+ = link_to 'Blame', project_blame_path(@project, @id),
+ class: 'btn js-blob-blame-link' unless blob.empty?
- .btn-group{ role: "group" }<
- -# only show normal/blame view links for text files
- - if blob.readable_text?
- - if blame
- = link_to 'Normal view', project_blob_path(@project, @id),
- class: 'btn'
- - else
- = link_to 'Blame', project_blame_path(@project, @id),
- class: 'btn js-blob-blame-link' unless blob.empty?
+ = link_to 'History', project_commits_path(@project, @id),
+ class: 'btn'
- = link_to 'History', project_commits_path(@project, @id),
- class: 'btn'
-
- = link_to 'Permalink', project_blob_path(@project,
- tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url'
+ = link_to 'Permalink', project_blob_path(@project,
+ tree_join(@commit.sha, @path)), class: 'btn js-data-file-blob-permalink-url'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index bbe0a2c97fd..f1a7528065a 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -7,7 +7,7 @@
- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project
- if show_menu
- .project-action-button.dropdown.inline
+ .project-action-button.dropdown.inline<
%a.btn.dropdown-toggle.has-tooltip.qa-create-new-dropdown{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
= icon('plus')
= icon("caret-down")
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index e155e3758fb..3f1d44a488a 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -13,7 +13,7 @@
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
- .tree-controls.d-none.d-sm-none.d-md-block
+ .tree-controls.d-none.d-sm-none.d-md-block<
- if @merge_request.present?
.control
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn'
diff --git a/app/views/projects/merge_requests/_awards_block.html.haml b/app/views/projects/merge_requests/_awards_block.html.haml
new file mode 100644
index 00000000000..1eab28a2ff3
--- /dev/null
+++ b/app/views/projects/merge_requests/_awards_block.html.haml
@@ -0,0 +1,5 @@
+.content-block.content-block-small.emoji-list-container.js-noteable-awards
+ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true do
+ - if mr_tabs_position_enabled?
+ .ml-auto.mt-auto.mb-auto
+ = render "projects/merge_requests/discussion_filter"
diff --git a/app/views/projects/merge_requests/_description.html.haml b/app/views/projects/merge_requests/_description.html.haml
new file mode 100644
index 00000000000..354a384b647
--- /dev/null
+++ b/app/views/projects/merge_requests/_description.html.haml
@@ -0,0 +1,9 @@
+%div
+ - if @merge_request.description.present?
+ .description.qa-description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' }
+ .md
+ = markdown_field(@merge_request, :description)
+ %textarea.hidden.js-task-list-field
+ = @merge_request.description
+
+ = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
diff --git a/app/views/projects/merge_requests/_discussion_filter.html.haml b/app/views/projects/merge_requests/_discussion_filter.html.haml
new file mode 100644
index 00000000000..96886661a8d
--- /dev/null
+++ b/app/views/projects/merge_requests/_discussion_filter.html.haml
@@ -0,0 +1,2 @@
+#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
+ notes_filters: UserPreference.notes_filters.to_json } }
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index 4f09f47d795..ec78b040167 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,13 +1,6 @@
-.detail-page-description
- %h2.title.qa-title
+.detail-page-description{ class: ("py-2" if mr_tabs_position_enabled?) }
+ %h2.title.qa-title{ class: ("mb-0" if mr_tabs_position_enabled?) }
= markdown_field(@merge_request, :title)
- %div
- - if @merge_request.description.present?
- .description.qa-description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' }
- .md
- = markdown_field(@merge_request, :description)
- %textarea.hidden.js-task-list-field
- = @merge_request.description
-
- = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom')
+ - unless mr_tabs_position_enabled?
+ = render "projects/merge_requests/description"
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 552f8dc173a..d1e8dc3a834 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -1,3 +1,4 @@
+- @no_breadcrumb_border = true
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
@@ -6,7 +7,7 @@
.alert.alert-danger
The source project of this merge request has been removed.
-.detail-page-header
+.detail-page-header{ class: ("border-bottom-0 pt-0 pb-0" if mr_tabs_position_enabled?) }
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= sprite_icon(state_icon_name, size: 16, css_class: 'd-block d-sm-none')
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
new file mode 100644
index 00000000000..f056189fec0
--- /dev/null
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -0,0 +1,13 @@
+- if @merge_request.source_branch_exists?
+ = render "projects/merge_requests/how_to_merge"
+
+= javascript_tag nonce: true do
+ :plain
+ window.gl = window.gl || {};
+ window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
+
+ window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
+ window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}';
+ window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.html', anchor: 'security-approvals-in-merge-requests-ultimate')}';
+
+#js-vue-mr-widget.mr-widget
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index c2bc5376fd7..310cd355d22 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -14,56 +14,54 @@
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box"
- - if @merge_request.source_branch_exists?
- = render "projects/merge_requests/how_to_merge"
-
- = javascript_tag nonce: true do
- :plain
- window.gl = window.gl || {};
- window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
-
- window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
- window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}';
- window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.html', anchor: 'security-approvals-in-merge-requests-ultimate')}';
-
- #js-vue-mr-widget.mr-widget
-
- .content-block.content-block-small.emoji-list-container.js-noteable-awards
- = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
+ - unless mr_tabs_position_enabled?
+ = render "projects/merge_requests/widget"
+ = render "projects/merge_requests/awards_block"
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container
%ul.merge-request-tabs.nav-tabs.nav.nav-links
- %li.notes-tab{ data: { qa_selector: 'notes_tab'} }
+ = render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
- = _("Discussion")
+ - if mr_tabs_position_enabled?
+ = _("Overview")
+ - else
+ = _("Discussion")
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
- %li.commits-tab
+ = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab" do
= tab_link_for @merge_request, :commits do
= _("Commits")
%span.badge.badge-pill= @commits_count
- if number_of_pipelines.nonzero?
- %li.pipelines-tab
+ = render "projects/merge_requests/tabs/tab", name: "pipelines", class: "pipelines-tab" do
= tab_link_for @merge_request, :pipelines do
= _("Pipelines")
%span.badge.badge-pill.js-pipelines-mr-count= number_of_pipelines
- %li.diffs-tab.qa-diffs-tab
+ = render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab qa-diffs-tab", id: "diffs-tab" do
= tab_link_for @merge_request, :diffs do
= _("Changes")
%span.badge.badge-pill= @merge_request.diff_size
+ - if mr_tabs_position_enabled? && show_tabs_feature_highlight?
+ .js-tabs-feature-highlight{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::TABS_POSITION_HIGHLIGHT } }
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
- #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
- notes_filters: UserPreference.notes_filters.to_json } }
+ - unless mr_tabs_position_enabled?
+ = render "projects/merge_requests/discussion_filter"
#js-vue-discussion-counter
.tab-content#diff-notes-app
#js-diff-file-finder
- #notes.notes.tab-pane.voting_notes
+ = render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
.row
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
+ - if mr_tabs_position_enabled?
+ - if @merge_request.description.present?
+ .detail-page-description
+ = render "projects/merge_requests/description"
+ = render "projects/merge_requests/widget"
+ = render "projects/merge_requests/awards_block"
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
noteable_type: 'MergeRequest',
@@ -71,12 +69,12 @@
help_page_path: suggest_changes_help_path,
current_user_data: @current_user_data} }
- #commits.commits.tab-pane
+ = render "projects/merge_requests/tabs/pane", name: "commits", id: "commits", class: "commits" do
-# This tab is always loaded via AJAX
- #pipelines.pipelines.tab-pane
+ = render "projects/merge_requests/tabs/pane", name: "pipelines", id: "pipelines", class: "pipelines" do
- if number_of_pipelines.nonzero?
= render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?,
+ = render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: { "is-locked": @merge_request.discussion_locked?,
endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
endpoint_metadata: diffs_metadata_project_json_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
endpoint_batch: diffs_batch_project_json_merge_request_path(@project, @merge_request, 'json', request.query_parameters),
@@ -87,7 +85,7 @@
is_fluid_layout: fluid_layout.to_s,
dismiss_endpoint: user_callouts_path,
show_suggest_popover: show_suggest_popover?.to_s,
- show_whitespace_default: @show_whitespace_default.to_s } }
+ show_whitespace_default: @show_whitespace_default.to_s }
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/tabs/_pane.html.haml b/app/views/projects/merge_requests/tabs/_pane.html.haml
new file mode 100644
index 00000000000..1a88d5f5134
--- /dev/null
+++ b/app/views/projects/merge_requests/tabs/_pane.html.haml
@@ -0,0 +1,7 @@
+- tab_name = local_assigns.fetch(:name, nil)
+- tab_id = local_assigns.fetch(:id, nil)
+- tab_class = local_assigns.fetch(:class, nil)
+- tab_data = local_assigns.fetch(:data, nil)
+
+.tab-pane{ id: tab_id, class: tab_class, style: ("display: block" if params[:tab] == tab_name), data: tab_data }
+ = yield
diff --git a/app/views/projects/merge_requests/tabs/_tab.html.haml b/app/views/projects/merge_requests/tabs/_tab.html.haml
new file mode 100644
index 00000000000..dcd8db90509
--- /dev/null
+++ b/app/views/projects/merge_requests/tabs/_tab.html.haml
@@ -0,0 +1,7 @@
+- tab_name = local_assigns.fetch(:name, nil)
+- tab_class = local_assigns.fetch(:class, nil)
+- qa_selector = local_assigns.fetch(:qa_selector, nil)
+- id = local_assigns.fetch(:id, nil)
+
+%li{ class: [tab_class, ("active" if params[:tab] == tab_name)], id: id, data: { qa_selector: qa_selector } }
+ = yield
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index b04d484c8a7..75805192a61 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,7 +1,7 @@
- commit = @repository.commit(tag.dereferenced_target)
- release = @releases.find { |release| release.tag == tag.name }
-%li.flex-row
- .row-main-content.str-truncated
+%li.flex-row.allow-wrap
+ .row-main-content
= icon('tag')
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
@@ -26,7 +26,7 @@
= _("Release")
= link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'tag-release-link'
- if release.description.present?
- .description.md.prepend-top-default
+ .md.prepend-top-default
= markdown_field(release, :description)
.row-fixed-content.controls.flex-row
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 127734ddfd7..2d987744dfd 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -75,7 +75,7 @@
= link_to new_project_tag_path(@project) do
#{ _('New tag') }
-.tree-controls
+.tree-controls<
= render_if_exists 'projects/tree/lock_link'
- if vue_file_list_enabled?
#js-tree-history-link.d-inline-block{ data: { history_link: project_commits_path(@project, @ref) } }
@@ -85,20 +85,19 @@
= render 'projects/find_file_link'
- if can_create_mr_from_fork
- = succeed " " do
- - if can_collaborate || current_user&.already_forked?(@project)
- - if vue_file_list_enabled?
- #js-tree-web-ide-link.d-inline-block
- - else
- = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
- = _('Web IDE')
+ - if can_collaborate || current_user&.already_forked?(@project)
+ - if vue_file_list_enabled?
+ #js-tree-web-ide-link.d-inline-block
- else
- = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
+ = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
= _('Web IDE')
- = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
+ - else
+ = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
+ = _('Web IDE')
+ = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path)
- if show_xcode_link?(@project)
- .project-action-button.project-xcode.inline
+ .project-action-button.project-xcode.inline<
= render "projects/buttons/xcode_link"
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 37f4efee9d2..2dbd2a74602 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -1,7 +1,7 @@
- snippet_blob = chunk_snippet(snippet_blob, @search_term)
- snippet = snippet_blob[:snippet_object]
- snippet_chunks = snippet_blob[:snippet_chunks]
-- snippet_path = reliable_snippet_path(snippet)
+- snippet_path = snippet_path(snippet)
.search-result-row
%span
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 7280146720e..a544e59c405 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4.snippet-title.term
- = link_to reliable_snippet_path(snippet_title) do
+ = link_to snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
= snippet_badge(snippet_title)
%span.cgray.monospace.tiny.float-right.term
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 875cacd1f4f..2eb96a7bc9b 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -6,7 +6,7 @@
- if is_current_user
- if can_update
= link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}", data: { qa_selector: 'close_issue_button' }
- if can_reopen
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}", data: { qa_selector: 'reopen_issue_button' }
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 1fef43c0c37..be574155436 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -18,7 +18,7 @@
.col-lg-4
%h4.prepend-top-0= _('Notification events')
%p
- - notification_link = link_to _('notification emails'), help_page_path('workflow/notifications'), target: '_blank'
+ - notification_link = link_to _('notification emails'), help_page_path('user/profile/notifications'), target: '_blank'
- paragraph = _('Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.') % { notification_link: notification_link.html_safe }
#{ paragraph.html_safe }
.col-lg-8
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 5602ea37b5c..7d102a1b280 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -5,7 +5,7 @@
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
.title
- = link_to reliable_snippet_path(snippet) do
+ = link_to snippet_path(snippet) do
= snippet.title
- if snippet.file_name.present?
%span.snippet-filename.d-none.d-sm-inline-block.ml-2
@@ -14,7 +14,7 @@
%ul.controls
%li
- = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
+ = link_to snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
= icon('comments')
= notes_count
%li
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 9952f373156..a408278f7c9 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -7,8 +7,9 @@
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do
= _("Delete")
- = link_to new_snippet_path, class: "btn btn-grouped btn-success btn-inverted", title: _("New snippet") do
- = _("New snippet")
+ - if can?(current_user, :create_personal_snippet)
+ = link_to new_snippet_path, class: "btn btn-grouped btn-success btn-inverted", title: _("New snippet") do
+ = _("New snippet")
- if @snippet.submittable_as_spam_by?(current_user)
= link_to _('Submit as spam'), mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam')
.d-block.d-sm-none.dropdown
@@ -17,9 +18,10 @@
= icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
- %li
- = link_to new_snippet_path, title: _("New snippet") do
- = _("New snippet")
+ - if can?(current_user, :create_personal_snippet)
+ %li
+ = link_to new_snippet_path, title: _("New snippet") do
+ = _("New snippet")
- if can?(current_user, :admin_personal_snippet, @snippet)
%li
= link_to snippet_path(@snippet), method: :delete, data: { confirm: _("Are you sure?") }, title: _('Delete Snippet') do
diff --git a/changelogs/unreleased/13979-dashboard-empty-state.yml b/changelogs/unreleased/13979-dashboard-empty-state.yml
new file mode 100644
index 00000000000..90a84833708
--- /dev/null
+++ b/changelogs/unreleased/13979-dashboard-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Use better context-specific empty state screens for the Security Dashboards
+merge_request: 18382
+author:
+type: changed
diff --git a/changelogs/unreleased/17580-enable-etag-caching-notes-for-mrs.yml b/changelogs/unreleased/17580-enable-etag-caching-notes-for-mrs.yml
new file mode 100644
index 00000000000..65e274c0bb0
--- /dev/null
+++ b/changelogs/unreleased/17580-enable-etag-caching-notes-for-mrs.yml
@@ -0,0 +1,5 @@
+---
+title: Enable ETag caching for MR notes polling
+merge_request: 20440
+author:
+type: performance
diff --git a/changelogs/unreleased/31611-limit-the-number-of-stored-sessions-per-user.yml b/changelogs/unreleased/31611-limit-the-number-of-stored-sessions-per-user.yml
new file mode 100644
index 00000000000..aac9e94e1cf
--- /dev/null
+++ b/changelogs/unreleased/31611-limit-the-number-of-stored-sessions-per-user.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Limit the number of stored sessions per user
+merge_request: 19325
+author:
+type: added
diff --git a/changelogs/unreleased/31830-limit-mr-target-branches.yml b/changelogs/unreleased/31830-limit-mr-target-branches.yml
new file mode 100644
index 00000000000..6247e74f8a6
--- /dev/null
+++ b/changelogs/unreleased/31830-limit-mr-target-branches.yml
@@ -0,0 +1,5 @@
+---
+title: Require group_id or project_id for MR target branch autocomplete action
+merge_request: 20933
+author:
+type: performance
diff --git a/changelogs/unreleased/32959-dismissal-ux-improvement.yml b/changelogs/unreleased/32959-dismissal-ux-improvement.yml
new file mode 100644
index 00000000000..20f0af549d3
--- /dev/null
+++ b/changelogs/unreleased/32959-dismissal-ux-improvement.yml
@@ -0,0 +1,5 @@
+---
+title: Improve UX for vulnerability dismissal note
+merge_request: 20768
+author:
+type: fixed
diff --git a/changelogs/unreleased/33318-make-internal-projects-poolable.yml b/changelogs/unreleased/33318-make-internal-projects-poolable.yml
new file mode 100644
index 00000000000..c13794018d2
--- /dev/null
+++ b/changelogs/unreleased/33318-make-internal-projects-poolable.yml
@@ -0,0 +1,5 @@
+---
+title: Make internal projects poolable
+merge_request: 19295
+author: briankabiro
+type: changed
diff --git a/changelogs/unreleased/33482-allow-text-wrapping-on-repository-tags-page.yml b/changelogs/unreleased/33482-allow-text-wrapping-on-repository-tags-page.yml
new file mode 100644
index 00000000000..567e1b7cfda
--- /dev/null
+++ b/changelogs/unreleased/33482-allow-text-wrapping-on-repository-tags-page.yml
@@ -0,0 +1,5 @@
+---
+title: Allow patch notes on repo tags page to word wrap
+merge_request: 20135
+author:
+type: fixed
diff --git a/changelogs/unreleased/33718-add-new-dep-scanning-flag.yml b/changelogs/unreleased/33718-add-new-dep-scanning-flag.yml
new file mode 100644
index 00000000000..e8ff06461e2
--- /dev/null
+++ b/changelogs/unreleased/33718-add-new-dep-scanning-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Add dependency scanning flag for skipping automatic bundler audit update
+merge_request: 20743
+author:
+type: added
diff --git a/changelogs/unreleased/34377-design-view-download-single-issue-design-image.yml b/changelogs/unreleased/34377-design-view-download-single-issue-design-image.yml
new file mode 100644
index 00000000000..67e37e39dc0
--- /dev/null
+++ b/changelogs/unreleased/34377-design-view-download-single-issue-design-image.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve Design view: Download single issue design image'
+merge_request: 20703
+author:
+type: added
diff --git a/changelogs/unreleased/34685-Pages-template-jekyll-outdated-and-not-working-as-expected.yml b/changelogs/unreleased/34685-Pages-template-jekyll-outdated-and-not-working-as-expected.yml
new file mode 100644
index 00000000000..0ed03916829
--- /dev/null
+++ b/changelogs/unreleased/34685-Pages-template-jekyll-outdated-and-not-working-as-expected.yml
@@ -0,0 +1,5 @@
+---
+title: Updated jekyll project_template
+merge_request: 20090
+author: Marc Schwede
+type: other
diff --git a/changelogs/unreleased/35458-expose-manual-actions-retry.yml b/changelogs/unreleased/35458-expose-manual-actions-retry.yml
new file mode 100644
index 00000000000..167dca796c4
--- /dev/null
+++ b/changelogs/unreleased/35458-expose-manual-actions-retry.yml
@@ -0,0 +1,5 @@
+---
+title: Exposed deployment build manual actions for merge request page
+merge_request: 20615
+author:
+type: changed
diff --git a/changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml b/changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml
new file mode 100644
index 00000000000..2d2450ebd68
--- /dev/null
+++ b/changelogs/unreleased/35570-update-deploy-instances-color-scheme.yml
@@ -0,0 +1,5 @@
+---
+title: Update deploy instances color scheme
+merge_request: 20890
+author:
+type: changed
diff --git a/changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml b/changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml
new file mode 100644
index 00000000000..213517c0ec1
--- /dev/null
+++ b/changelogs/unreleased/36412-Sentry-error-page-stuck-loading.yml
@@ -0,0 +1,5 @@
+---
+title: Handle empty stacktrace and entries with no code
+merge_request: 20458
+author:
+type: fixed
diff --git a/changelogs/unreleased/36955-snowplow-custom-events-for-monitor-apm-add-metric-button-fe.yml b/changelogs/unreleased/36955-snowplow-custom-events-for-monitor-apm-add-metric-button-fe.yml
new file mode 100644
index 00000000000..738f3007214
--- /dev/null
+++ b/changelogs/unreleased/36955-snowplow-custom-events-for-monitor-apm-add-metric-button-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Track adding metric via monitoring dashboard
+merge_request: 20818
+author:
+type: added
diff --git a/changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml b/changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml
new file mode 100644
index 00000000000..b6e3f1af414
--- /dev/null
+++ b/changelogs/unreleased/37006-fix-open-details-page-in-new-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Fix opening Sentry error details in new tab
+merge_request: 20611
+author:
+type: fixed
diff --git a/changelogs/unreleased/37057-record-import-failures.yml b/changelogs/unreleased/37057-record-import-failures.yml
new file mode 100644
index 00000000000..2358220ef29
--- /dev/null
+++ b/changelogs/unreleased/37057-record-import-failures.yml
@@ -0,0 +1,5 @@
+---
+title: Collect project import failures instead of failing fast
+merge_request: 20727
+author:
+type: other
diff --git a/changelogs/unreleased/37313-scroll-to-bottom.yml b/changelogs/unreleased/37313-scroll-to-bottom.yml
new file mode 100644
index 00000000000..d7251bd8100
--- /dev/null
+++ b/changelogs/unreleased/37313-scroll-to-bottom.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes job log not scrolling to the bottom
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37680-tree-control-buttons-misbehave-on-small-viewports.yml b/changelogs/unreleased/37680-tree-control-buttons-misbehave-on-small-viewports.yml
new file mode 100644
index 00000000000..58e63e127f9
--- /dev/null
+++ b/changelogs/unreleased/37680-tree-control-buttons-misbehave-on-small-viewports.yml
@@ -0,0 +1,5 @@
+---
+title: Remove whitespaces between tree-controls elements
+merge_request: 20952
+author:
+type: other
diff --git a/changelogs/unreleased/7603-make-it-easy-to-generate-and-share-the-maven-xml-for-a-library.yml b/changelogs/unreleased/7603-make-it-easy-to-generate-and-share-the-maven-xml-for-a-library.yml
new file mode 100644
index 00000000000..ed02816f7d1
--- /dev/null
+++ b/changelogs/unreleased/7603-make-it-easy-to-generate-and-share-the-maven-xml-for-a-library.yml
@@ -0,0 +1,5 @@
+---
+title: Add Maven installation commands to package detail page for Maven packages
+merge_request: 20300
+author:
+type: added
diff --git a/changelogs/unreleased/Replace-BoardService_in_assignee_select_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_assignee_select_spec-js.yml
new file mode 100644
index 00000000000..8bc76beeb8b
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_assignee_select_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20880
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-BoardService_in_board_card_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_board_card_spec-js.yml
new file mode 100644
index 00000000000..ddce6c69343
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_board_card_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20881
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-BoardService_in_board_list_common_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_board_list_common_spec-js.yml
new file mode 100644
index 00000000000..faf5c2a0ef0
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_board_list_common_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20872
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-BoardService_in_board_new_issue_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_board_new_issue_spec-js.yml
new file mode 100644
index 00000000000..22830aa8af6
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_board_new_issue_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20874
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml
new file mode 100644
index 00000000000..549127c365b
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_board_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20875
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml b/changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml
new file mode 100644
index 00000000000..330bf2493d6
--- /dev/null
+++ b/changelogs/unreleased/Replace-BoardService_in_issue_spec-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes references of BoardService
+merge_request: 20876
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Updated-hexo-project_template.yml b/changelogs/unreleased/Updated-hexo-project_template.yml
new file mode 100644
index 00000000000..4777220ca55
--- /dev/null
+++ b/changelogs/unreleased/Updated-hexo-project_template.yml
@@ -0,0 +1,5 @@
+---
+title: Updated hexo project_template
+merge_request: 20105
+author: Marc Schwede
+type: other
diff --git a/changelogs/unreleased/Updated-hugo-project_template.yml b/changelogs/unreleased/Updated-hugo-project_template.yml
new file mode 100644
index 00000000000..58488dfcb4c
--- /dev/null
+++ b/changelogs/unreleased/Updated-hugo-project_template.yml
@@ -0,0 +1,5 @@
+---
+title: Updated hugo project_template
+merge_request: 20109
+author: Marc Schwede
+type: other
diff --git a/changelogs/unreleased/add_body_data_elements_for_page_type_id_project_id_and_namespace_id.yml b/changelogs/unreleased/add_body_data_elements_for_page_type_id_project_id_and_namespace_id.yml
new file mode 100644
index 00000000000..587741a8ea5
--- /dev/null
+++ b/changelogs/unreleased/add_body_data_elements_for_page_type_id_project_id_and_namespace_id.yml
@@ -0,0 +1,5 @@
+---
+title: Add body data elements for pageview context
+merge_request: 18450
+author:
+type: added
diff --git a/changelogs/unreleased/bvl-cache-repository-ancestor.yml b/changelogs/unreleased/bvl-cache-repository-ancestor.yml
new file mode 100644
index 00000000000..6c50c2319fc
--- /dev/null
+++ b/changelogs/unreleased/bvl-cache-repository-ancestor.yml
@@ -0,0 +1,5 @@
+---
+title: Cache the ancestor? Gitaly call to speed up polling for the merge request widget
+merge_request: 20958
+author:
+type: performance
diff --git a/changelogs/unreleased/chore-admin-mode-rack-attack-default-paths-migration.yml b/changelogs/unreleased/chore-admin-mode-rack-attack-default-paths-migration.yml
new file mode 100644
index 00000000000..20486c3bfa4
--- /dev/null
+++ b/changelogs/unreleased/chore-admin-mode-rack-attack-default-paths-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Add admin mode controller path to Rack::Attack defaults
+merge_request: 20735
+author: Diego Louzán
+type: changed
diff --git a/changelogs/unreleased/ci_template_cluster_applications.yml b/changelogs/unreleased/ci_template_cluster_applications.yml
new file mode 100644
index 00000000000..aab3544ef1c
--- /dev/null
+++ b/changelogs/unreleased/ci_template_cluster_applications.yml
@@ -0,0 +1,5 @@
+---
+title: CI template for installing cluster applications
+merge_request: 20822
+author:
+type: added
diff --git a/changelogs/unreleased/cleanup-monitoring-dashboard-unused-methods.yml b/changelogs/unreleased/cleanup-monitoring-dashboard-unused-methods.yml
new file mode 100644
index 00000000000..f4dac5e2e0f
--- /dev/null
+++ b/changelogs/unreleased/cleanup-monitoring-dashboard-unused-methods.yml
@@ -0,0 +1,5 @@
+---
+title: Removed unused methods in monitoring dashboard
+merge_request: 20819
+author:
+type: other
diff --git a/changelogs/unreleased/dz-move-operations-routes.yml b/changelogs/unreleased/dz-move-operations-routes.yml
new file mode 100644
index 00000000000..76e7e81fdea
--- /dev/null
+++ b/changelogs/unreleased/dz-move-operations-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Move operations project routes under - scope
+merge_request: 20456
+author:
+type: deprecated
diff --git a/changelogs/unreleased/feat-circuit-project-service.yml b/changelogs/unreleased/feat-circuit-project-service.yml
new file mode 100644
index 00000000000..ec073ede9ee
--- /dev/null
+++ b/changelogs/unreleased/feat-circuit-project-service.yml
@@ -0,0 +1,5 @@
+---
+title: Add Unify Circuit project integration service
+merge_request: 19849
+author: Fabio Huser
+type: added
diff --git a/changelogs/unreleased/filter-for-project-and-group-audit-events.yml b/changelogs/unreleased/filter-for-project-and-group-audit-events.yml
new file mode 100644
index 00000000000..4fe4ea0beb5
--- /dev/null
+++ b/changelogs/unreleased/filter-for-project-and-group-audit-events.yml
@@ -0,0 +1,5 @@
+---
+title: Add created_before/after filter to group/project audit events
+merge_request: 20641
+author:
+type: added
diff --git a/changelogs/unreleased/fix-fork-link-display-bug.yml b/changelogs/unreleased/fix-fork-link-display-bug.yml
new file mode 100644
index 00000000000..2a8f2b1a38d
--- /dev/null
+++ b/changelogs/unreleased/fix-fork-link-display-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a display bug in the fork removal description message
+merge_request: 20843
+author:
+type: fixed
diff --git a/changelogs/unreleased/fixes-35624.yml b/changelogs/unreleased/fixes-35624.yml
new file mode 100644
index 00000000000..855a4ad8e93
--- /dev/null
+++ b/changelogs/unreleased/fixes-35624.yml
@@ -0,0 +1,5 @@
+---
+title: Resets aria-describedby on mouseleave
+merge_request: 20092
+author: carolcarvalhosa
+type: fixed
diff --git a/changelogs/unreleased/fj-31133-snippet-content-size-limit.yml b/changelogs/unreleased/fj-31133-snippet-content-size-limit.yml
new file mode 100644
index 00000000000..acc8e4efb04
--- /dev/null
+++ b/changelogs/unreleased/fj-31133-snippet-content-size-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Add limit for snippet content size
+merge_request: 20346
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-37436-fix-create-personal-snippet-ability.yml b/changelogs/unreleased/fj-37436-fix-create-personal-snippet-ability.yml
new file mode 100644
index 00000000000..7b1d0548f11
--- /dev/null
+++ b/changelogs/unreleased/fj-37436-fix-create-personal-snippet-ability.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure to check create_personal_snippet ability
+merge_request: 20838
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-add-filters-to-snippets-finder.yml b/changelogs/unreleased/fj-add-filters-to-snippets-finder.yml
new file mode 100644
index 00000000000..df8c70152d1
--- /dev/null
+++ b/changelogs/unreleased/fj-add-filters-to-snippets-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Add more filters to SnippetsFinder
+merge_request: 20767
+author:
+type: changed
diff --git a/changelogs/unreleased/gitaly-2108-repos-gc-after-move.yml b/changelogs/unreleased/gitaly-2108-repos-gc-after-move.yml
new file mode 100644
index 00000000000..68092b9c348
--- /dev/null
+++ b/changelogs/unreleased/gitaly-2108-repos-gc-after-move.yml
@@ -0,0 +1,5 @@
+---
+title: Run housekeeping after moving a repository between shards
+merge_request: 20863
+author:
+type: performance
diff --git a/changelogs/unreleased/helm_values_default.yml b/changelogs/unreleased/helm_values_default.yml
new file mode 100644
index 00000000000..c731ef85e26
--- /dev/null
+++ b/changelogs/unreleased/helm_values_default.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade auto-deploy-image for helm default values file
+merge_request: 20588
+author:
+type: changed
diff --git a/changelogs/unreleased/hly-search-by-project-full-path.yml b/changelogs/unreleased/hly-search-by-project-full-path.yml
new file mode 100644
index 00000000000..a8db5f56a33
--- /dev/null
+++ b/changelogs/unreleased/hly-search-by-project-full-path.yml
@@ -0,0 +1,5 @@
+---
+title: Allow searching of projects by full path
+merge_request: 20659
+author:
+type: added
diff --git a/changelogs/unreleased/id-optimize-query-for-ci-pipelines.yml b/changelogs/unreleased/id-optimize-query-for-ci-pipelines.yml
new file mode 100644
index 00000000000..b20d5a5c3ed
--- /dev/null
+++ b/changelogs/unreleased/id-optimize-query-for-ci-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize query for CI pipelines of merge request
+merge_request: 19653
+author:
+type: performance
diff --git a/changelogs/unreleased/large_imports_rake_task.yml b/changelogs/unreleased/large_imports_rake_task.yml
new file mode 100644
index 00000000000..cf855da8f92
--- /dev/null
+++ b/changelogs/unreleased/large_imports_rake_task.yml
@@ -0,0 +1,5 @@
+---
+title: Import large gitlab_project exports via rake task
+merge_request: 20724
+author:
+type: added
diff --git a/changelogs/unreleased/nicolasdular-add-target-path-to-broadcast-message.yml b/changelogs/unreleased/nicolasdular-add-target-path-to-broadcast-message.yml
new file mode 100644
index 00000000000..9645a155037
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-add-target-path-to-broadcast-message.yml
@@ -0,0 +1,5 @@
+---
+title: Add path based targeting to broadcast messages
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/osw-delete-fork-relation-upon-visibility-change.yml b/changelogs/unreleased/osw-delete-fork-relation-upon-visibility-change.yml
new file mode 100644
index 00000000000..64a7f6c7427
--- /dev/null
+++ b/changelogs/unreleased/osw-delete-fork-relation-upon-visibility-change.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust fork network relations upon project visibility change
+merge_request: 20466
+author:
+type: added
diff --git a/changelogs/unreleased/ph-33813-moveMergeRequestDescription.yml b/changelogs/unreleased/ph-33813-moveMergeRequestDescription.yml
new file mode 100644
index 00000000000..0fa53a94946
--- /dev/null
+++ b/changelogs/unreleased/ph-33813-moveMergeRequestDescription.yml
@@ -0,0 +1,5 @@
+---
+title: Move merge request description into discussions tab
+merge_request: 18940
+author:
+type: changed
diff --git a/changelogs/unreleased/qa-add-email-delivery-tests.yml b/changelogs/unreleased/qa-add-email-delivery-tests.yml
new file mode 100644
index 00000000000..85795541cea
--- /dev/null
+++ b/changelogs/unreleased/qa-add-email-delivery-tests.yml
@@ -0,0 +1,5 @@
+---
+title: Add e2e qa test for email delivery
+merge_request: 20675
+author: Diego Louzán
+type: other
diff --git a/changelogs/unreleased/sh-fix-issue-34366.yml b/changelogs/unreleased/sh-fix-issue-34366.yml
new file mode 100644
index 00000000000..efc19f8392e
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-34366.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error in updating runner session
+merge_request: 20902
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-remove-feature-flag-diverging-commits.yml b/changelogs/unreleased/sh-remove-feature-flag-diverging-commits.yml
new file mode 100644
index 00000000000..693a5e42246
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-feature-flag-diverging-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Remove feature flag for limiting diverging commit counts
+merge_request: 20999
+author:
+type: other
diff --git a/changelogs/unreleased/sy-grafana-fix-uid.yml b/changelogs/unreleased/sy-grafana-fix-uid.yml
new file mode 100644
index 00000000000..778706d540b
--- /dev/null
+++ b/changelogs/unreleased/sy-grafana-fix-uid.yml
@@ -0,0 +1,5 @@
+---
+title: Accept user-defined dashboard uids in Grafana embeds
+merge_request: 20486
+author:
+type: fixed
diff --git a/changelogs/unreleased/tz-fe-timings-performancebar.yml b/changelogs/unreleased/tz-fe-timings-performancebar.yml
new file mode 100644
index 00000000000..6b6f2cc7ea0
--- /dev/null
+++ b/changelogs/unreleased/tz-fe-timings-performancebar.yml
@@ -0,0 +1,5 @@
+---
+title: Added Total/Frontend metrics to the performance bar
+merge_request: 20725
+author:
+type: added
diff --git a/changelogs/unreleased/update-managed-namespace-prefix-copy.yml b/changelogs/unreleased/update-managed-namespace-prefix-copy.yml
new file mode 100644
index 00000000000..e19e8078f64
--- /dev/null
+++ b/changelogs/unreleased/update-managed-namespace-prefix-copy.yml
@@ -0,0 +1,5 @@
+---
+title: Update copy on managed namespace prefixes
+merge_request: 20935
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-size-after-commit.yml b/changelogs/unreleased/update-size-after-commit.yml
new file mode 100644
index 00000000000..4e5f5cd6782
--- /dev/null
+++ b/changelogs/unreleased/update-size-after-commit.yml
@@ -0,0 +1,5 @@
+---
+title: UpdateProjectStatistics updates after commit
+merge_request: 20852
+author:
+type: performance
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 35e67e04bfb..b4a1e0da41a 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -118,8 +118,5 @@ end
Sidekiq.configure_client do |config|
config.redis = queues_config_hash
- config.client_middleware do |chain|
- chain.add Gitlab::SidekiqMiddleware::CorrelationInjector
- chain.add Gitlab::SidekiqStatus::ClientMiddleware
- end
+ config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5b1a0d2bb7a..848846b5f5b 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
+# rubocop: disable Cop/PutProjectRoutesUnderScope
resources :projects, only: [:index, :new, :create]
draw :git_http
get '/projects/:id' => 'projects#resolve'
+# rubocop: enable Cop/PutProjectRoutesUnderScope
constraints(::Constraints::ProjectUrlConstrainer.new) do
# If the route has a wildcard segment, the segment has a regex constraint,
@@ -207,9 +209,62 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :production
end
end
+
+ concerns :clusterable
+
+ namespace :serverless do
+ scope :functions do
+ get '/:environment_id/:id', to: 'functions#show'
+ get '/:environment_id/:id/metrics', to: 'functions#metrics', as: :metrics
+ end
+
+ resources :functions, only: [:index]
+ end
+
+ resources :environments, except: [:destroy] do
+ member do
+ post :stop
+ get :terminal
+ get :metrics
+ get :additional_metrics
+ get :metrics_dashboard
+ get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
+
+ get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy', as: :prometheus_api
+ end
+
+ collection do
+ get :metrics, action: :metrics_redirect
+ get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
+ get :search
+ end
+
+ resources :deployments, only: [:index] do
+ member do
+ get :metrics
+ get :additional_metrics
+ end
+ end
+ end
+
+ resources :error_tracking, only: [:index], controller: :error_tracking do
+ collection do
+ get ':issue_id/details',
+ to: 'error_tracking#details',
+ as: 'details'
+ get ':issue_id/stack_trace',
+ to: 'error_tracking#stack_trace',
+ as: 'stack_trace'
+ post :list_projects
+ end
+ end
end
# End of the /-/ scope.
+ # All new routes should go under /-/ scope.
+ # Look for scope '-' at the top of the file.
+ # rubocop: disable Cop/PutProjectRoutesUnderScope
+
#
# Templates
#
@@ -367,43 +422,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- concerns :clusterable
-
- resources :environments, except: [:destroy] do
- member do
- post :stop
- get :terminal
- get :metrics
- get :additional_metrics
- get :metrics_dashboard
- get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
-
- get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy', as: :prometheus_api
- end
-
- collection do
- get :metrics, action: :metrics_redirect
- get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
- get :search
- end
-
- resources :deployments, only: [:index] do
- member do
- get :metrics
- get :additional_metrics
- end
- end
- end
-
- namespace :serverless do
- scope :functions do
- get '/:environment_id/:id', to: 'functions#show'
- get '/:environment_id/:id/metrics', to: 'functions#metrics', as: :metrics
- end
-
- resources :functions, only: [:index]
- end
-
draw :legacy_builds
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
@@ -501,18 +519,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :error_tracking, only: [:index], controller: :error_tracking do
- collection do
- get ':issue_id/details',
- to: 'error_tracking#details',
- as: 'details'
- get ':issue_id/stack_trace',
- to: 'error_tracking#stack_trace',
- as: 'stack_trace'
- post :list_projects
- end
- end
-
scope :usage_ping, controller: :usage_ping do
post :web_ide_clientside_preview
end
@@ -522,6 +528,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
draw :wiki
draw :repository
+ # All new routes should go under /-/ scope.
+ # Look for scope '-' at the top of the file.
+ # rubocop: enable Cop/PutProjectRoutesUnderScope
+
# Legacy routes.
# Introduced in 12.0.
# Should be removed with https://gitlab.com/gitlab-org/gitlab/issues/28848.
@@ -530,9 +540,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
:project_members, :deploy_keys, :deploy_tokens,
:labels, :milestones, :services, :boards, :releases,
:forks, :group_links, :import, :avatar, :mirror,
- :cycle_analytics, :mattermost, :variables, :triggers)
+ :cycle_analytics, :mattermost, :variables, :triggers,
+ :environments, :protected_environments, :error_tracking,
+ :serverless, :clusters, :audit_events)
end
+ # rubocop: disable Cop/PutProjectRoutesUnderScope
resources(:projects,
path: '/',
constraints: { id: Gitlab::PathRegex.project_route_regex },
@@ -554,5 +567,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
put :new_issuable_address
end
end
+ # rubocop: enable Cop/PutProjectRoutesUnderScope
end
end
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index af95f9d6f76..c5d02e1d320 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -29,6 +29,10 @@ def ce_port_changelog?(changelog_path)
helper.ee? && !ee_changelog?(changelog_path)
end
+def docs_only_change?
+ helper.changes_by_category.keys == [:docs]
+end
+
def check_changelog(path)
yaml = YAML.safe_load(File.read(path))
@@ -55,7 +59,7 @@ def sanitized_mr_title
gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`')
end
-changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
+changelog_needed = !docs_only_change? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
if git.modified_files.include?("CHANGELOG.md")
diff --git a/db/fixtures/development/21_conversational_development_index_metrics.rb b/db/fixtures/development/21_dev_ops_score_metrics.rb
index 4cd0a82ed1a..afea7fb4bd0 100644
--- a/db/fixtures/development/21_conversational_development_index_metrics.rb
+++ b/db/fixtures/development/21_dev_ops_score_metrics.rb
@@ -1,5 +1,5 @@
Gitlab::Seeder.quiet do
- conversational_development_index_metric = ConversationalDevelopmentIndex::Metric.new(
+ dev_ops_score_metric = DevOpsScore::Metric.new(
leader_issues: 10.2,
instance_issues: 3.2,
@@ -31,10 +31,10 @@ Gitlab::Seeder.quiet do
instance_service_desk_issues: 15.1
)
- if conversational_development_index_metric.save
+ if dev_ops_score_metric.save
print '.'
else
- puts conversational_development_index_metric.errors.full_messages
+ puts dev_ops_score_metric.errors.full_messages
print 'F'
end
end
diff --git a/db/migrate/20180215181245_users_name_lower_index.rb b/db/migrate/20180215181245_users_name_lower_index.rb
index fa1a115a78a..46f02885c3f 100644
--- a/db/migrate/20180215181245_users_name_lower_index.rb
+++ b/db/migrate/20180215181245_users_name_lower_index.rb
@@ -11,15 +11,11 @@ class UsersNameLowerIndex < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- return unless Gitlab::Database.postgresql?
-
# On GitLab.com this produces an index with a size of roughly 60 MB.
execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON users (LOWER(name))"
end
def down
- return unless Gitlab::Database.postgresql?
-
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
end
end
diff --git a/db/migrate/20180309121820_reschedule_commits_count_for_merge_request_diff.rb b/db/migrate/20180309121820_reschedule_commits_count_for_merge_request_diff.rb
index ecb06dd4312..3d85a19b82f 100644
--- a/db/migrate/20180309121820_reschedule_commits_count_for_merge_request_diff.rb
+++ b/db/migrate/20180309121820_reschedule_commits_count_for_merge_request_diff.rb
@@ -18,7 +18,7 @@ class RescheduleCommitsCountForMergeRequestDiff < ActiveRecord::Migration[4.2]
def up
say 'Populating the MergeRequestDiff `commits_count` (reschedule)'
- execute("SET statement_timeout TO '60s'") if Gitlab::Database.postgresql?
+ execute("SET statement_timeout TO '60s'")
MergeRequestDiff.where(commits_count: nil).each_batch(of: BATCH_SIZE) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb
index fa74330d5d9..e789837193f 100644
--- a/db/migrate/20180504195842_project_name_lower_index.rb
+++ b/db/migrate/20180504195842_project_name_lower_index.rb
@@ -11,16 +11,12 @@ class ProjectNameLowerIndex < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- return unless Gitlab::Database.postgresql?
-
disable_statement_timeout do
execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))"
end
end
def down
- return unless Gitlab::Database.postgresql?
-
disable_statement_timeout do
execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
end
diff --git a/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
index 36f4770ff32..859e341d04b 100644
--- a/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
+++ b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
@@ -5,34 +5,20 @@ class AddNotNullConstraintsToProjectAuthorizations < ActiveRecord::Migration[4.2
DOWNTIME = false
def up
- if Gitlab::Database.postgresql?
- # One-pass version for PostgreSQL
- execute <<~SQL
+ execute <<~SQL
ALTER TABLE project_authorizations
ALTER COLUMN user_id SET NOT NULL,
ALTER COLUMN project_id SET NOT NULL,
ALTER COLUMN access_level SET NOT NULL
- SQL
- else
- change_column_null :project_authorizations, :user_id, false
- change_column_null :project_authorizations, :project_id, false
- change_column_null :project_authorizations, :access_level, false
- end
+ SQL
end
def down
- if Gitlab::Database.postgresql?
- # One-pass version for PostgreSQL
- execute <<~SQL
+ execute <<~SQL
ALTER TABLE project_authorizations
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN project_id DROP NOT NULL,
ALTER COLUMN access_level DROP NOT NULL
- SQL
- else
- change_column_null :project_authorizations, :user_id, true
- change_column_null :project_authorizations, :project_id, true
- change_column_null :project_authorizations, :access_level, true
- end
+ SQL
end
end
diff --git a/db/migrate/20190402150158_backport_enterprise_schema.rb b/db/migrate/20190402150158_backport_enterprise_schema.rb
index 3f13b68c2f3..d1e911a04e6 100644
--- a/db/migrate/20190402150158_backport_enterprise_schema.rb
+++ b/db/migrate/20190402150158_backport_enterprise_schema.rb
@@ -464,15 +464,12 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0]
end
def update_environments
- return unless Gitlab::Database.postgresql?
return if index_exists?(:environments, :name, name: 'index_environments_on_name_varchar_pattern_ops')
execute('CREATE INDEX CONCURRENTLY index_environments_on_name_varchar_pattern_ops ON environments (name varchar_pattern_ops);')
end
def revert_environments
- return unless Gitlab::Database.postgresql?
-
remove_concurrent_index_by_name(
:environments,
'index_environments_on_name_varchar_pattern_ops'
diff --git a/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb b/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb
index a1d3ffdb8c8..850112b4f0b 100644
--- a/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb
+++ b/db/migrate/20191014132931_remove_index_on_snippets_project_id.rb
@@ -8,10 +8,13 @@ class RemoveIndexOnSnippetsProjectId < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- remove_concurrent_index :snippets, [:project_id]
+ remove_concurrent_index_by_name :snippets, 'index_snippets_on_project_id'
+
+ # This is an extra index that is not present in db/schema.rb but known to exist on some installs
+ remove_concurrent_index_by_name :snippets, :snippets_project_id_idx if index_exists_by_name? :snippets, :snippets_project_id_idx
end
def down
- add_concurrent_index :snippets, [:project_id]
+ add_concurrent_index :snippets, [:project_id], name: 'index_snippets_on_project_id'
end
end
diff --git a/db/migrate/20191118173522_add_snippet_size_limit_to_application_settings.rb b/db/migrate/20191118173522_add_snippet_size_limit_to_application_settings.rb
new file mode 100644
index 00000000000..b6b30febbd6
--- /dev/null
+++ b/db/migrate/20191118173522_add_snippet_size_limit_to_application_settings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddSnippetSizeLimitToApplicationSettings < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ add_column :application_settings, :snippet_size_limit, :bigint, default: 50.megabytes, null: false
+ end
+
+ def down
+ remove_column :application_settings, :snippet_size_limit
+ end
+end
diff --git a/db/migrate/20191125114345_add_admin_mode_protected_path.rb b/db/migrate/20191125114345_add_admin_mode_protected_path.rb
new file mode 100644
index 00000000000..7e9b0d5a285
--- /dev/null
+++ b/db/migrate/20191125114345_add_admin_mode_protected_path.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class AddAdminModeProtectedPath < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ ADMIN_MODE_ENDPOINT = '/admin/session'
+
+ OLD_DEFAULT_PROTECTED_PATHS = [
+ '/users/password',
+ '/users/sign_in',
+ '/api/v3/session.json',
+ '/api/v3/session',
+ '/api/v4/session.json',
+ '/api/v4/session',
+ '/users',
+ '/users/confirmation',
+ '/unsubscribes/',
+ '/import/github/personal_access_token'
+ ]
+
+ NEW_DEFAULT_PROTECTED_PATHS = OLD_DEFAULT_PROTECTED_PATHS.dup << ADMIN_MODE_ENDPOINT
+
+ class ApplicationSetting < ActiveRecord::Base
+ self.table_name = 'application_settings'
+ end
+
+ def up
+ change_column_default :application_settings, :protected_paths, NEW_DEFAULT_PROTECTED_PATHS
+
+ # schema allows nulls for protected_paths
+ ApplicationSetting.where.not(protected_paths: nil).each do |application_setting|
+ unless application_setting.protected_paths.include?(ADMIN_MODE_ENDPOINT)
+ updated_protected_paths = application_setting.protected_paths << ADMIN_MODE_ENDPOINT
+
+ application_setting.update(protected_paths: updated_protected_paths)
+ end
+ end
+ end
+
+ def down
+ change_column_default :application_settings, :protected_paths, OLD_DEFAULT_PROTECTED_PATHS
+
+ # schema allows nulls for protected_paths
+ ApplicationSetting.where.not(protected_paths: nil).each do |application_setting|
+ if application_setting.protected_paths.include?(ADMIN_MODE_ENDPOINT)
+ updated_protected_paths = application_setting.protected_paths - [ADMIN_MODE_ENDPOINT]
+
+ application_setting.update(protected_paths: updated_protected_paths)
+ end
+ end
+ end
+end
diff --git a/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb b/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb
new file mode 100644
index 00000000000..65aa758e502
--- /dev/null
+++ b/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddTargetPathToBroadcastMessage < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :broadcast_messages, :target_path, :string, limit: 255
+ end
+end
diff --git a/db/migrate/20191125140458_create_import_failures.rb b/db/migrate/20191125140458_create_import_failures.rb
new file mode 100644
index 00000000000..43e8efe90a4
--- /dev/null
+++ b/db/migrate/20191125140458_create_import_failures.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateImportFailures < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :import_failures do |t|
+ t.integer :relation_index
+ t.references :project, null: false, index: true
+ t.datetime_with_timezone :created_at, null: false
+ t.string :relation_key, limit: 64
+ t.string :exception_class, limit: 128
+ t.string :correlation_id_value, limit: 128, index: true
+ t.string :exception_message, limit: 255
+ end
+ end
+end
diff --git a/db/optional_migrations/composite_primary_keys.rb b/db/optional_migrations/composite_primary_keys.rb
index e0bb0312a35..1fcb9664ff6 100644
--- a/db/optional_migrations/composite_primary_keys.rb
+++ b/db/optional_migrations/composite_primary_keys.rb
@@ -27,8 +27,6 @@ class CompositePrimaryKeysMigration < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- return unless Gitlab::Database.postgresql?
-
disable_statement_timeout do
TABLES.each do |index|
add_primary_key(index)
@@ -37,8 +35,6 @@ class CompositePrimaryKeysMigration < ActiveRecord::Migration[4.2]
end
def down
- return unless Gitlab::Database.postgresql?
-
disable_statement_timeout do
TABLES.each do |index|
remove_primary_key(index)
diff --git a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb
index 3b3cb4267d4..e363642b2ac 100644
--- a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb
+++ b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb
@@ -14,11 +14,9 @@ class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration[4.2]
# These indexes were created on Postgres only in:
# ReworkRedirectRoutesIndexes:
# https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/16211
- if Gitlab::Database.postgresql?
- disable_statement_timeout do
- execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};"
- execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};"
- end
+ disable_statement_timeout do
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};"
end
remove_column(:redirect_routes, :permanent)
@@ -27,11 +25,9 @@ class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration[4.2]
def down
add_column(:redirect_routes, :permanent, :boolean)
- if Gitlab::Database.postgresql?
- disable_statement_timeout do
- execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);")
- execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;")
- end
+ disable_statement_timeout do
+ execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);")
+ execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;")
end
end
end
diff --git a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb
index d44ec1036c4..f0257e303f7 100644
--- a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb
+++ b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb
@@ -16,8 +16,6 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration[4.2]
# This same index is also added in the `ReworkRedirectRoutesIndexes` so this
# is a no-op in most cases.
def up
- return unless Gitlab::Database.postgresql?
-
disable_statement_timeout do
unless index_exists_by_name?(:redirect_routes, INDEX_NAME)
execute("CREATE UNIQUE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (lower(path) varchar_pattern_ops);")
diff --git a/db/post_migrate/20180706223200_populate_site_statistics.rb b/db/post_migrate/20180706223200_populate_site_statistics.rb
index 0859aa88866..6f887a0c18f 100644
--- a/db/post_migrate/20180706223200_populate_site_statistics.rb
+++ b/db/post_migrate/20180706223200_populate_site_statistics.rb
@@ -7,13 +7,13 @@ class PopulateSiteStatistics < ActiveRecord::Migration[4.2]
def up
transaction do
- execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
+ execute('SET LOCAL statement_timeout TO 0') # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
execute("UPDATE site_statistics SET repositories_count = (SELECT COUNT(*) FROM projects)")
end
transaction do
- execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
+ execute('SET LOCAL statement_timeout TO 0') # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
end
diff --git a/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
index 9bf6aed833d..b272bad7f92 100644
--- a/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
+++ b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
@@ -20,7 +20,7 @@ class MigrateNullWikiAccessLevels < ActiveRecord::Migration[4.2]
# We need to re-count wikis as previous attempt was not considering the NULLs.
transaction do
- execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
+ execute('SET LOCAL statement_timeout TO 0') # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
end
diff --git a/db/post_migrate/20180826111825_recalculate_site_statistics.rb b/db/post_migrate/20180826111825_recalculate_site_statistics.rb
index 7c1fca3884d..938707c9ba4 100644
--- a/db/post_migrate/20180826111825_recalculate_site_statistics.rb
+++ b/db/post_migrate/20180826111825_recalculate_site_statistics.rb
@@ -9,13 +9,13 @@ class RecalculateSiteStatistics < ActiveRecord::Migration[4.2]
def up
transaction do
- execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
+ execute('SET LOCAL statement_timeout TO 0') # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
execute("UPDATE site_statistics SET repositories_count = (SELECT COUNT(*) FROM projects)")
end
transaction do
- execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
+ execute('SET LOCAL statement_timeout TO 0') # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48967
execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
end
diff --git a/db/post_migrate/20191125024005_cleanup_deploy_access_levels_for_removed_groups.rb b/db/post_migrate/20191125024005_cleanup_deploy_access_levels_for_removed_groups.rb
new file mode 100644
index 00000000000..29592612a02
--- /dev/null
+++ b/db/post_migrate/20191125024005_cleanup_deploy_access_levels_for_removed_groups.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class CleanupDeployAccessLevelsForRemovedGroups < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ return unless Gitlab.ee?
+
+ delete = <<~SQL
+ DELETE FROM protected_environment_deploy_access_levels d
+ USING protected_environments p
+ WHERE d.protected_environment_id=p.id
+ AND d.group_id IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM project_group_links WHERE project_id=p.project_id AND group_id=d.group_id)
+ RETURNING *
+ SQL
+
+ # At the time of writing there are 4 such records on GitLab.com,
+ # execution time is expected to be around 15ms.
+ records = execute(delete)
+
+ logger = Gitlab::BackgroundMigration::Logger.build
+ records.to_a.each do |record|
+ logger.info record.as_json.merge(message: "protected_environments_deploy_access_levels was deleted")
+ end
+ end
+
+ def down
+ # There is no pragmatic way to restore
+ # the records deleted in the `#up` method above.
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 57d05abd980..4be04185af5 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_11_24_150431) do
+ActiveRecord::Schema.define(version: 2019_11_25_140458) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -328,7 +328,7 @@ ActiveRecord::Schema.define(version: 2019_11_24_150431) do
t.boolean "throttle_protected_paths_enabled", default: false, null: false
t.integer "throttle_protected_paths_requests_per_period", default: 10, null: false
t.integer "throttle_protected_paths_period_in_seconds", default: 60, null: false
- t.string "protected_paths", limit: 255, default: ["/users/password", "/users/sign_in", "/api/v3/session.json", "/api/v3/session", "/api/v4/session.json", "/api/v4/session", "/users", "/users/confirmation", "/unsubscribes/", "/import/github/personal_access_token"], array: true
+ t.string "protected_paths", limit: 255, default: ["/users/password", "/users/sign_in", "/api/v3/session.json", "/api/v3/session", "/api/v4/session.json", "/api/v4/session", "/users", "/users/confirmation", "/unsubscribes/", "/import/github/personal_access_token", "/admin/session"], array: true
t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", default: 3600
@@ -361,6 +361,7 @@ ActiveRecord::Schema.define(version: 2019_11_24_150431) do
t.string "encrypted_slack_app_secret_iv", limit: 255
t.text "encrypted_slack_app_verification_token"
t.string "encrypted_slack_app_verification_token_iv", limit: 255
+ t.bigint "snippet_size_limit", default: 52428800, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
@@ -572,6 +573,7 @@ ActiveRecord::Schema.define(version: 2019_11_24_150431) do
t.string "font"
t.text "message_html", null: false
t.integer "cached_markdown_version"
+ t.string "target_path", limit: 255
t.index ["starts_at", "ends_at", "id"], name: "index_broadcast_messages_on_starts_at_and_ends_at_and_id"
end
@@ -1948,6 +1950,18 @@ ActiveRecord::Schema.define(version: 2019_11_24_150431) do
t.index ["updated_at"], name: "index_import_export_uploads_on_updated_at"
end
+ create_table "import_failures", force: :cascade do |t|
+ t.integer "relation_index"
+ t.bigint "project_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.string "relation_key", limit: 64
+ t.string "exception_class", limit: 128
+ t.string "correlation_id_value", limit: 128
+ t.string "exception_message", limit: 255
+ t.index ["correlation_id_value"], name: "index_import_failures_on_correlation_id_value"
+ t.index ["project_id"], name: "index_import_failures_on_project_id"
+ end
+
create_table "index_statuses", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.datetime "indexed_at"
diff --git a/doc/administration/auth/ldap-ee.md b/doc/administration/auth/ldap-ee.md
index ba104a4c574..34fd97a24ee 100644
--- a/doc/administration/auth/ldap-ee.md
+++ b/doc/administration/auth/ldap-ee.md
@@ -19,7 +19,7 @@ NOTE: **Note:**
- Group sync: Once an hour, GitLab will update group membership
based on LDAP group members.
-## Multiple LDAP servers **(STARTER ONLY)**
+## Multiple LDAP servers
With GitLab Enterprise Edition Starter, you can configure multiple LDAP servers
that your GitLab instance will connect to.
diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md
index 1088d1446fd..84ddd4278ab 100644
--- a/doc/administration/auth/smartcard.md
+++ b/doc/administration/auth/smartcard.md
@@ -4,12 +4,18 @@ type: reference
# Smartcard authentication **(PREMIUM ONLY)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33669) in GitLab 12.6,
-if a user has a pre-existing username and password, they can still use that to log
-in by default. However, this can be disabled.
-
GitLab supports authentication using smartcards.
+## Existing password authentication
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33669) in GitLab 12.6.
+
+By default, existing users can continue to log in with a username and password when smartcard
+authentication is enabled.
+
+To force existing users to use only smartcard authentication,
+[disable username and password authentication](../../user/admin_area/settings/sign_in_restrictions.md#password-authentication-enabled).
+
## Authentication methods
GitLab supports two authentication methods:
@@ -88,10 +94,7 @@ Certificate:
### Authentication against an LDAP server
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7693) in
-[GitLab Premium](https://about.gitlab.com/pricing/) 11.8 as an experimental
-feature. Smartcard authentication against an LDAP server may change or be
-removed completely in future releases.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7693) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8 as an experimental feature. Smartcard authentication against an LDAP server may change or be removed completely in future releases.
GitLab implements a standard way of certificate matching following
[RFC4523](https://tools.ietf.org/html/rfc4523). It uses the
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 0702e0aa141..437c9db1630 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -1,4 +1,4 @@
-# Custom server-side Git hooks
+# Custom server-side Git hooks **(CORE ONLY)**
NOTE: **Note:**
Custom Git hooks must be configured on the filesystem of the GitLab
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 314f5aad82e..c544c531d9f 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -334,7 +334,10 @@ When you tail the Gitaly logs on your Gitaly server you should see requests
coming in. One sure way to trigger a Gitaly request is to clone a repository
from your GitLab server over HTTP.
-DANGER: **Danger:** If you have [custom server-side Git hooks](../custom_hooks.md#custom-server-side-git-hooks) configured, either per repository or globally, you must move these to the Gitaly node. If you have multiple Gitaly nodes, copy your custom hook(s) to all nodes.
+DANGER: **Danger:**
+If you have [custom server-side Git hooks](../custom_hooks.md) configured,
+either per repository or globally, you must move these to the Gitaly node.
+If you have multiple Gitaly nodes, copy your custom hook(s) to all nodes.
### Disabling the Gitaly service in a cluster environment
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 1c01f3cebd5..b510314abfb 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -181,7 +181,7 @@ git_data_dirs({
})
```
-For more information on Gitaly server configuration, see our [gitaly documentation](index.md#3-gitaly-server-configuration).
+For more information on Gitaly server configuration, see our [Gitaly documentation](index.md#3-gitaly-server-configuration).
#### GitLab
diff --git a/doc/administration/gitaly/reference.md b/doc/administration/gitaly/reference.md
index fe88ef13958..2c5e54743c3 100644
--- a/doc/administration/gitaly/reference.md
+++ b/doc/administration/gitaly/reference.md
@@ -134,7 +134,7 @@ A lot of Gitaly RPCs need to look up Git objects from repositories.
Most of the time we use `git cat-file --batch` processes for that. For
better performance, Gitaly can re-use these `git cat-file` processes
across RPC calls. Previously used processes are kept around in a
-["git cat-file cache"](https://about.gitlab.com/blog/2019/07/08/git-performance-on-nfs/#enter-cat-file-cache).
+["Git cat-file cache"](https://about.gitlab.com/blog/2019/07/08/git-performance-on-nfs/#enter-cat-file-cache).
In order to control how much system resources this uses, we have a maximum number
of cat-file processes that can go into the cache.
@@ -165,11 +165,11 @@ Gitaly restarts its `gitaly-ruby` helpers when their memory exceeds the
| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
-| `dir` | string | yes | Path to where gitaly-ruby is installed (needed to boot the process).|
-| `max_rss` | integer | no | Resident set size limit that triggers a gitaly-ruby restart, in bytes. Default is `200000000` (200MB). |
-| `graceful_restart_timeout` | string | no | Grace period before a gitaly-ruby process is forcibly terminated after exceeding `max_rss`. Default is `10m` (10 minutes).|
-| `restart_delay` | string | no |Time that gitaly-ruby memory must remain high before a restart. Default is `5m` (5 minutes).|
-| `num_workers` | integer | no |Number of gitaly-ruby worker processes. Try increasing this number in case of `ResourceExhausted` errors. Default is `2`, minimum is `2`.|
+| `dir` | string | yes | Path to where `gitaly-ruby` is installed (needed to boot the process).|
+| `max_rss` | integer | no | Resident set size limit that triggers a `gitaly-ruby` restart, in bytes. Default is `200000000` (200MB). |
+| `graceful_restart_timeout` | string | no | Grace period before a `gitaly-ruby` process is forcibly terminated after exceeding `max_rss`. Default is `10m` (10 minutes).|
+| `restart_delay` | string | no |Time that `gitaly-ruby` memory must remain high before a restart. Default is `5m` (5 minutes).|
+| `num_workers` | integer | no |Number of `gitaly-ruby` worker processes. Try increasing this number in case of `ResourceExhausted` errors. Default is `2`, minimum is `2`.|
| `linguist_languages_path` | string | no | Override for dynamic `languages.json` discovery. Defaults to an empty string (use of dynamic discovery).|
Example:
@@ -231,11 +231,11 @@ The following values configure logging in Gitaly under the `[logging]` section.
| `level` | string | no | Log level: `debug`, `info`, `warn`, `error`, `fatal`, or `panic`. Default: `info`. |
| `sentry_dsn` | string | no | Sentry DSN for exception monitoring. |
| `sentry_environment` | string | no | [Sentry Environment](https://docs.sentry.io/enriching-error-data/environments/) for exception monitoring. |
-| `ruby_sentry_dsn` | string | no | Sentry DSN for gitaly-ruby exception monitoring. |
+| `ruby_sentry_dsn` | string | no | Sentry DSN for `gitaly-ruby` exception monitoring. |
While the main Gitaly application logs go to stdout, there are some extra log
files that go to a configured directory, like the GitLab Shell logs.
-Gitlab Shell does not support `panic` or `trace` level logs. `panic` will fall
+GitLab Shell does not support `panic` or `trace` level logs. `panic` will fall
back to `error`, while `trace` will fall back to `debug`. Any other invalid log
levels will default to `info`.
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 81719ba51da..f3a8475d75d 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -84,7 +84,7 @@ you can continue with the next step.
1. [Load Balancer(s)](load_balancer.md)[^2]
1. [Consul](consul.md)
-1. [PostgreSQL](database.md#postgresql-in-a-scaled-environment) with [PGBouncer](https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html)
+1. [PostgreSQL](database.md#postgresql-in-a-scaled-environment) with [PgBouncer](https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html)
1. [Redis](redis.md#redis-in-a-scaled-environment)
1. [Gitaly](gitaly.md) (recommended) and / or [NFS](nfs.md)[^4]
1. [GitLab application nodes](gitlab.md)
@@ -219,13 +219,43 @@ Note that your exact needs may be more, depending on your workload. Your
workload is influenced by factors such as - but not limited to - how active your
users are, how much automation you use, mirroring, and repo/change size.
+### 5,000 User Configuration
+
+- **Supported Users (approximate):** 5,000
+- **Test RPS Rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
+- **Status:** Work-in-progress
+- **Known Issues:** For the latest list of known performance issues head
+[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
+
+NOTE: **Note:** This architecture is a work-in-progress of the work so far. The
+Quality team will be certifying this environment in late 2019 or early 2020. The specifications
+may be adjusted prior to certification based on performance testing.
+
+| Service | Nodes | Configuration | GCP type |
+| ----------------------------|-------|-----------------------|---------------|
+| GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
+| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Gitaly <br> - Gitaly Ruby workers on each node set to 20% of available CPUs | X[^1] . | 8 vCPU, 30GB Memory | n1-standard-8 |
+| Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 |
+| Redis Persistent + Sentinel | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 |
+| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| NFS Server[^4] . | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| S3 Object Storage[^3] . | - | - | - |
+| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| External load balancing node[^2] . | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Internal load balancing node[^2] . | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+
+NOTE: **Note:** Memory values are given directly by GCP machine sizes. On different cloud
+vendors a best effort like for like can be used.
+
### 10,000 User Configuration
- **Supported Users (approximate):** 10,000
- **Test RPS Rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
-- **Known Issues:** While validating the reference architectures, slow API
-endpoints were discovered. For details, see the related issues list in
-[this issue](https://gitlab.com/gitlab-org/quality/performance/issues/125).
+- **Known Issues:** For the latest list of known performance issues head
+[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
| Service | Nodes | Configuration | GCP type |
| ----------------------------|-------|-----------------------|---------------|
@@ -250,9 +280,8 @@ vendors a best effort like for like can be used.
- **Supported Users (approximate):** 25,000
- **Test RPS Rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
-- **Known Issues:** While validating the reference architectures, slow API
-endpoints were discovered. For details, see the related issues list in
-[this issue](https://gitlab.com/gitlab-org/quality/performance/issues/125).
+- **Known Issues:** For the latest list of known performance issues head
+[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
| Service | Nodes | Configuration | GCP type |
| ----------------------------|-------|-----------------------|---------------|
@@ -277,9 +306,8 @@ vendors a best effort like for like can be used.
- **Supported Users (approximate):** 50,000
- **Test RPS Rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
-- **Known Issues:** While validating the reference architectures, slow API
-endpoints were discovered. For details, see the related issues list in
-[this issue](https://gitlab.com/gitlab-org/quality/performance/issues/125).
+- **Known Issues:** For the latest list of known performance issues head
+[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
| Service | Nodes | Configuration | GCP type |
| ----------------------------|-------|-----------------------|---------------|
@@ -300,15 +328,16 @@ endpoints were discovered. For details, see the related issues list in
NOTE: **Note:** Memory values are given directly by GCP machine sizes. On different cloud
vendors a best effort like for like can be used.
-[^1]: Gitaly node requirements are dependent on customer data. We recommend 2
- nodes as an absolute minimum for performance at the 10,000 and 25,000 user
- scale and 4 nodes as an absolute minimum at the 50,000 user scale, but
- additional nodes should be considered in conjunction with a review of
- project counts and sizes.
+[^1]: Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend 2 nodes as an absolute minimum for HA environments
+ and at least 4 nodes should be used when supporting 50,000 or more users.
+ We recommend that each Gitaly node should store no more than 5TB of data.
+ Additional nodes should be considered in conjunction with a review of expected
+ data size and spread based on the recommendations above.
[^2]: Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
as the load balancer. However other reputable load balancers with similar feature sets
- should also work here but be aware these aren't validated.
+ should also work instead but be aware these aren't validated.
[^3]: For data objects such as LFS, Uploads, Artifacts, etc... We recommend a S3 Object Storage
where possible over NFS due to better performance and availability. Several types of objects
diff --git a/doc/administration/index.md b/doc/administration/index.md
index bf21347fb99..40ec9b85455 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -80,7 +80,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance.
- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq MemoryKiller, Unicorn).
- [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components.
-- [Invalidate markdown cache](invalidate_markdown_cache.md): Invalidate any cached markdown.
+- [Invalidate Markdown cache](invalidate_markdown_cache.md): Invalidate any cached Markdown.
#### Updating GitLab
@@ -158,6 +158,10 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota-starter-only): Limit the usage of pipeline minutes for Shared Runners. **(STARTER ONLY)**
- [Enable/disable Auto DevOps](../topics/autodevops/index.md#enablingdisabling-auto-devops): Enable or disable Auto DevOps for your instance.
+## Snippet settings
+
+- [Setting snippet content size limit](snippets/index.md): Set a maximum size limit for snippets' content.
+
## Git configuration options
- [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
diff --git a/doc/administration/invalidate_markdown_cache.md b/doc/administration/invalidate_markdown_cache.md
index ad64cb077c1..ebd8578e410 100644
--- a/doc/administration/invalidate_markdown_cache.md
+++ b/doc/administration/invalidate_markdown_cache.md
@@ -1,6 +1,6 @@
# Invalidate Markdown Cache
-For performance reasons, GitLab caches the HTML version of markdown text
+For performance reasons, GitLab caches the HTML version of Markdown text
(e.g. issue and merge request descriptions, comments). It's possible
that these cached versions become outdated, for example
when the `external_url` configuration option is changed - causing links
diff --git a/doc/administration/monitoring/performance/img/performance_bar.png b/doc/administration/monitoring/performance/img/performance_bar.png
index 73f2ccbe4bb..e876e2f373b 100644
--- a/doc/administration/monitoring/performance/img/performance_bar.png
+++ b/doc/administration/monitoring/performance/img/performance_bar.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/img/performance_bar_frontend.png b/doc/administration/monitoring/performance/img/performance_bar_frontend.png
new file mode 100644
index 00000000000..489f855fe33
--- /dev/null
+++ b/doc/administration/monitoring/performance/img/performance_bar_frontend.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index caddc87d8c1..e65fdfd028d 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -16,6 +16,12 @@ It allows you to see (from left to right):
![Rugged profiling using the Performance Bar](img/performance_bar_rugged_calls.png)
- time taken and number of Redis calls; click through for details of these calls
![Redis profiling using the Performance Bar](img/performance_bar_redis_calls.png)
+- total load timings of the page; click through for details of these calls
+ - BE = Backend - Time that the actual base page took to load
+ - FCP = [First Contentful Paint](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint) - Time until something was visible to the user
+ - DOM = [DomContentLoaded](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp) Event
+ - Number of Requests that the page loaded
+ ![Frontend requests using the Performance Bar](img/performance_bar_frontend.png)
- a link to add a request's details to the performance bar; the request can be
added by its full URL (authenticated as the current user), or by the value of
its `X-Request-Id` header
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 02920293daa..80fa30da357 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -25,8 +25,8 @@ The following metrics are available:
| Metric | Type | Since | Description | Labels |
|:---------------------------------------------------------------|:----------|-----------------------:|:----------------------------------------------------------------------------------------------------|:----------------------------------------------------|
-| `gitlab_banzai_cached_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering markdown into HTML when cached output exists | controller, action |
-| `gitlab_banzai_cacheless_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering markdown into HTML when cached outupt does not exist | controller, action |
+| `gitlab_banzai_cached_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering Markdown into HTML when cached output exists | controller, action |
+| `gitlab_banzai_cacheless_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering Markdown into HTML when cached outupt does not exist | controller, action |
| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | controller, action |
| `gitlab_cache_operation_duration_seconds` | Histogram | 10.2 | Cache access time | |
| `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller/action | controller, action, operation |
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index a62e3ab603d..e735d8dd97e 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -457,36 +457,40 @@ If Registry is enabled in your GitLab instance, but you don't need it for your
project, you can disable it from your project's settings. Read the user guide
on how to achieve that.
-## Disable Container Registry but use GitLab as an auth endpoint
+## Use an external container registry with GitLab as an auth endpoint
**Omnibus GitLab**
-You can use GitLab as an auth endpoint and use a non-bundled Container Registry.
+You can use GitLab as an auth endpoint with an external container registry.
1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
```ruby
gitlab_rails['registry_enabled'] = true
- gitlab_rails['registry_host'] = "registry.gitlab.example.com"
- gitlab_rails['registry_port'] = "5005"
gitlab_rails['registry_api_url'] = "http://localhost:5000"
- gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
```
-1. A certificate keypair is required for GitLab and the Container Registry to
- communicate securely. By default Omnibus GitLab will generate one keypair,
- which is saved to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key`.
- When using a non-bundled Container Registry, you will need to supply a
- custom certificate key. To do that, add the following to
- `/etc/gitlab/gitlab.rb`
+ NOTE: **Note:**
+ `gitlab_rails['registry_enabled'] = true` is needed to enable GitLab's
+ Container Registry features and authentication endpoint. GitLab's bundled
+ Container Registry service will not be started even with this enabled.
+
+1. A certificate-key pair is required for GitLab and the external container
+ registry to communicate securely. You will need to create a certificate-key
+ pair, configuring the external container registry with the public
+ certificate and configuring GitLab with the private key. To do that, add
+ the following to `/etc/gitlab/gitlab.rb`:
```ruby
- gitlab_rails['registry_key_path'] = "/custom/path/to/registry-key.key"
# registry['internal_key'] should contain the contents of the custom key
# file. Line breaks in the key file should be marked using `\n` character
# Example:
registry['internal_key'] = "---BEGIN RSA PRIVATE KEY---\nMIIEpQIBAA\n"
+
+ # Optionally define a custom file for Omnibus GitLab to write the contents
+ # of registry['internal_key'] to.
+ gitlab_rails['registry_key_path'] = "/custom/path/to/registry-key.key"
```
NOTE: **Note:**
@@ -496,7 +500,16 @@ You can use GitLab as an auth endpoint and use a non-bundled Container Registry.
`/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` and will populate
it.
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+1. To change the container registry URL displayed in the GitLab Container
+ Registry pages, set the following configurations:
+
+ ```ruby
+ gitlab_rails['registry_host'] = "registry.gitlab.example.com"
+ gitlab_rails['registry_port'] = "5005"
+ ```
+
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
+ for the changes to take effect.
**Installations from source**
diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md
index 517d6b01438..26c811ca54d 100644
--- a/doc/administration/raketasks/uploads/migrate.md
+++ b/doc/administration/raketasks/uploads/migrate.md
@@ -122,10 +122,10 @@ your data out of Object Storage and back into your local storage.
**Before proceeding, it is important to disable both `direct_upload` and `background_upload` under `uploads` settings in `gitlab.rb`**
CAUTION: **Warning:**
- **Extended downtime is required** so no new files are created in object storage during
- the migration. A configuration setting will be added soon to allow migrating
- from object storage to local files with only a brief moment of downtime for configuration changes.
- See issue [gitlab-org/gitlab#30979](https://gitlab.com/gitlab-org/gitlab/issues/30979)
+**Extended downtime is required** so no new files are created in object storage during
+the migration. A configuration setting will be added soon to allow migrating
+from object storage to local files with only a brief moment of downtime for configuration changes.
+To follow progress, see the [relevant issue](https://gitlab.com/gitlab-org/gitlab/issues/30979).
### All-in-one rake task
diff --git a/doc/administration/snippets/index.md b/doc/administration/snippets/index.md
new file mode 100644
index 00000000000..2e17db7b1f6
--- /dev/null
+++ b/doc/administration/snippets/index.md
@@ -0,0 +1,71 @@
+---
+type: reference, howto
+---
+
+# Snippets settings **(CORE ONLY)**
+
+Adjust the snippets' settings of your GitLab instance.
+
+## Snippets content size limit
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31133) in GitLab 12.6.
+
+You can set a content size max limit in snippets. This limit can prevent
+abuses of the feature. The default content size limit is **52428800 Bytes** (50MB).
+
+### How does it work?
+
+The content size limit will be applied when a snippet is created or
+updated. Nevertheless, in order not to break any existing snippet,
+the limit will only be enforced in stored snippets when the content
+is updated.
+
+### Snippets size limit configuration
+
+This setting is not available through the [Admin Area settings](../../user/admin_area/settings/index.md).
+In order to configure this setting, use either the Rails console
+or the [Application settings API](../../api/settings.md).
+
+NOTE: **IMPORTANT:**
+The value of the limit **must** be in Bytes.
+
+#### Through the Rails console
+
+The steps to configure this setting through the Rails console are:
+
+1. Start the Rails console:
+
+ ```bash
+ # For Omnibus installations
+ sudo gitlab-rails console
+
+ # For installations from source
+ sudo -u git -H bundle exec rails console production
+ ```
+
+1. Update the snippets maximum file size:
+
+ ```ruby
+ ApplicationSetting.first.update!(snippet_size_limit: 50.megabytes)
+ ```
+
+To retrieve the current value, start the Rails console and run:
+
+ ```ruby
+ Gitlab::CurrentSettings.snippet_size_limit
+ ```
+
+#### Through the API
+
+The process to set the snippets size limit through the Application Settings API is
+exactly the same as you would do to [update any other setting](../../api/settings.md#change-application-settings).
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings?snippet_size_limit=52428800
+```
+
+You can also use the API to [retrieve the current value](../../api/settings.md#get-current-application-settings).
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings
+```
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index ca58c4f6836..cb0b24ae026 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -923,7 +923,7 @@ queue = Sidekiq::Queue.new('update_merge_requests')
queue.each { |job| job.delete if job.args[0]==125 and job.args[4]=='ref/heads/my_branch'}
```
-**Note:** Running jobs will not be killed. Stop sidekiq before doing this, to get all matching jobs.
+**Note:** Running jobs will not be killed. Stop Sidekiq before doing this, to get all matching jobs.
### Enable debug logging of Sidekiq
diff --git a/doc/administration/troubleshooting/postgresql.md b/doc/administration/troubleshooting/postgresql.md
index f427cd88ce0..65c6952bf1c 100644
--- a/doc/administration/troubleshooting/postgresql.md
+++ b/doc/administration/troubleshooting/postgresql.md
@@ -46,7 +46,7 @@ This section is for links to information elsewhere in the GitLab documentation.
- Managing Omnibus PostgreSQL versions [from the development docs](https://docs.gitlab.com/omnibus/development/managing-postgresql-versions.html)
- [PostgreSQL scaling and HA](../high_availability/database.md)
- - including [troubleshooting](../high_availability/database.md#troubleshooting) gitlab-ctl repmgr-check-master and pgbouncer errors
+ - including [troubleshooting](../high_availability/database.md#troubleshooting) `gitlab-ctl repmgr-check-master` and PgBouncer errors
- [Developer database documentation](../../development/README.md#database-guides) - some of which is absolutely not for production use. Including:
- understanding EXPLAIN plans
@@ -58,30 +58,30 @@ This section is for links to information elsewhere in the GitLab documentation.
- required extension pg_trgm
- required extension postgres_fdw for Geo
-- Errors like this in the production/sidekiq log; see: [Set default_transaction_isolation into read committed](https://docs.gitlab.com/omnibus/settings/database.html#set-default_transaction_isolation-into-read-committed)
+- Errors like this in the `production/sidekiq` log; see: [Set default_transaction_isolation into read committed](https://docs.gitlab.com/omnibus/settings/database.html#set-default_transaction_isolation-into-read-committed):
-```
-ActiveRecord::StatementInvalid PG::TRSerializationFailure: ERROR: could not serialize access due to concurrent update
-```
+ ```plaintext
+ ActiveRecord::StatementInvalid PG::TRSerializationFailure: ERROR: could not serialize access due to concurrent update
+ ```
-- PostgreSQL HA - [replication slot errors](https://docs.gitlab.com/omnibus/settings/database.html#troubleshooting-upgrades-in-an-ha-cluster)
+- PostgreSQL HA - [replication slot errors](https://docs.gitlab.com/omnibus/settings/database.html#troubleshooting-upgrades-in-an-ha-cluster):
-```
-pg_basebackup: could not create temporary replication slot "pg_basebackup_12345": ERROR: all replication slots are in use
-HINT: Free one or increase max_replication_slots.
-```
+ ```plaintext
+ pg_basebackup: could not create temporary replication slot "pg_basebackup_12345": ERROR: all replication slots are in use
+ HINT: Free one or increase max_replication_slots.
+ ```
- GEO [replication errors](../geo/replication/troubleshooting.md#fixing-replication-errors) including:
-```
-ERROR: replication slots can only be used if max_replication_slots > 0
+ ```plaintext
+ ERROR: replication slots can only be used if max_replication_slots > 0
-FATAL: could not start WAL streaming: ERROR: replication slot “geo_secondary_my_domain_com” does not exist
+ FATAL: could not start WAL streaming: ERROR: replication slot “geo_secondary_my_domain_com” does not exist
-Command exceeded allowed execution time
+ Command exceeded allowed execution time
-PANIC: could not write to file ‘pg_xlog/xlogtemp.123’: No space left on device
-```
+ PANIC: could not write to file ‘pg_xlog/xlogtemp.123’: No space left on device
+ ```
- [Checking GEO configuration](../geo/replication/troubleshooting.md#checking-configuration) including
- reconfiguring hosts/ports
@@ -96,7 +96,7 @@ PANIC: could not write to file ‘pg_xlog/xlogtemp.123’: No space left on devi
References:
- [Issue #1 Deadlocks with GitLab 12.1, PostgreSQL 10.7](https://gitlab.com/gitlab-org/gitlab/issues/30528)
-- [Customer ticket (internal) GitLab 12.1.6](https://gitlab.zendesk.com/agent/tickets/134307) and [google doc (internal)](https://docs.google.com/document/d/19xw2d_D1ChLiU-MO1QzWab-4-QXgsIUcN5e_04WTKy4)
+- [Customer ticket (internal) GitLab 12.1.6](https://gitlab.zendesk.com/agent/tickets/134307) and [Google doc (internal)](https://docs.google.com/document/d/19xw2d_D1ChLiU-MO1QzWab-4-QXgsIUcN5e_04WTKy4)
- [Issue #2 deadlocks can occur if an instance is flooded with pushes](https://gitlab.com/gitlab-org/gitlab/issues/33650). Provided for context about how GitLab code can have this sort of unanticipated effect in unusual situations.
```
@@ -117,7 +117,7 @@ Quoting from from issue [#1](https://gitlab.com/gitlab-org/gitlab/issues/30528):
TIP: **Tip:** In support, our general approach to reconfiguring timeouts (applies also to the HTTP stack as well) is that it's acceptable to do it temporarily as a workaround. If it makes GitLab usable for the customer, then it buys time to understand the problem more completely, implement a hot fix, or make some other change that addresses the root cause. Generally, the timeouts should be put back to reasonable defaults once the root cause is resolved.
-In this case, the guidance we had from development was to drop deadlock_timeout and/or statement_timeout but to leave the third setting at 60s. Setting idle_in_transaction protects the database from sessions potentially hanging for days. There's more discussion in [the issue relating to introducing this timeout on gitlab.com.](https://gitlab.com/gitlab-com/gl-infra/production/issues/1053)
+In this case, the guidance we had from development was to drop deadlock_timeout and/or statement_timeout but to leave the third setting at 60s. Setting idle_in_transaction protects the database from sessions potentially hanging for days. There's more discussion in [the issue relating to introducing this timeout on GitLab.com](https://gitlab.com/gitlab-com/gl-infra/production/issues/1053).
PostgresSQL defaults:
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 6fc6599a47d..916c99d5f89 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -13,6 +13,8 @@ GET /projects/:id/deployments
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `order_by`| string | no | Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref` fields. Default is `id` |
| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc` |
+| `updated_after` | datetime | no | Return deployments updated after the specified date |
+| `updated_before` | datetime | no | Return deployments updated before the specified date |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments"
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 29f9cb40e41..fe551cfb397 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -627,7 +627,8 @@ POST /projects/:id/issues
| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values.|
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
| `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. |
-| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_id` **(ULTIMATE)** | integer | no | ID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -729,7 +730,8 @@ PUT /projects/:id/issues/:issue_iid
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
| `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. 0 |
| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
-| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_id` **(ULTIMATE)** | integer | no | ID of the epic to add the issue to. Valid values are greater than or equal to 0. |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
diff --git a/doc/api/markdown.md b/doc/api/markdown.md
index f5aee725c6a..f4ad1de9ad8 100644
--- a/doc/api/markdown.md
+++ b/doc/api/markdown.md
@@ -12,7 +12,7 @@ POST /api/v4/markdown
| Attribute | Type | Required | Description |
| --------- | ------- | ------------- | ------------------------------------------ |
-| `text` | string | yes | The markdown text to render |
+| `text` | string | yes | The Markdown text to render |
| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` |
| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. |
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 41c4bcd00ce..b5e24188043 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -312,7 +312,7 @@ POST /projects/:id/releases
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `name` | string | no | The release name. |
| `tag_name` | string | yes | The tag where the release will be created from. |
-| `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). |
+| `description` | string | yes | The description of the release. You can use [Markdown](../../user/markdown.md). |
| `ref` | string | yes, if `tag_name` doesn't exist | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. |
| `milestones` | array of string | no | The title of each milestone the release is associated with. |
| `assets:links` | array of hash | no | An array of assets links. |
@@ -439,7 +439,7 @@ PUT /projects/:id/releases/:tag_name
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag where the release will be created from. |
| `name` | string | no | The release name. |
-| `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). |
+| `description` | string | no | The description of the release. You can use [Markdown](../../user/markdown.md). |
| `milestones` | array of string | no | The title of each milestone to associate with the release (`[]` to remove all milestones from the release). |
| `released_at` | datetime | no | The date when the release will be/was ready. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
diff --git a/doc/api/services.md b/doc/api/services.md
index 609c7e62e36..02a31ba9d38 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -229,6 +229,51 @@ Get Campfire service settings for a project.
GET /projects/:id/services/campfire
```
+## Unify Circuit
+
+Unify Circuit RTC and collaboration tool.
+
+### Create/Edit Unify Circuit service
+
+Set Unify Circuit service for a project.
+
+```
+PUT /projects/:id/services/unify-circuit
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Unify Circuit webhook. For example, `https://circuit.com/rest/v2/webhooks/incoming/...`. |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `branches_to_be_notified` | string | all | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Unify Circuit service
+
+Delete Unify Circuit service for a project.
+
+```
+DELETE /projects/:id/services/unify-circuit
+```
+
+### Get Unify Circuit service settings
+
+Get Unify Circuit service settings for a project.
+
+```
+GET /projects/:id/services/unify-circuit
+```
+
## Custom Issue Tracker
Custom issue tracker
@@ -480,6 +525,7 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
@@ -1088,6 +1134,7 @@ Parameters:
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `tag_push_events` | boolean | false | Enable notifications for tag push events |
| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
| `push_channel` | string | false | The name of the channel to receive push events notifications |
@@ -1095,6 +1142,7 @@ Parameters:
| `confidential_issue_channel` | string | false | The name of the channel to receive confidential issues events notifications |
| `merge_request_channel` | string | false | The name of the channel to receive merge request events notifications |
| `note_channel` | string | false | The name of the channel to receive note events notifications |
+| `confidential_note_channel` | boolean | The name of the channel to receive confidential note events notifications |
| `tag_push_channel` | string | false | The name of the channel to receive tag push events notifications |
| `pipeline_channel` | string | false | The name of the channel to receive pipeline events notifications |
| `wiki_page_channel` | string | false | The name of the channel to receive wiki page events notifications |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 51d5e5f35d7..ad9ffcbf872 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -265,7 +265,7 @@ are listed in the descriptions of the relevant settings.
| `html_emails_enabled` | boolean | no | Enable HTML emails. |
| `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `bitbucket_server`, `gitlab`, `google_code`, `fogbugz`, `git`, `gitlab_project`, `gitea`, `manifest`, and `phabricator`. |
| `instance_statistics_visibility_private` | boolean | no | When set to `true` Instance statistics will only be available to admins. |
-| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
+| `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. |
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB |
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB |
@@ -350,3 +350,4 @@ are listed in the descriptions of the relevant settings.
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `web_ide_clientside_preview_enabled` | boolean | no | Client side evaluation (allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation). |
+| `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).|
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 13c4b83dda8..11291065edc 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -189,7 +189,7 @@ Parameters:
Request body:
-- `description` (required) - Release notes with markdown support
+- `description` (required) - Release notes with Markdown support
```json
{
@@ -221,7 +221,7 @@ Parameters:
Request body:
-- `description` (required) - Release notes with markdown support
+- `description` (required) - Release notes with Markdown support
```json
{
diff --git a/doc/ci/jenkins/index.md b/doc/ci/jenkins/index.md
index 321d0d2778f..6e9e723feb5 100644
--- a/doc/ci/jenkins/index.md
+++ b/doc/ci/jenkins/index.md
@@ -228,5 +228,5 @@ our very powerful [`only/except` rules system](../yaml/README.md#onlyexcept-basi
```yaml
my_job:
- only: branches
+ only: [branches]
```
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index a49279f1932..7a7fa08fac1 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -30,7 +30,7 @@ Pipelines for merge requests have the following requirements and limitations:
## Configuring pipelines for merge requests
-To configure pipelines for merge requests, add the `only: merge_requests` parameter to
+To configure pipelines for merge requests, add the `only: [merge_requests]` parameter to
the jobs that you want to run only for merge requests.
Then, when developers create or update merge requests, a pipeline runs
@@ -68,7 +68,7 @@ After the merge request is updated with new commits:
- The pipeline fetches the latest code from the source branch and run tests against it.
In the above example, the pipeline contains only a `test` job.
-Since the `build` and `deploy` jobs don't have the `only: merge_requests` parameter,
+Since the `build` and `deploy` jobs don't have the `only: [merge_requests]` parameter,
they will not run in the merge request.
Pipelines tagged with the **detached** badge indicate that they were triggered
@@ -86,7 +86,7 @@ Read the [documentation on Merge Trains](pipelines_for_merged_results/merge_trai
## Excluding certain jobs
-The behavior of the `only: merge_requests` parameter is such that _only_ jobs with
+The behavior of the `only: [merge_requests]` parameter is such that _only_ jobs with
that parameter are run in the context of a merge request; no other jobs will be run.
However, you may want to reverse this behavior, having all of your jobs to run _except_
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index cff797549ba..488d9a05a3c 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -284,6 +284,8 @@ export CI_PROJECT_PATH="gitlab-org/gitlab-foss"
export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-foss"
export CI_REGISTRY="registry.example.com"
export CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-foss"
+export CI_REGISTRY_USER="gitlab-ci-token"
+export CI_REGISTRY_PASSWORD="longalfanumstring"
export CI_RUNNER_ID="10"
export CI_RUNNER_DESCRIPTION="my runner"
export CI_RUNNER_TAGS="docker, linux"
@@ -295,10 +297,8 @@ export CI_SERVER_VERSION="8.9.0"
export CI_SERVER_VERSION_MAJOR="8"
export CI_SERVER_VERSION_MINOR="9"
export CI_SERVER_VERSION_PATCH="0"
-export GITLAB_USER_ID="42"
export GITLAB_USER_EMAIL="user@example.com"
-export CI_REGISTRY_USER="gitlab-ci-token"
-export CI_REGISTRY_PASSWORD="longalfanumstring"
+export GITLAB_USER_ID="42"
```
### `.gitlab-ci.yml` defined variables
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index b93ff62cc21..837fcd01050 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -84,7 +84,7 @@ future GitLab releases.**
| `CI_PAGES_URL` | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. |
| `CI_PIPELINE_ID` | 8.10 | all | The unique id of the current pipeline that GitLab CI uses internally |
| `CI_PIPELINE_IID` | 11.0 | all | The unique id of the current pipeline scoped to project |
-| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
+| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, `pipeline` and `merge_request_event`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered](../triggers/README.md) |
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
| `CI_PROJECT_DIR` | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 12c009d9e90..4a06f99f99e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -780,7 +780,7 @@ it is possible to define a job to be created based on files modified
in a merge request.
In order to deduce the correct base SHA of the source branch, we recommend combining
-this keyword with `only: merge_requests`. This way, file differences are correctly
+this keyword with `only: [merge_requests]`. This way, file differences are correctly
calculated from any further commits, thus all changes in the merge requests are properly
tested in pipelines.
@@ -802,7 +802,7 @@ either files in `service-one` directory or the `Dockerfile`, GitLab creates
and triggers the `docker build service one` job.
Note that if [pipelines for merge requests](../merge_request_pipelines/index.md) is
-combined with `only: change`, but `only: merge_requests` is omitted, there could be
+combined with `only: [change]`, but `only: [merge_requests]` is omitted, there could be
unwanted behavior.
For example:
@@ -1247,6 +1247,7 @@ This is useful if you want to avoid jobs entering `pending` state immediately.
You can set the period with `start_in` key. The value of `start_in` key is an elapsed time in seconds, unless a unit is
provided. `start_in` key must be less than or equal to one week. Examples of valid values include:
+- `'5'`
- `10 seconds`
- `30 minutes`
- `1 day`
diff --git a/doc/development/README.md b/doc/development/README.md
index 66df6f46e86..6aeaf31ed29 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -69,6 +69,8 @@ description: 'Learn how to contribute to GitLab.'
- [Developing against interacting components or features](interacting_components.md)
- [File uploads](uploads.md)
- [Auto DevOps development guide](auto_devops.md)
+- [Mass Inserting Models](mass_insert.md)
+- [Cycle Analytics development guide](cycle_analytics.md)
## Performance guides
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 1cf2ca3667d..1ef0b928820 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -1,5 +1,13 @@
# GraphQL API
+## How GitLab implements GraphQL
+
+We use the [graphql-ruby gem](https://graphql-ruby.org/) written by [Robert Mosolgo](https://github.com/rmosolgo/).
+
+All GraphQL queries are directed to a single endpoint
+([`app/controllers/graphql_controller.rb#execute`](https://gitlab.com/gitlab-org/gitlab/blob/master/app%2Fcontrollers%2Fgraphql_controller.rb)),
+which is exposed as an API endpoint at `/api/graphql`.
+
## Deep Dive
In March 2019, Nick Thomas hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1)
@@ -22,8 +30,31 @@ add a `HTTP_PRIVATE_TOKEN` header.
## Types
+We use a code-first schema, and we declare what type everything is in Ruby.
+
+For example, `app/graphql/types/issue_type.rb`:
+
+```ruby
+graphql_name 'Issue'
+
+field :iid, GraphQL::ID_TYPE, null: false
+field :title, GraphQL::STRING_TYPE, null: false
+
+# we also have a method here that we've defined, that extends `field`
+markdown_field :title_html, null: true
+field :description, GraphQL::STRING_TYPE, null: true
+markdown_field :description_html, null: true
+```
+
+We give each type a name (in this case `Issue`).
+
+The `iid`, `title` and `description` are _scalar_ GraphQL types.
+`iid` is a `GraphQL::ID_TYPE`, a special string type that signifies a unique ID.
+`title` and `description` are regular `GraphQL::STRING_TYPE` types.
+
When exposing a model through the GraphQL API, we do so by creating a
-new type in `app/graphql/types`.
+new type in `app/graphql/types`. You can also declare custom GraphQL data types
+for scalar data types (e.g. `TimeType`).
When exposing properties in a type, make sure to keep the logic inside
the definition as minimal as possible. Instead, consider moving any
@@ -293,6 +324,8 @@ If the:
- Resource is part of a collection, the collection will be filtered to
exclude the objects that the user's authorization checks failed against.
+Also see [authorizing resources in a mutation](#authorizing-resources).
+
TIP: **Tip:**
Try to load only what the currently authenticated user is allowed to
view with our existing finders first, without relying on authorization
@@ -391,6 +424,11 @@ end
## Resolvers
+We define how the application serves the response using _resolvers_
+stored in the `app/graphql/resolvers` directory.
+The resolver provides the actual implementation logic for retrieving
+the objects in question.
+
To find objects to display in a field, we can add resolvers to
`app/graphql/resolvers`.
@@ -618,7 +656,61 @@ it 'returns a successful response' do
end
```
+## Notes about Query flow and GraphQL infrastructure
+
+GitLab's GraphQL infrastructure can be found in `lib/gitlab/graphql`.
+
+[Instrumentation](https://graphql-ruby.org/queries/instrumentation.html) is functionality
+that wraps around a query being executed. It is implemented as a module that uses the `Instrumentation` class.
+
+Example: `Present`
+
+```ruby
+module Present
+ #... some code above...
+
+ def self.use(schema_definition)
+ schema_definition.instrument(:field, Instrumentation.new)
+ end
+end
+```
+
+A [Query Analyzer](https://graphql-ruby.org/queries/analysis.html#analyzer-api) contains a series
+of callbacks to validate queries before they are executed. Each field can pass through
+the analyzer, and the final value is also available to you.
+
+[Multiplex queries](https://graphql-ruby.org/queries/multiplex.html) enable
+multiple queries to be sent in a single request. This reduces the number of requests sent to the server.
+(there are custom Multiplex Query Analyzers and Multiplex Instrumentation provided by graphql-ruby).
+
+### Query limits
+
+Queries and mutations are limited by depth, complexity, and recursion
+to protect server resources from overly ambitious or malicious queries.
+These values can be set as defaults and overridden in specific queries as needed.
+The complexity values can be set per object as well, and the final query complexity is
+evaluated based on how many objects are being returned. This is useful
+for objects that are expensive (e.g. requiring Gitaly calls).
+
+For example, a conditional complexity method in a resolver:
+
+```ruby
+def self.resolver_complexity(args, child_complexity:)
+ complexity = super
+ complexity += 2 if args[:labelName]
+
+ complexity
+end
+```
+
+More about complexity:
+[graphql-ruby docs](https://graphql-ruby.org/queries/complexity_and_depth.html)
+
## Documentation and Schema
+Our schema is located at `app/graphql/gitlab_schema.rb`.
+See the [schema reference](../api/graphql/reference/index.md) for details.
+
+This generated GraphQL documentation needs to be updated when the schema changes.
For information on generating GraphQL documentation and schema files, see
-[Rake tasks related to GraphQL](rake_tasks.md#update-graphql-documentation-and-schema-definitions).
+[updating the schema documentation](rake_tasks.md#update-graphql-documentation-and-schema-definitions).
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index f825b3d7088..d53675cc716 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -40,8 +40,8 @@ This saves you time as you don't have to wait for the same errors to be detected
[rss-source]: https://github.com/rubocop-hq/ruby-style-guide/blob/master/README.adoc#source-code-layout
[rss-naming]: https://github.com/rubocop-hq/ruby-style-guide/blob/master/README.adoc#naming-conventions
[doc-guidelines]: ../documentation/index.md "Documentation guidelines"
-[js-styleguide]: ../fe_guide/style_guide_js.md "JavaScript styleguide"
-[scss-styleguide]: ../fe_guide/style_guide_scss.md "SCSS styleguide"
+[js-styleguide]: ../fe_guide/style/javascript.md "JavaScript styleguide"
+[scss-styleguide]: ../fe_guide/style/scss.md "SCSS styleguide"
[newlines-styleguide]: ../newlines_styleguide.md "Newlines styleguide"
[testing]: ../testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
diff --git a/doc/development/cycle_analytics.md b/doc/development/cycle_analytics.md
new file mode 100644
index 00000000000..284645cdae7
--- /dev/null
+++ b/doc/development/cycle_analytics.md
@@ -0,0 +1,246 @@
+# Cycle Analytics development guide
+
+Cycle analytics calculates the time between two arbitrary events recorded on domain objects and provides aggregated statistics about the duration.
+
+## Stage
+
+During development, events occur that move issues and merge requests through different stages of progress until they are considered finished. These stages can be expressed with the `Stage` model.
+
+Example stage:
+
+- Name: Development
+- Start event: Issue created
+- End event: Issue first mentioned in commit
+- Parent: `Group: gitlab-org`
+
+### Events
+
+Events are the smallest building blocks of the cycle analytics feature. A stage consists of two events:
+
+- Start
+- End
+
+These events play a key role in the duration calculation.
+
+Formula: `duration = end_event_time - start_event_time`
+
+To make the duration calculation flexible, each `Event` is implemented as a separate class. They're responsible for defining a timestamp expression that will be used in the calculation query.
+
+#### Implementing an `Event` class
+
+There are a few methods that are required to be implemented, the `StageEvent` base class describes them in great detail. The most important ones are:
+
+- `object_type`
+- `timestamp_projection`
+
+The `object_type` method defines which domain object will be queried for the calculation. Currently two models are allowed:
+
+- `Issue`
+- `MergeRequest`
+
+For the duration calculation the `timestamp_projection` method will be used.
+
+```ruby
+def timestamp_projection
+ # your timestamp expression comes here
+end
+
+# event will use the issue creation time in the duration calculation
+def timestamp_projection
+ Issue.arel_table[:created_at]
+end
+```
+
+NOTE: **Note:**
+More complex expressions are also possible (e.g. using `COALESCE`). Look at the existing event classes for examples.
+
+In some cases, defining the `timestamp_projection` method is not enough. The calculation query should know which table contains the timestamp expression. Each `Event` class is responsible for making modifications to the calculation query to make the `timestamp_projection` work. This usually means joining an additional table.
+
+Example for joining the `issue_metrics` table and using the `first_mentioned_in_commit_at` column as the timestamp expression:
+
+```ruby
+def object_type
+ Issue
+end
+
+def timestamp_projection
+ IssueMetrics.arel_table[:first_mentioned_in_commit_at]
+end
+
+def apply_query_customization(query)
+ # in this case the query attribute will be based on the Issue model: `Issue.where(...)`
+ query.joins(:metrics)
+end
+```
+
+### Validating start and end events
+
+Some start/end event pairs are not "compatible" with each other. For example:
+
+- "Issue created" to "Merge Request created": The event classes are defined on different domain models, the `object_type` method is different.
+- "Issue closed" to "Issue created": Issue must be created first before it can be closed.
+- "Issue closed" to "Issue closed": Duration is always 0.
+
+The `StageEvents` module describes the allowed `start_event` and `end_event` pairings (`PAIRING_RULES` constant). If a new event is added, it needs to be registered in this module.
+​To add a new event:​
+
+1. Add an entry in `ENUM_MAPPING` with a unique number, it'll be used in the `Stage` model as `enum`.
+1. Define which events are compatible with the event in the `PAIRING_RULES` hash.
+
+Supported start/end event pairings:
+
+```mermaid
+graph LR;
+ IssueCreated --> IssueClosed;
+ IssueCreated --> IssueFirstAddedToBoard;
+ IssueCreated --> IssueFirstAssociatedWithMilestone;
+ IssueCreated --> IssueFirstMentionedInCommit;
+ IssueCreated --> IssueLastEdited;
+ IssueCreated --> IssueLabelAdded;
+ IssueCreated --> IssueLabelRemoved;
+ MergeRequestCreated --> MergeRequestMerged;
+ MergeRequestCreated --> MergeRequestClosed;
+ MergeRequestCreated --> MergeRequestFirstDeployedToProduction;
+ MergeRequestCreated --> MergeRequestLastBuildStarted;
+ MergeRequestCreated --> MergeRequestLastBuildFinished;
+ MergeRequestCreated --> MergeRequestLastEdited;
+ MergeRequestCreated --> MergeRequestLabelAdded;
+ MergeRequestCreated --> MergeRequestLabelRemoved;
+ MergeRequestLastBuildStarted --> MergeRequestLastBuildFinished;
+ MergeRequestLastBuildStarted --> MergeRequestClosed;
+ MergeRequestLastBuildStarted --> MergeRequestFirstDeployedToProduction;
+ MergeRequestLastBuildStarted --> MergeRequestLastEdited;
+ MergeRequestLastBuildStarted --> MergeRequestMerged;
+ MergeRequestLastBuildStarted --> MergeRequestLabelAdded;
+ MergeRequestLastBuildStarted --> MergeRequestLabelRemoved;
+ MergeRequestMerged --> MergeRequestFirstDeployedToProduction;
+ MergeRequestMerged --> MergeRequestClosed;
+ MergeRequestMerged --> MergeRequestFirstDeployedToProduction;
+ MergeRequestMerged --> MergeRequestLastEdited;
+ MergeRequestMerged --> MergeRequestLabelAdded;
+ MergeRequestMerged --> MergeRequestLabelRemoved;
+ IssueLabelAdded --> IssueLabelAdded;
+ IssueLabelAdded --> IssueLabelRemoved;
+ IssueLabelAdded --> IssueClosed;
+ IssueLabelRemoved --> IssueClosed;
+ IssueFirstAddedToBoard --> IssueClosed;
+ IssueFirstAddedToBoard --> IssueFirstAssociatedWithMilestone;
+ IssueFirstAddedToBoard --> IssueFirstMentionedInCommit;
+ IssueFirstAddedToBoard --> IssueLastEdited;
+ IssueFirstAddedToBoard --> IssueLabelAdded;
+ IssueFirstAddedToBoard --> IssueLabelRemoved;
+ IssueFirstAssociatedWithMilestone --> IssueClosed;
+ IssueFirstAssociatedWithMilestone --> IssueFirstAddedToBoard;
+ IssueFirstAssociatedWithMilestone --> IssueFirstMentionedInCommit;
+ IssueFirstAssociatedWithMilestone --> IssueLastEdited;
+ IssueFirstAssociatedWithMilestone --> IssueLabelAdded;
+ IssueFirstAssociatedWithMilestone --> IssueLabelRemoved;
+ IssueFirstMentionedInCommit --> IssueClosed;
+ IssueFirstMentionedInCommit --> IssueFirstAssociatedWithMilestone;
+ IssueFirstMentionedInCommit --> IssueFirstAddedToBoard;
+ IssueFirstMentionedInCommit --> IssueLastEdited;
+ IssueFirstMentionedInCommit --> IssueLabelAdded;
+ IssueFirstMentionedInCommit --> IssueLabelRemoved;
+ IssueClosed --> IssueLastEdited;
+ IssueClosed --> IssueLabelAdded;
+ IssueClosed --> IssueLabelRemoved;
+ MergeRequestClosed --> MergeRequestFirstDeployedToProduction;
+ MergeRequestClosed --> MergeRequestLastEdited;
+ MergeRequestClosed --> MergeRequestLabelAdded;
+ MergeRequestClosed --> MergeRequestLabelRemoved;
+ MergeRequestFirstDeployedToProduction --> MergeRequestLastEdited;
+ MergeRequestFirstDeployedToProduction --> MergeRequestLabelAdded;
+ MergeRequestFirstDeployedToProduction --> MergeRequestLabelRemoved;
+ MergeRequestLastBuildFinished --> MergeRequestClosed;
+ MergeRequestLastBuildFinished --> MergeRequestFirstDeployedToProduction;
+ MergeRequestLastBuildFinished --> MergeRequestLastEdited;
+ MergeRequestLastBuildFinished --> MergeRequestMerged;
+ MergeRequestLastBuildFinished --> MergeRequestLabelAdded;
+ MergeRequestLastBuildFinished --> MergeRequestLabelRemoved;
+ MergeRequestLabelAdded --> MergeRequestLabelAdded;
+ MergeRequestLabelAdded --> MergeRequestLabelRemoved;
+ MergeRequestLabelRemoved --> MergeRequestLabelAdded;
+ MergeRequestLabelRemoved --> MergeRequestLabelRemoved;
+```
+
+### Parent
+
+Teams and organizations might define their own way of building software, thus stages can be completely different. For each stage, a parent object needs to be defined.
+
+Currently supported parents:
+
+- `Project`
+- `Group`
+
+#### How parent relationship it work
+
+1. User navigates to the cycle analytics page.
+1. User selects a group.
+1. Backend loads the defined stages for the selected group.
+1. Additions and modifications to the stages will be persisted within the selected group only.
+
+### Default stages
+
+The [original implementation](https://gitlab.com/gitlab-org/gitlab/issues/847) of cycle analytics defined 7 stages. These stages are always available for each parent, however altering these stages is not possible.
+​
+To make things efficient and reduce the number of records created, the default stages are expressed as in-memory objects (not persisted). When the user creates a custom stage for the first time, all the stages will be persisted. This behaviour is implemented in the cycle analytics service objects.
+​
+The reason for this was that we'd like to add the abilities to hide and order stages later on.
+
+## Data Collector
+
+`DataCollector` is the central point where the data will be queried from the database. The class always operates on a single stage and consists of the following components:
+
+- `BaseQueryBuilder`:
+ - Responsible for composing the initial query.
+ - Deals with `Stage` specific configuration: events and their query customizations.
+ - Parameters coming from the UI: date ranges.
+- `Median`: Calculates the median duration for a stage using the query from `BaseQueryBuilder`.
+- `RecordsFetcher`: Loads relevant records for a stage using the query from `BaseQueryBuilder` and specific `Finder` classes to apply visibility rules.
+- `DataForDurationChart`: Loads calculated durations with the finish time (end event timestamp) for the scatterplot chart.
+
+For a new calculation or a query, implement it as a new method call in the `DataCollector` class.
+
+## Database query
+
+Structure of the database query:
+
+```sql
+SELECT (customized by: Median or RecordsFetcher or DataForDurationChart)
+FROM OBJECT_TYPE (Issue or MergeRequest)
+INNER JOIN (several JOIN statements, depending on the events)
+WHERE
+ (Filter by the PARENT model, example: filter Issues from Project A)
+ (Date range filter based on the OBJECT_TYPE.created_at)
+ (Check if the START_EVENT is earlier than END_EVENT, preventing negative duration)
+```
+
+Structure of the `SELECT` statement for `Median`:
+
+```sql
+SELECT (calculate median from START_EVENT_TIME-END_EVENT_TIME)
+```
+
+Structure of the `SELECT` statement for `DataForDurationChart`:
+
+```sql
+SELECT (START_EVENT_TIME-END_EVENT_TIME) as duration, END_EVENT.timestamp
+```
+
+## High-level overview
+
+- Rails Controller (`Analytics::CycleAnalytics` module): Cycle analytics exposes its data via JSON endpoints, implemented within the `analytics` workspace. Configuring the stages are also implements JSON endpoints (CRUD).
+- Services (`Analytics::CycleAnalytics` module): All `Stage` related actions will be delegated to respective service objects.
+- Models (`Analytics::CycleAnalytics` module): Models are used to persist the `Stage` objects `ProjectStage` and `GroupStage`.
+- Feature classes (`Gitlab::Analytics::CycleAnalytics` module):
+ - Responsible for composing queries and define feature specific busines logic.
+ - `DataCollector`, `Event`, `StageEvents`, etc.
+
+## Testing
+
+Since we have a lots of events and possible pairings, testing each pairing is not possible. The rule is to have at least one test case using an `Event` class.
+
+Writing a test case for a stage using a new `Event` can be challenging since data must be created for both events. To make this a bit simpler, each test case must be implemented in the `data_collector_spec.rb` where the stage is tested through the `DataCollector`. Each test case will be turned into multiple tests, covering the following cases:
+
+- Different parents: `Group` or `Project`
+- Different calculations: `Median`, `RecordsFetcher` or `DataForDurationChart`
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index f3c19002417..38785897361 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -101,6 +101,7 @@ and details for a database reviewer:
- Check consistency with `db/schema.rb` and that migrations are [reversible](migration_style_guide.md#reversibility)
- Check queries timing (If any): Queries executed in a migration
need to fit comfortably within `15s` - preferably much less than that - on GitLab.com.
+ - For column removals, make sure the column has been [ignored in a previous release](what_requires_downtime.md#dropping-columns)
- Check [background migrations](background_migrations.md):
- Establish a time estimate for execution on GitLab.com.
- They should only be used when migrating data in larger tables.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index a6474b8b141..64a59796ebc 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -30,6 +30,23 @@ The source of the documentation exists within the codebase of each GitLab applic
Documentation issues and merge requests are part of their respective repositories and all have the label `Documentation`.
+### Branch naming
+
+The [CI pipeline for the main GitLab project](../pipelines.md) is configured to automatically
+run only the jobs that match the type of contribution. If your contribution contains
+**only** documentation changes, then only documentation-related jobs will be run, and
+the pipeline will complete much faster than a code contribution.
+
+If you are submitting documentation-only changes to Runner, Omnibus, or Charts,
+the fast pipeline is not determined automatically. Instead, create branches for
+docs-only merge requests using the following guide:
+
+| Branch name | Valid example |
+|:----------------------|:-----------------------------|
+| Starting with `docs/` | `docs/update-api-issues` |
+| Starting with `docs-` | `docs-update-api-issues` |
+| Ending in `-docs` | `123-update-api-issues-docs` |
+
## Contributing to docs
[Contributions to GitLab docs](workflow.md) are welcome from the entire GitLab community.
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 359bac75e0d..fac0a581957 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -699,7 +699,7 @@ nicely on different mobile devices.
## Code blocks
- Always wrap code added to a sentence in inline code blocks (`` ` ``).
- E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`.
+ E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: [master]`.
File names, commands, entries, and anything that refers to code should be added to code blocks.
To make things easier for the user, always add a full code block for things that can be
useful to copy and paste, as they can easily do it with the button on code blocks.
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 894a613ec2d..b813ea24750 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -39,6 +39,50 @@ To distinguish queries from mutations and fragments, the following naming conven
- `addUser.mutation.graphql` for mutations;
- `basicUser.fragment.graphql` for fragments.
+GraphQL:
+
+- Queries are stored in `(ee/)app/assets/javascripts/` under the feature. For example, `respository/queries`. Frontend components can use these stored queries.
+- Mutations are stored in
+ `(ee/)app/assets/javascripts/<subfolders>/<name of mutation>.mutation.graphql`.
+
+### Fragments
+
+Fragments are a way to make your complex GraphQL queries more readable and re-usable.
+They can be stored in a separate file and imported.
+
+For example, a fragment that references another fragment:
+
+```ruby
+fragment BaseEpic on Epic {
+ id
+ iid
+ title
+ webPath
+ relativePosition
+ userPermissions {
+ adminEpic
+ createEpic
+ }
+}
+
+fragment EpicNode on Epic {
+ ...BaseEpic
+ state
+ reference(full: true)
+ relationPath
+ createdAt
+ closedAt
+ hasChildren
+ hasIssues
+ group {
+ fullPath
+ }
+}
+```
+
+More about fragments:
+[GraphQL Docs](https://graphql.org/learn/queries/#fragments)
+
## Usage in Vue
To use Vue Apollo, import the [Vue Apollo][vue-apollo] plugin as well
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 1cf798cedb6..f13ef767660 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -76,15 +76,18 @@ Read the [frontend's FAQ](frontend_faq.md) for common small pieces of helpful in
## Style Guides
-### [JavaScript Style Guide](style_guide_js.md)
+See the relevant style guides for our guidelines and for information on linting:
-We use eslint to enforce our JavaScript style guides. Our guide is based on
+- [JavaScript](style/javascript.md). Our guide is based on
the excellent [Airbnb][airbnb-js-style-guide] style guide with a few small
changes.
+- [SCSS](style/scss.md): our SCSS conventions which are enforced through [`scss-lint`](https://github.com/brigade/scss-lint).
+- [HTML](style/html.md). Guidelines for writing HTML code consistent with the rest of the codebase.
+- [Vue](style/vue.md). Guidelines and conventions for Vue code may be found here.
-### [SCSS Style Guide](style_guide_scss.md)
+## Tooling
-Our SCSS conventions which are enforced through [scss-lint](https://github.com/sds/scss-lint).
+Our code is automatically formatted with [Prettier](https://prettier.io) to follow our guidelines. Read our [Tooling guide](tooling.md) for more detail.
## [Performance](performance.md)
diff --git a/doc/development/fe_guide/style/html.md b/doc/development/fe_guide/style/html.md
new file mode 100644
index 00000000000..1445da3f0e1
--- /dev/null
+++ b/doc/development/fe_guide/style/html.md
@@ -0,0 +1,53 @@
+# HTML style guide
+
+## Buttons
+
+### Button type
+
+Button tags requires a `type` attribute according to the [W3C HTML specification](https://www.w3.org/TR/2011/WD-html5-20110525/the-button-element.html#dom-button-type).
+
+```html
+// bad
+<button></button>
+
+// good
+<button type="button"></button>
+```
+
+### Button role
+
+If an HTML element has an `onClick` handler but is not a button, it should have `role="button"`. This is [more accessible](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role).
+
+```html
+// bad
+<div onClick="doSomething"></div>
+
+// good
+<div role="button" onClick="doSomething"></div>
+```
+
+## Links
+
+### Blank target
+
+Use `rel="noopener noreferrer"` whenever your links open in a new window, i.e. `target="_blank"`. This prevents a security vulnerability [documented by JitBit](https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/).
+
+```html
+// bad
+<a href="url" target="_blank"></a>
+
+// good
+<a href="url" target="_blank" rel="noopener noreferrer"></a>
+```
+
+### Fake links
+
+**Do not use fake links.** Use a button tag if a link only invokes JavaScript click event handlers, which is more semantic.
+
+```html
+// bad
+<a class="js-do-something" href="#"></a>
+
+// good
+<button class="js-do-something" type="button"></button>
+```
diff --git a/doc/development/fe_guide/style/index.md b/doc/development/fe_guide/style/index.md
new file mode 100644
index 00000000000..3b07a8557f5
--- /dev/null
+++ b/doc/development/fe_guide/style/index.md
@@ -0,0 +1,21 @@
+# GitLab development style guides
+
+See below the relevant style guides, guidelines, linting, and other information for developing GitLab.
+
+## JavaScript style guide
+
+We use `eslint` to enforce our [JavaScript style guides](javascript.md). Our guide is based on
+the excellent [AirBnB](https://github.com/airbnb/javascript) style guide with a few small
+changes.
+
+## SCSS style guide
+
+Our [SCSS conventions](scss.md) which are enforced through [`scss-lint`](https://github.com/brigade/scss-lint).
+
+## HTML style guide
+
+Guidelines for writing [HTML code](html.md) consistent with the rest of the codebase.
+
+## Vue style guide
+
+Guidelines and conventions for Vue code may be found within the [Vue style guide](vue.md).
diff --git a/doc/development/fe_guide/style/javascript.md b/doc/development/fe_guide/style/javascript.md
new file mode 100644
index 00000000000..f40e8c7b5df
--- /dev/null
+++ b/doc/development/fe_guide/style/javascript.md
@@ -0,0 +1,275 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/development/fe_guide/style_guide_js.html'
+---
+
+# JavaScript style guide
+
+We use [Airbnb's JavaScript Style Guide](https://github.com/airbnb/javascript) and it's accompanying
+linter to manage most of our JavaScript style guidelines.
+
+In addition to the style guidelines set by Airbnb, we also have a few specific rules
+listed below.
+
+> **Tip:**
+You can run eslint locally by running `yarn eslint`
+
+## Avoid forEach
+
+Avoid forEach when mutating data. Use `map`, `reduce` or `filter` instead of `forEach`
+when mutating data. This will minimize mutations in functions,
+which aligns with [Airbnb's style guide](https://github.com/airbnb/javascript#testing--for-real).
+
+```javascript
+// bad
+users.forEach((user, index) => {
+ user.id = index;
+});
+
+// good
+const usersWithId = users.map((user, index) => {
+ return Object.assign({}, user, { id: index });
+});
+```
+
+## Limit number of parameters
+
+If your function or method has more than 3 parameters, use an object as a parameter
+instead.
+
+```javascript
+// bad
+function a(p1, p2, p3) {
+ // ...
+};
+
+// good
+function a(p) {
+ // ...
+};
+```
+
+## Avoid classes to handle DOM events
+
+If the only purpose of the class is to bind a DOM event and handle the callback, prefer
+using a function.
+
+```javascript
+// bad
+class myClass {
+ constructor(config) {
+ this.config = config;
+ }
+
+ init() {
+ document.addEventListener('click', () => {});
+ }
+}
+
+// good
+
+const myFunction = () => {
+ document.addEventListener('click', () => {
+ // handle callback here
+ });
+}
+```
+
+## Pass element container to constructor
+
+When your class manipulates the DOM, receive the element container as a parameter.
+This is more maintainable and performant.
+
+```javascript
+// bad
+class a {
+ constructor() {
+ document.querySelector('.b');
+ }
+}
+
+// good
+class a {
+ constructor(options) {
+ options.container.querySelector('.b');
+ }
+}
+```
+
+## Use ParseInt
+
+Use `ParseInt` when converting a numeric string into a number.
+
+```javascript
+// bad
+Number('10')
+
+// good
+parseInt('10', 10);
+```
+
+## CSS Selectors - Use `js-` prefix
+
+If a CSS class is only being used in JavaScript as a reference to the element, prefix
+the class name with `js-`.
+
+```html
+// bad
+<button class="add-user"></button>
+
+// good
+<button class="js-add-user"></button>
+```
+
+## ES Module Syntax
+
+Use ES module syntax to import modules:
+
+```javascript
+// bad
+const SomeClass = require('some_class');
+
+// good
+import SomeClass from 'some_class';
+
+// bad
+module.exports = SomeClass;
+
+// good
+export default SomeClass;
+```
+
+_Note:_ We still use `require` in `scripts/` and `config/` files.
+
+## Absolute vs relative paths for modules
+
+Use relative paths if the module you are importing is less than two levels up.
+
+```javascript
+// bad
+import GitLabStyleGuide from '~/guides/GitLabStyleGuide';
+
+// good
+import GitLabStyleGuide from '../GitLabStyleGuide';
+```
+
+If the module you are importing is two or more levels up, use an absolute path instead:
+
+```javascript
+// bad
+import GitLabStyleGuide from '../../../guides/GitLabStyleGuide';
+
+// good
+import GitLabStyleGuide from '~/GitLabStyleGuide';
+```
+
+Additionally, **do not add to global namespace**.
+
+## Do not use `DOMContentLoaded` in non-page modules
+
+Imported modules should act the same each time they are loaded. `DOMContentLoaded`
+events are only allowed on modules loaded in the `/pages/*` directory because those
+are loaded dynamically with webpack.
+
+## Avoid XSS
+
+Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many
+vulnerabilities.
+
+## ESLint
+
+ESLint behaviour can be found in our [tooling guide](../tooling.md).
+
+## IIFEs
+
+Avoid using IIFEs (Immediately-Invoked Function Expressions). Although
+we have a lot of examples of files which wrap their contents in IIFEs,
+this is no longer necessary after the transition from Sprockets to webpack.
+Do not use them anymore and feel free to remove them when refactoring legacy code.
+
+## Global namespace
+
+Avoid adding to the global namespace.
+
+```javascript
+// bad
+window.MyClass = class { /* ... */ };
+
+// good
+export default class MyClass { /* ... */ }
+```
+
+## Side effects
+
+### Top-level side effects
+
+Top-level side effects are forbidden in any script which contains `export`:
+
+```javascript
+// bad
+export default class MyClass { /* ... */ }
+
+document.addEventListener("DOMContentLoaded", function(event) {
+ new MyClass();
+}
+```
+
+### Avoid side effects in constructors
+
+Avoid making asynchronous calls, API requests or DOM manipulations in the `constructor`.
+Move them into separate functions instead. This will make tests easier to write and
+avoids violating the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle).
+
+```javascript
+// bad
+class myClass {
+ constructor(config) {
+ this.config = config;
+ axios.get(this.config.endpoint)
+ }
+}
+
+// good
+class myClass {
+ constructor(config) {
+ this.config = config;
+ }
+
+ makeRequest() {
+ axios.get(this.config.endpoint)
+ }
+}
+const instance = new myClass();
+instance.makeRequest();
+```
+
+## Pure Functions and Data Mutation
+
+Strive to write many small pure functions and minimize where mutations occur
+
+ ```javascript
+ // bad
+ const values = {foo: 1};
+
+ function impureFunction(items) {
+ const bar = 1;
+
+ items.foo = items.a * bar + 2;
+
+ return items.a;
+ }
+
+ const c = impureFunction(values);
+
+ // good
+ var values = {foo: 1};
+
+ function pureFunction (foo) {
+ var bar = 1;
+
+ foo = foo * bar + 2;
+
+ return foo;
+ }
+
+ var c = pureFunction(values.foo);
+ ```
diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md
new file mode 100644
index 00000000000..02e7351d135
--- /dev/null
+++ b/doc/development/fe_guide/style/scss.md
@@ -0,0 +1,285 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/development/fe_guide/style_guide_scss.html'
+---
+
+# SCSS style guide
+
+This style guide recommends best practices for SCSS to make styles easy to read,
+easy to maintain, and performant for the end-user.
+
+## Rules
+
+### Utility Classes
+
+As part of the effort for [cleaning up our CSS and moving our components into GitLab-UI](https://gitlab.com/groups/gitlab-org/-/epics/950)
+led by the [GitLab UI WG](https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/20623) we prefer the use of utility classes over adding new CSS. However, complex CSS can be addressed by adding component classes.
+
+#### Where are utility classes defined?
+
+- [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/)
+- [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) (old)
+- [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) (new)
+
+#### Where should I put new utility classes?
+
+New utility classes should be added to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss). Existing classes include:
+
+| Name | Pattern | Example |
+|------|---------|---------|
+| Background color | `.bg-{variant}-{shade}` | `.bg-warning-400` |
+| Text color | `.text-{variant}-{shade}` | `.text-success-500` |
+| Font size | `.text-{size}` | `.text-2` |
+
+- `{variant}` is one of 'primary', 'secondary', 'success', 'warning', 'error'
+- `{shade}` is one of the shades listed on [colors](https://design.gitlab.com/product-foundations/colors/)
+- `{size}` is a number from 1-6 from our [Type scale](https://design.gitlab.com/product-foundations/typography/)
+
+#### When should I create component classes?
+
+We recommend a "utility-first" approach.
+
+1. Start with utility classes.
+1. If composing utility classes into a component class removes code duplication and encapsulates a clear responsibility, do it.
+
+This encourages an organic growth of component classes and prevents the creation of one-off unreusable classes. Also, the kind of classes that emerge from "utility-first" tend to be design-centered (e.g. `.button`, `.alert`, `.card`) rather than domain-centered (e.g. `.security-report-widget`, `.commit-header-icon`).
+
+Examples of component classes that were created using "utility-first" include:
+
+- [`.circle-icon-container`](https://gitlab.com/gitlab-org/gitlab/blob/579fa8b8ec7eb38d40c96521f517c9dab8c3b97a/app/assets/stylesheets/framework/icons.scss#L85)
+- [`.d-flex-center`](https://gitlab.com/gitlab-org/gitlab/blob/900083d89cd6af391d26ab7922b3f64fa2839bef/app/assets/stylesheets/framework/common.scss#L425)
+
+Inspiration:
+
+- <https://tailwindcss.com/docs/utility-first/>
+- <https://tailwindcss.com/docs/extracting-components/>
+
+### Naming
+
+Filenames should use `snake_case`.
+
+CSS classes should use the `lowercase-hyphenated` format rather than
+`snake_case` or `camelCase`.
+
+```scss
+// Bad
+.class_name {
+ color: #fff;
+}
+
+// Bad
+.className {
+ color: #fff;
+}
+
+// Good
+.class-name {
+ color: #fff;
+}
+```
+
+### Formatting
+
+You should always use a space before a brace, braces should be on the same
+line, each property should each get its own line, and there should be a space
+between the property and its value.
+
+```scss
+// Bad
+.container-item {
+ width: 100px; height: 100px;
+ margin-top: 0;
+}
+
+// Bad
+.container-item
+{
+ width: 100px;
+ height: 100px;
+ margin-top: 0;
+}
+
+// Bad
+.container-item{
+ width:100px;
+ height:100px;
+ margin-top:0;
+}
+
+// Good
+.container-item {
+ width: 100px;
+ height: 100px;
+ margin-top: 0;
+}
+```
+
+Note that there is an exception for single-line rulesets, although these are
+not typically recommended.
+
+```scss
+p { margin: 0; padding: 0; }
+```
+
+### Colors
+
+HEX (hexadecimal) colors should use shorthand where possible, and should use
+lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3`
+vs. `#e3e3e3`.
+
+```scss
+// Bad
+p {
+ color: #ffffff;
+}
+
+// Bad
+p {
+ color: #FFFFFF;
+}
+
+// Good
+p {
+ color: #fff;
+}
+```
+
+### Indentation
+
+Indentation should always use two spaces for each indentation level.
+
+```scss
+// Bad, four spaces
+p {
+ color: #f00;
+}
+
+// Good
+p {
+ color: #f00;
+}
+```
+
+### Semicolons
+
+Always include semicolons after every property. When the stylesheets are
+minified, the semicolons will be removed automatically.
+
+```scss
+// Bad
+.container-item {
+ width: 100px;
+ height: 100px
+}
+
+// Good
+.container-item {
+ width: 100px;
+ height: 100px;
+}
+```
+
+### Shorthand
+
+The shorthand form should be used for properties that support it.
+
+```scss
+// Bad
+margin: 10px 15px 10px 15px;
+padding: 10px 10px 10px 10px;
+
+// Good
+margin: 10px 15px;
+padding: 10px;
+```
+
+### Zero Units
+
+Omit length units on zero values, they're unnecessary and not including them
+is slightly more performant.
+
+```scss
+// Bad
+.item-with-padding {
+ padding: 0px;
+}
+
+// Good
+.item-with-padding {
+ padding: 0;
+}
+```
+
+### Selectors with a `js-` Prefix
+
+Do not use any selector prefixed with `js-` for styling purposes. These
+selectors are intended for use only with JavaScript to allow for removal or
+renaming without breaking styling.
+
+### IDs
+
+Don't use ID selectors in CSS.
+
+```scss
+// Bad
+#my-element {
+ padding: 0;
+}
+
+// Good
+.my-element {
+ padding: 0;
+}
+```
+
+### Variables
+
+Before adding a new variable for a color or a size, guarantee:
+
+- There isn't already one
+- There isn't a similar one we can use instead.
+
+## Linting
+
+We use [SCSS Lint](https://github.com/sds/scss-lint) to check for style guide conformity. It uses the
+ruleset in `.scss-lint.yml`, which is located in the home directory of the
+project.
+
+To check if any warnings will be produced by your changes, you can run `rake
+scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
+catch any warnings.
+
+If the Rake task is throwing warnings you don't understand, SCSS Lint's
+documentation includes [a full list of their linters][scss-lint-documentation](https://github.com/sds/scss-lint/blob/master/lib/scss_lint/linter/README.md).
+
+### Fixing issues
+
+If you want to automate changing a large portion of the codebase to conform to
+the SCSS style guide, you can use [CSSComb][csscomb]. First install
+[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
+CSSComb globally (system-wide). Run it in the GitLab directory with
+`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
+
+Note that this won't fix every problem, but it should fix a majority.
+
+### Ignoring issues
+
+If you want a line or set of lines to be ignored by the linter, you can use
+`// scss-lint:disable RuleName` ([more info](https://github.com/sds/scss-lint#disabling-linters-via-source)):
+
+```scss
+// This lint rule is disabled because it is supported only in Chrome/Safari
+// scss-lint:disable PropertySpelling
+body {
+ text-decoration-skip: ink;
+}
+// scss-lint:enable PropertySpelling
+```
+
+Make sure a comment is added on the line above the `disable` rule, otherwise the
+linter will throw a warning. `DisableLinterReason` is enabled to make sure the
+style guide isn't being ignored, and to communicate to others why the style
+guide is ignored in this instance.
+
+[csscomb]: https://github.com/csscomb/csscomb.js
+[node]: https://github.com/nodejs/node
+[npm]: https://www.npmjs.com/
diff --git a/doc/development/fe_guide/style/vue.md b/doc/development/fe_guide/style/vue.md
new file mode 100644
index 00000000000..2499623e66a
--- /dev/null
+++ b/doc/development/fe_guide/style/vue.md
@@ -0,0 +1,418 @@
+# Vue.js style guide
+
+## Linting
+
+We default to [eslint-vue-plugin](https://github.com/vuejs/eslint-plugin-vue), with the `plugin:vue/recommended`.
+Please check this [rules](https://github.com/vuejs/eslint-plugin-vue#bulb-rules) for more documentation.
+
+## Basic Rules
+
+1. The service has it's own file
+1. The store has it's own file
+1. Use a function in the bundle file to instantiate the Vue component:
+
+ ```javascript
+ // bad
+ class {
+ init() {
+ new Component({})
+ }
+ }
+
+ // good
+ document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#element',
+ components: {
+ componentName
+ },
+ render: createElement => createElement('component-name'),
+ }));
+ ```
+
+1. Do not use a singleton for the service or the store
+
+ ```javascript
+ // bad
+ class Store {
+ constructor() {
+ if (!this.prototype.singleton) {
+ // do something
+ }
+ }
+ }
+
+ // good
+ class Store {
+ constructor() {
+ // do something
+ }
+ }
+ ```
+
+1. Use `.vue` for Vue templates. Do not use `%template` in HAML.
+
+## Naming
+
+1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]).
+1. **Reference Naming**: Use PascalCase for their instances:
+
+ ```javascript
+ // bad
+ import cardBoard from 'cardBoard.vue'
+
+ components: {
+ cardBoard,
+ };
+
+ // good
+ import CardBoard from 'cardBoard.vue'
+
+ components: {
+ CardBoard,
+ };
+ ```
+
+1. **Props Naming:** Avoid using DOM component prop names.
+1. **Props Naming:** Use kebab-case instead of camelCase to provide props in templates.
+
+ ```javascript
+ // bad
+ <component class="btn">
+
+ // good
+ <component css-class="btn">
+
+ // bad
+ <component myProp="prop" />
+
+ // good
+ <component my-prop="prop" />
+ ```
+
+[#34371]: https://gitlab.com/gitlab-org/gitlab-foss/issues/34371
+
+## Alignment
+
+1. Follow these alignment styles for the template method:
+
+ 1. With more than one attribute, all attributes should be on a new line:
+
+ ```javascript
+ // bad
+ <component v-if="bar"
+ param="baz" />
+
+ <button class="btn">Click me</button>
+
+ // good
+ <component
+ v-if="bar"
+ param="baz"
+ />
+
+ <button class="btn">
+ Click me
+ </button>
+ ```
+
+ 1. The tag can be inline if there is only one attribute:
+
+ ```javascript
+ // good
+ <component bar="bar" />
+
+ // good
+ <component
+ bar="bar"
+ />
+
+ // bad
+ <component
+ bar="bar" />
+ ```
+
+## Quotes
+
+1. Always use double quotes `"` inside templates and single quotes `'` for all other JS.
+
+ ```javascript
+ // bad
+ template: `
+ <button :class='style'>Button</button>
+ `
+
+ // good
+ template: `
+ <button :class="style">Button</button>
+ `
+ ```
+
+## Props
+
+1. Props should be declared as an object
+
+ ```javascript
+ // bad
+ props: ['foo']
+
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ default: 'bar'
+ }
+ }
+ ```
+
+1. Required key should always be provided when declaring a prop
+
+ ```javascript
+ // bad
+ props: {
+ foo: {
+ type: String,
+ }
+ }
+
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ default: 'bar'
+ }
+ }
+ ```
+
+1. Default key should be provided if the prop is not required.
+ _Note:_ There are some scenarios where we need to check for the existence of the property.
+ On those a default key should not be provided.
+
+ ```javascript
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ }
+ }
+
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ default: 'bar'
+ }
+ }
+
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: true
+ }
+ }
+ ```
+
+## Data
+
+1. `data` method should always be a function
+
+ ```javascript
+ // bad
+ data: {
+ foo: 'foo'
+ }
+
+ // good
+ data() {
+ return {
+ foo: 'foo'
+ };
+ }
+ ```
+
+## Directives
+
+1. Shorthand `@` is preferable over `v-on`
+
+ ```javascript
+ // bad
+ <component v-on:click="eventHandler"/>
+
+ // good
+ <component @click="eventHandler"/>
+ ```
+
+1. Shorthand `:` is preferable over `v-bind`
+
+ ```javascript
+ // bad
+ <component v-bind:class="btn"/>
+
+ // good
+ <component :class="btn"/>
+ ```
+
+1. Shorthand `#` is preferable over `v-slot`
+
+ ```javascript
+ // bad
+ <template v-slot:header></template>
+
+ // good
+ <template #header></template>
+ ```
+
+## Closing tags
+
+1. Prefer self closing component tags
+
+ ```javascript
+ // bad
+ <component></component>
+
+ // good
+ <component />
+ ```
+
+## Component usage within templates
+
+1. Prefer a component's kebab-cased name over other styles when using it in a template
+
+ ```javascript
+ // bad
+ <MyComponent />
+
+ // good
+ <my-component />
+ ```
+
+## Ordering
+
+1. Tag order in `.vue` file
+
+ ```
+ <script>
+ // ...
+ </script>
+
+ <template>
+ // ...
+ </template>
+
+ // We don't use scoped styles but there are few instances of this
+ <style>
+ // ...
+ </style>
+ ```
+
+1. Properties in a Vue Component:
+ Check [order of properties in components rule](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/order-in-components.md).
+
+## `:key`
+
+When using `v-for` you need to provide a *unique* `:key` attribute for each item.
+
+1. If the elements of the array being iterated have an unique `id` it is advised to use it:
+
+ ```html
+ <div
+ v-for="item in items"
+ :key="item.id"
+ >
+ <!-- content -->
+ </div>
+ ```
+
+1. When the elements being iterated don't have a unique id, you can use the array index as the `:key` attribute
+
+ ```html
+ <div
+ v-for="(item, index) in items"
+ :key="index"
+ >
+ <!-- content -->
+ </div>
+ ```
+
+1. When using `v-for` with `template` and there is more than one child element, the `:key` values must be unique. It's advised to use `kebab-case` namespaces.
+
+ ```html
+ <template v-for="(item, index) in items">
+ <span :key="`span-${index}`"></span>
+ <button :key="`button-${index}`"></button>
+ </template>
+ ```
+
+1. When dealing with nested `v-for` use the same guidelines as above.
+
+ ```html
+ <div
+ v-for="item in items"
+ :key="item.id"
+ >
+ <span
+ v-for="element in array"
+ :key="element.id"
+ >
+ <!-- content -->
+ </span>
+ </div>
+ ```
+
+Useful links:
+
+1. [`key`](https://vuejs.org/v2/guide/list.html#key)
+1. [Vue Style Guide: Keyed v-for](https://vuejs.org/v2/style-guide/#Keyed-v-for-essential )
+
+## Vue and Bootstrap
+
+1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
+
+ ```javascript
+ // bad
+ <span
+ class="has-tooltip"
+ title="Some tooltip text">
+ Text
+ </span>
+
+ // good
+ <span
+ v-tooltip
+ title="Some tooltip text">
+ Text
+ </span>
+ ```
+
+1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js`
+
+1. Don't change `data-original-title`.
+
+ ```javascript
+ // bad
+ <span data-original-title="tooltip text">Foo</span>
+
+ // good
+ <span title="tooltip text">Foo</span>
+
+ $('span').tooltip('_fixTitle');
+ ```
+
+## The JavaScript/Vue Accord
+
+The goal of this accord is to make sure we are all on the same page.
+
+1. When writing Vue, you may not use jQuery in your application.
+ 1. If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery.
+ 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
+ 1. If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners.
+ 1. We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit).
+1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application.
+1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with it's priority to be determined by maintainers.
+1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests.
+1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
+1. Once you have chosen a centralized state management solution you must use it for your entire application. i.e. Don't mix and match your state management solutions.
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 43cd8180b6e..f3fa80325ef 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -1,731 +1,5 @@
-# Style guides and linting
+---
+redirect_to: 'style/javascript.md'
+---
-See the relevant style guides for our guidelines and for information on linting:
-
-## JavaScript
-
-We defer to [Airbnb][airbnb-js-style-guide] on most style-related
-conventions and enforce them with eslint.
-
-See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab/blob/master/.eslintrc.yml) for specific rules and patterns.
-
-### Common
-
-#### ESlint
-
-1. **Never** disable eslint rules unless you have a good reason.
- You may see a lot of legacy files with `/* eslint-disable some-rule, some-other-rule */`
- at the top, but legacy files are a special case. Any time you develop a new feature or
- refactor an existing one, you should abide by the eslint rules.
-
-1. **Never Ever EVER** disable eslint globally for a file
-
- ```javascript
- // bad
- /* eslint-disable */
-
- // better
- /* eslint-disable some-rule, some-other-rule */
-
- // best
- // nothing :)
- ```
-
-1. If you do need to disable a rule for a single violation, try to do it as locally as possible
-
- ```javascript
- // bad
- /* eslint-disable no-new */
-
- import Foo from 'foo';
-
- new Foo();
-
- // better
- import Foo from 'foo';
-
- // eslint-disable-next-line no-new
- new Foo();
- ```
-
-1. There are few rules that we need to disable due to technical debt. Which are:
- 1. [no-new](https://eslint.org/docs/rules/no-new)
- 1. [class-methods-use-this](https://eslint.org/docs/rules/class-methods-use-this)
-
-1. When they are needed _always_ place ESlint directive comment blocks on the first line of a script,
- followed by any global declarations, then a blank newline prior to any imports or code.
-
- ```javascript
- // bad
- /* global Foo */
- /* eslint-disable no-new */
- import Bar from './bar';
-
- // good
- /* eslint-disable no-new */
- /* global Foo */
-
- import Bar from './bar';
- ```
-
-1. **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
-
-1. When declaring multiple globals, always use one `/* global [name] */` line per variable.
-
- ```javascript
- // bad
- /* globals Flash, Cookies, jQuery */
-
- // good
- /* global Flash */
- /* global Cookies */
- /* global jQuery */
- ```
-
-1. Use up to 3 parameters for a function or class. If you need more accept an Object instead.
-
- ```javascript
- // bad
- fn(p1, p2, p3, p4) {}
-
- // good
- fn(options) {}
- ```
-
-#### Modules, Imports, and Exports
-
-1. Use ES module syntax to import modules
-
- ```javascript
- // bad
- const SomeClass = require('some_class');
-
- // good
- import SomeClass from 'some_class';
-
- // bad
- module.exports = SomeClass;
-
- // good
- export default SomeClass;
- ```
-
- Import statements are following usual naming guidelines, for example object literals use camel case:
-
- ```javascript
- // some_object file
- export default {
- key: 'value',
- };
-
- // bad
- import ObjectLiteral from 'some_object';
-
- // good
- import objectLiteral from 'some_object';
- ```
-
-1. Relative paths: when importing a module in the same directory, a child
- directory, or an immediate parent directory prefer relative paths. When
- importing a module which is two or more levels up, prefer either `~/` or `ee/`.
-
- In **app/assets/javascripts/my-feature/subdir**:
-
- ```javascript
- // bad
- import Foo from '~/my-feature/foo';
- import Bar from '~/my-feature/subdir/bar';
- import Bin from '~/my-feature/subdir/lib/bin';
-
- // good
- import Foo from '../foo';
- import Bar from './bar';
- import Bin from './lib/bin';
- ```
-
- In **spec/javascripts**:
-
- ```javascript
- // bad
- import Foo from '../../app/assets/javascripts/my-feature/foo';
-
- // good
- import Foo from '~/my-feature/foo';
- ```
-
- When referencing an **EE component**:
-
- ```javascript
- // bad
- import Foo from '../../../../../ee/app/assets/javascripts/my-feature/ee-foo';
-
- // good
- import Foo from 'ee/my-feature/foo';
- ```
-
-1. Avoid using IIFE. Although we have a lot of examples of files which wrap their
- contents in IIFEs (immediately-invoked function expressions),
- this is no longer necessary after the transition from Sprockets to webpack.
- Do not use them anymore and feel free to remove them when refactoring legacy code.
-
-1. Avoid adding to the global namespace.
-
- ```javascript
- // bad
- window.MyClass = class { /* ... */ };
-
- // good
- export default class MyClass { /* ... */ }
- ```
-
-1. Side effects are forbidden in any script which contains export
-
- ```javascript
- // bad
- export default class MyClass { /* ... */ }
-
- document.addEventListener("DOMContentLoaded", function(event) {
- new MyClass();
- }
- ```
-
-#### Data Mutation and Pure functions
-
-1. Strive to write many small pure functions, and minimize where mutations occur.
-
- ```javascript
- // bad
- const values = {foo: 1};
-
- function impureFunction(items) {
- const bar = 1;
-
- items.foo = items.a * bar + 2;
-
- return items.a;
- }
-
- const c = impureFunction(values);
-
- // good
- var values = {foo: 1};
-
- function pureFunction (foo) {
- var bar = 1;
-
- foo = foo * bar + 2;
-
- return foo;
- }
-
- var c = pureFunction(values.foo);
- ```
-
-1. Avoid constructors with side-effects.
- Although we aim for code without side-effects we need some side-effects for our code to run.
-
- If the class won't do anything if we only instantiate it, it's ok to add side effects into the constructor (_Note:_ The following is just an example. If the only purpose of the class is to add an event listener and handle the callback a function will be more suitable.)
-
- ```javascript
- // Bad
- export class Foo {
- constructor() {
- this.init();
- }
- init() {
- document.addEventListener('click', this.handleCallback)
- },
- handleCallback() {
-
- }
- }
-
- // Good
- export class Foo {
- constructor() {
- document.addEventListener()
- }
- handleCallback() {
- }
- }
- ```
-
- On the other hand, if a class only needs to extend a third party/add event listeners in some specific cases, they should be initialized outside of the constructor.
-
-1. Prefer `.map`, `.reduce` or `.filter` over `.forEach`
- A forEach will most likely cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
- `.reduce` or `.filter`
-
- ```javascript
- const users = [ { name: 'Foo' }, { name: 'Bar' } ];
-
- // bad
- users.forEach((user, index) => {
- user.id = index;
- });
-
- // good
- const usersWithId = users.map((user, index) => {
- return Object.assign({}, user, { id: index });
- });
- ```
-
-#### Parse Strings into Numbers
-
-1. `parseInt()` is preferable over `Number()` or `+`
-
- ```javascript
- // bad
- +'10' // 10
-
- // good
- Number('10') // 10
-
- // better
- parseInt('10', 10);
- ```
-
-#### CSS classes used for JavaScript
-
-1. If the class is being used in JavaScript it needs to be prepend with `js-`
-
- ```html
- // bad
- <button class="add-user">
- Add User
- </button>
-
- // good
- <button class="js-add-user">
- Add User
- </button>
- ```
-
-### Vue.js
-
-#### `eslint-vue-plugin`
-
-We default to [eslint-vue-plugin][eslint-plugin-vue], with the `plugin:vue/recommended`.
-Please check this [rules][eslint-plugin-vue-rules] for more documentation.
-
-#### Basic Rules
-
-1. The service has it's own file
-1. The store has it's own file
-1. Use a function in the bundle file to instantiate the Vue component:
-
- ```javascript
- // bad
- class {
- init() {
- new Component({})
- }
- }
-
- // good
- document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#element',
- components: {
- componentName
- },
- render: createElement => createElement('component-name'),
- }));
- ```
-
-1. Do not use a singleton for the service or the store
-
- ```javascript
- // bad
- class Store {
- constructor() {
- if (!this.prototype.singleton) {
- // do something
- }
- }
- }
-
- // good
- class Store {
- constructor() {
- // do something
- }
- }
- ```
-
-1. Use `.vue` for Vue templates. Do not use `%template` in HAML.
-
-#### Naming
-
-1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]).
-1. **Reference Naming**: Use PascalCase for their instances:
-
- ```javascript
- // bad
- import cardBoard from 'cardBoard.vue'
-
- components: {
- cardBoard,
- };
-
- // good
- import CardBoard from 'cardBoard.vue'
-
- components: {
- CardBoard,
- };
- ```
-
-1. **Props Naming:** Avoid using DOM component prop names.
-1. **Props Naming:** Use kebab-case instead of camelCase to provide props in templates.
-
- ```javascript
- // bad
- <component class="btn">
-
- // good
- <component css-class="btn">
-
- // bad
- <component myProp="prop" />
-
- // good
- <component my-prop="prop" />
- ```
-
-[#34371]: https://gitlab.com/gitlab-org/gitlab-foss/issues/34371
-
-#### Alignment
-
-1. Follow these alignment styles for the template method:
-
- 1. With more than one attribute, all attributes should be on a new line:
-
- ```javascript
- // bad
- <component v-if="bar"
- param="baz" />
-
- <button class="btn">Click me</button>
-
- // good
- <component
- v-if="bar"
- param="baz"
- />
-
- <button class="btn">
- Click me
- </button>
- ```
-
- 1. The tag can be inline if there is only one attribute:
-
- ```javascript
- // good
- <component bar="bar" />
-
- // good
- <component
- bar="bar"
- />
-
- // bad
- <component
- bar="bar" />
- ```
-
-#### Quotes
-
-1. Always use double quotes `"` inside templates and single quotes `'` for all other JS.
-
- ```javascript
- // bad
- template: `
- <button :class='style'>Button</button>
- `
-
- // good
- template: `
- <button :class="style">Button</button>
- `
- ```
-
-#### Props
-
-1. Props should be declared as an object
-
- ```javascript
- // bad
- props: ['foo']
-
- // good
- props: {
- foo: {
- type: String,
- required: false,
- default: 'bar'
- }
- }
- ```
-
-1. Required key should always be provided when declaring a prop
-
- ```javascript
- // bad
- props: {
- foo: {
- type: String,
- }
- }
-
- // good
- props: {
- foo: {
- type: String,
- required: false,
- default: 'bar'
- }
- }
- ```
-
-1. Default key should be provided if the prop is not required.
- _Note:_ There are some scenarios where we need to check for the existence of the property.
- On those a default key should not be provided.
-
- ```javascript
- // good
- props: {
- foo: {
- type: String,
- required: false,
- }
- }
-
- // good
- props: {
- foo: {
- type: String,
- required: false,
- default: 'bar'
- }
- }
-
- // good
- props: {
- foo: {
- type: String,
- required: true
- }
- }
- ```
-
-#### Data
-
-1. `data` method should always be a function
-
- ```javascript
- // bad
- data: {
- foo: 'foo'
- }
-
- // good
- data() {
- return {
- foo: 'foo'
- };
- }
- ```
-
-#### Directives
-
-1. Shorthand `@` is preferable over `v-on`
-
- ```javascript
- // bad
- <component v-on:click="eventHandler"/>
-
- // good
- <component @click="eventHandler"/>
- ```
-
-1. Shorthand `:` is preferable over `v-bind`
-
- ```javascript
- // bad
- <component v-bind:class="btn"/>
-
- // good
- <component :class="btn"/>
- ```
-
-1. Shorthand `#` is preferable over `v-slot`
-
- ```javascript
- // bad
- <template v-slot:header></template>
-
- // good
- <template #header></template>
- ```
-
-#### Closing tags
-
-1. Prefer self closing component tags
-
- ```javascript
- // bad
- <component></component>
-
- // good
- <component />
- ```
-
-#### Component usage within templates
-
-1. Prefer a component's kebab-cased name over other styles when using it in a template
-
- ```javascript
- // bad
- <MyComponent />
-
- // good
- <my-component />
- ```
-
-#### Ordering
-
-1. Tag order in `.vue` file
-
- ```
- <script>
- // ...
- </script>
-
- <template>
- // ...
- </template>
-
- // We don't use scoped styles but there are few instances of this
- <style>
- // ...
- </style>
- ```
-
-1. Properties in a Vue Component:
- Check [order of properties in components rule][vue-order].
-
-#### `:key`
-
-When using `v-for` you need to provide a *unique* `:key` attribute for each item.
-
-1. If the elements of the array being iterated have an unique `id` it is advised to use it:
-
- ```html
- <div
- v-for="item in items"
- :key="item.id"
- >
- <!-- content -->
- </div>
- ```
-
-1. When the elements being iterated don't have a unique id, you can use the array index as the `:key` attribute
-
- ```html
- <div
- v-for="(item, index) in items"
- :key="index"
- >
- <!-- content -->
- </div>
- ```
-
-1. When using `v-for` with `template` and there is more than one child element, the `:key` values must be unique. It's advised to use `kebab-case` namespaces.
-
- ```html
- <template v-for="(item, index) in items">
- <span :key="`span-${index}`"></span>
- <button :key="`button-${index}`"></button>
- </template>
- ```
-
-1. When dealing with nested `v-for` use the same guidelines as above.
-
- ```html
- <div
- v-for="item in items"
- :key="item.id"
- >
- <span
- v-for="element in array"
- :key="element.id"
- >
- <!-- content -->
- </span>
- </div>
- ```
-
-Useful links:
-
-1. [`key`](https://vuejs.org/v2/guide/list.html#key)
-1. [Vue Style Guide: Keyed v-for](https://vuejs.org/v2/style-guide/#Keyed-v-for-essential )
-
-#### Vue and Bootstrap
-
-1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
-
- ```javascript
- // bad
- <span
- class="has-tooltip"
- title="Some tooltip text">
- Text
- </span>
-
- // good
- <span
- v-tooltip
- title="Some tooltip text">
- Text
- </span>
- ```
-
-1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js`
-
-1. Don't change `data-original-title`.
-
- ```javascript
- // bad
- <span data-original-title="tooltip text">Foo</span>
-
- // good
- <span title="tooltip text">Foo</span>
-
- $('span').tooltip('_fixTitle');
- ```
-
-### The JavaScript/Vue Accord
-
-The goal of this accord is to make sure we are all on the same page.
-
-1. When writing Vue, you may not use jQuery in your application.
- 1. If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery.
- 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
- 1. If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners.
- 1. We will avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit).
-1. You may query the `window` object 1 time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application.
-1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with it's priority to be determined by maintainers.
-1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests.
-1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
-1. Once you have chosen a centralized state management solution you must use it for your entire application. i.e. Don't mix and match your state management solutions.
-
-## SCSS
-
-- [SCSS](style_guide_scss.md)
-
-[airbnb-js-style-guide]: https://github.com/airbnb/javascript
-[eslintrc]: https://gitlab.com/gitlab-org/gitlab/blob/master/.eslintrc
-[eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue
-[eslint-plugin-vue-rules]: https://github.com/vuejs/eslint-plugin-vue#bulb-rules
-[vue-order]: https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/order-in-components.md
+This document was moved to [another location](style/javascript.md).
diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md
index 1a5b24ce6f4..2b4e6427a18 100644
--- a/doc/development/fe_guide/style_guide_scss.md
+++ b/doc/development/fe_guide/style_guide_scss.md
@@ -1,281 +1,5 @@
-# SCSS styleguide
+---
+redirect_to: 'style/scss.md'
+---
-This style guide recommends best practices for SCSS to make styles easy to read,
-easy to maintain, and performant for the end-user.
-
-## Rules
-
-### Utility Classes
-
-As part of the effort for [cleaning up our CSS and moving our components into GitLab-UI](https://gitlab.com/groups/gitlab-org/-/epics/950)
-led by the [GitLab UI WG](https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/20623) we prefer the use of utility classes over adding new CSS. However, complex CSS can be addressed by adding component classes.
-
-#### Where are utility classes defined?
-
-- [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/)
-- [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) (old)
-- [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) (new)
-
-#### Where should I put new utility classes?
-
-New utility classes should be added to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss). Existing classes include:
-
-| Name | Pattern | Example |
-|------|---------|---------|
-| Background color | `.bg-{variant}-{shade}` | `.bg-warning-400` |
-| Text color | `.text-{variant}-{shade}` | `.text-success-500` |
-| Font size | `.text-{size}` | `.text-2` |
-
-- `{variant}` is one of 'primary', 'secondary', 'success', 'warning', 'error'
-- `{shade}` is one of the shades listed on [colors](https://design.gitlab.com/product-foundations/colors/)
-- `{size}` is a number from 1-6 from our [Type scale](https://design.gitlab.com/product-foundations/typography/)
-
-#### When should I create component classes?
-
-We recommend a "utility-first" approach.
-
-1. Start with utility classes.
-1. If composing utility classes into a component class removes code duplication and encapsulates a clear responsibility, do it.
-
-This encourages an organic growth of component classes and prevents the creation of one-off unreusable classes. Also, the kind of classes that emerge from "utility-first" tend to be design-centered (e.g. `.button`, `.alert`, `.card`) rather than domain-centered (e.g. `.security-report-widget`, `.commit-header-icon`).
-
-Examples of component classes that were created using "utility-first" include:
-
-- [`.circle-icon-container`](https://gitlab.com/gitlab-org/gitlab/blob/579fa8b8ec7eb38d40c96521f517c9dab8c3b97a/app/assets/stylesheets/framework/icons.scss#L85)
-- [`.d-flex-center`](https://gitlab.com/gitlab-org/gitlab/blob/900083d89cd6af391d26ab7922b3f64fa2839bef/app/assets/stylesheets/framework/common.scss#L425)
-
-Inspiration:
-
-- <https://tailwindcss.com/docs/utility-first/>
-- <https://tailwindcss.com/docs/extracting-components/>
-
-### Naming
-
-Filenames should use `snake_case`.
-
-CSS classes should use the `lowercase-hyphenated` format rather than
-`snake_case` or `camelCase`.
-
-```scss
-// Bad
-.class_name {
- color: #fff;
-}
-
-// Bad
-.className {
- color: #fff;
-}
-
-// Good
-.class-name {
- color: #fff;
-}
-```
-
-### Formatting
-
-You should always use a space before a brace, braces should be on the same
-line, each property should each get its own line, and there should be a space
-between the property and its value.
-
-```scss
-// Bad
-.container-item {
- width: 100px; height: 100px;
- margin-top: 0;
-}
-
-// Bad
-.container-item
-{
- width: 100px;
- height: 100px;
- margin-top: 0;
-}
-
-// Bad
-.container-item{
- width:100px;
- height:100px;
- margin-top:0;
-}
-
-// Good
-.container-item {
- width: 100px;
- height: 100px;
- margin-top: 0;
-}
-```
-
-Note that there is an exception for single-line rulesets, although these are
-not typically recommended.
-
-```scss
-p { margin: 0; padding: 0; }
-```
-
-### Colors
-
-HEX (hexadecimal) colors should use shorthand where possible, and should use
-lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3`
-vs. `#e3e3e3`.
-
-```scss
-// Bad
-p {
- color: #ffffff;
-}
-
-// Bad
-p {
- color: #FFFFFF;
-}
-
-// Good
-p {
- color: #fff;
-}
-```
-
-### Indentation
-
-Indentation should always use two spaces for each indentation level.
-
-```scss
-// Bad, four spaces
-p {
- color: #f00;
-}
-
-// Good
-p {
- color: #f00;
-}
-```
-
-### Semicolons
-
-Always include semicolons after every property. When the stylesheets are
-minified, the semicolons will be removed automatically.
-
-```scss
-// Bad
-.container-item {
- width: 100px;
- height: 100px
-}
-
-// Good
-.container-item {
- width: 100px;
- height: 100px;
-}
-```
-
-### Shorthand
-
-The shorthand form should be used for properties that support it.
-
-```scss
-// Bad
-margin: 10px 15px 10px 15px;
-padding: 10px 10px 10px 10px;
-
-// Good
-margin: 10px 15px;
-padding: 10px;
-```
-
-### Zero Units
-
-Omit length units on zero values, they're unnecessary and not including them
-is slightly more performant.
-
-```scss
-// Bad
-.item-with-padding {
- padding: 0px;
-}
-
-// Good
-.item-with-padding {
- padding: 0;
-}
-```
-
-### Selectors with a `js-` Prefix
-
-Do not use any selector prefixed with `js-` for styling purposes. These
-selectors are intended for use only with JavaScript to allow for removal or
-renaming without breaking styling.
-
-### IDs
-
-Don't use ID selectors in CSS.
-
-```scss
-// Bad
-#my-element {
- padding: 0;
-}
-
-// Good
-.my-element {
- padding: 0;
-}
-```
-
-### Variables
-
-Before adding a new variable for a color or a size, guarantee:
-
-- There isn't already one
-- There isn't a similar one we can use instead.
-
-## Linting
-
-We use [SCSS Lint](https://github.com/sds/scss-lint) to check for style guide conformity. It uses the
-ruleset in `.scss-lint.yml`, which is located in the home directory of the
-project.
-
-To check if any warnings will be produced by your changes, you can run `rake
-scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
-catch any warnings.
-
-If the Rake task is throwing warnings you don't understand, SCSS Lint's
-documentation includes [a full list of their linters][scss-lint-documentation](https://github.com/sds/scss-lint/blob/master/lib/scss_lint/linter/README.md).
-
-### Fixing issues
-
-If you want to automate changing a large portion of the codebase to conform to
-the SCSS style guide, you can use [CSSComb][csscomb]. First install
-[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
-CSSComb globally (system-wide). Run it in the GitLab directory with
-`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
-
-Note that this won't fix every problem, but it should fix a majority.
-
-### Ignoring issues
-
-If you want a line or set of lines to be ignored by the linter, you can use
-`// scss-lint:disable RuleName` ([more info](https://github.com/sds/scss-lint#disabling-linters-via-source)):
-
-```scss
-// This lint rule is disabled because it is supported only in Chrome/Safari
-// scss-lint:disable PropertySpelling
-body {
- text-decoration-skip: ink;
-}
-// scss-lint:enable PropertySpelling
-```
-
-Make sure a comment is added on the line above the `disable` rule, otherwise the
-linter will throw a warning. `DisableLinterReason` is enabled to make sure the
-style guide isn't being ignored, and to communicate to others why the style
-guide is ignored in this instance.
-
-[csscomb]: https://github.com/csscomb/csscomb.js
-[node]: https://github.com/nodejs/node
-[npm]: https://www.npmjs.com/
+This document was moved to [another location](style/scss.md).
diff --git a/doc/development/fe_guide/tooling.md b/doc/development/fe_guide/tooling.md
new file mode 100644
index 00000000000..ec5e094cfa6
--- /dev/null
+++ b/doc/development/fe_guide/tooling.md
@@ -0,0 +1,154 @@
+# Tooling
+
+## ESLint
+
+We use ESLint to encapsulate and enforce frontend code standards. Our configuration may be found in the [gitlab-eslint-config](https://gitlab.com/gitlab-org/gitlab-eslint-config) project.
+
+### Disabling ESLint in new files
+
+Do not disable ESLint when creating new files. Existing files may have existing rules
+disabled due to legacy compatibility reasons but they are in the process of being refactored.
+
+Do not disable specific ESLint rules. To avoid introducing technical debt, you may disable the following
+rules only if you are invoking/instantiating existing code modules.
+
+- [`no-new`](https://eslint.org/docs/rules/no-new)
+- [`class-method-use-this`](https://eslint.org/docs/rules/class-methods-use-this)
+
+NOTE: **Note:**
+Disable these rules on a per-line basis. This makes it easier to refactor
+in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`.
+
+### Disabling ESLint for a single violation
+
+If you do need to disable a rule for a single violation, disable it for the smallest amount of code necessary:
+
+```javascript
+// bad
+/* eslint-disable no-new */
+
+import Foo from 'foo';
+
+new Foo();
+
+// better
+import Foo from 'foo';
+
+// eslint-disable-next-line no-new
+new Foo();
+```
+
+### The `no-undef` rule and declaring globals
+
+**Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
+
+When declaring multiple globals, always use one `/* global [name] */` line per variable.
+
+```javascript
+// bad
+/* globals Flash, Cookies, jQuery */
+
+// good
+/* global Flash */
+/* global Cookies */
+/* global jQuery */
+```
+
+## Formatting with Prettier
+
+Our code is automatically formatted with [Prettier](https://prettier.io) to follow our style guides. Prettier is taking care of formatting .js, .vue, and .scss files based on the standard prettier rules. You can find all settings for Prettier in `.prettierrc`.
+
+### Editor
+
+The easiest way to include prettier in your workflow is by setting up your preferred editor (all major editors are supported) accordingly. We suggest setting up prettier to run automatically when each file is saved. Find [here](https://prettier.io/docs/en/editors.html) the best way to set it up in your preferred editor.
+
+Please take care that you only let Prettier format the same file types as the global Yarn script does (.js, .vue, and .scss). In VSCode by example you can easily exclude file formats in your settings file:
+
+```
+ "prettier.disableLanguages": [
+ "json",
+ "markdown"
+ ],
+```
+
+### Yarn Script
+
+The following yarn scripts are available to do global formatting:
+
+```
+yarn prettier-staged-save
+```
+
+Updates all currently staged files (based on `git diff`) with Prettier and saves the needed changes.
+
+```
+yarn prettier-staged
+```
+
+Checks all currently staged files (based on `git diff`) with Prettier and log which files would need manual updating to the console.
+
+```
+yarn prettier-all
+```
+
+Checks all files with Prettier and logs which files need manual updating to the console.
+
+```
+yarn prettier-all-save
+```
+
+Formats all files in the repository with Prettier. (This should only be used to test global rule updates otherwise you would end up with huge MR's).
+
+The source of these Yarn scripts can be found in `/scripts/frontend/prettier.js`.
+
+#### Scripts during Conversion period
+
+```
+node ./scripts/frontend/prettier.js check-all ./vendor/
+```
+
+This will go over all files in a specific folder check it.
+
+```
+node ./scripts/frontend/prettier.js save-all ./vendor/
+```
+
+This will go over all files in a specific folder and save it.
+
+### VSCode Settings
+
+#### Select Prettier as default formatter
+
+To select Prettier as a formatter, add the following properties to your User or Workspace Settings:
+
+```javascript
+{
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[vue]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ }
+}
+```
+
+#### Format on Save
+
+To automatically format your files with Prettier, add the following properties to your User or Workspace Settings:
+
+```javascript
+{
+ "[html]": {
+ "editor.formatOnSave": true
+ },
+ "[javascript]": {
+ "editor.formatOnSave": true
+ },
+ "[vue]": {
+ "editor.formatOnSave": true
+ },
+}
+```
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index e13a7d1e478..96bc89675fe 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -78,7 +78,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
render(createElement) {
return createElement('my-component', {
props: {
- endpoint: this.isLoading,
+ endpoint: this.endpoint,
},
});
},
@@ -179,7 +179,7 @@ Check this [page](vuex.md) for more details.
## Style guide
-Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
+Please refer to the Vue section of our [style guide](style/vue.md)
for best practices while writing your Vue components and templates.
## Testing Vue Components
diff --git a/doc/development/git_object_deduplication.md b/doc/development/git_object_deduplication.md
index 6d9eb90d482..938882ba5a2 100644
--- a/doc/development/git_object_deduplication.md
+++ b/doc/development/git_object_deduplication.md
@@ -111,7 +111,7 @@ are as follows:
contents of the pool repository are a Git clone of the source
project repository.
- The occasion for creating a pool is when an existing eligible
- (public, hashed storage, non-forked) GitLab project gets forked and
+ (non-private, hashed storage, non-forked) GitLab project gets forked and
this project does not belong to a pool repository yet. The fork
parent project becomes the source project of the new pool, and both
the fork parent and the fork child project become members of the new
diff --git a/doc/development/mass_insert.md b/doc/development/mass_insert.md
new file mode 100644
index 00000000000..891ce0db87d
--- /dev/null
+++ b/doc/development/mass_insert.md
@@ -0,0 +1,13 @@
+# Mass Inserting Rails Models
+
+Setting the environment variable [`MASS_INSERT=1`](rake_tasks.md#env-variables)
+when running `rake setup` will create millions of records, but these records
+aren't visible to the `root` user by default.
+
+To make any number of the mass-inserted projects visible to the `root` user, run
+the following snippet in the rails console.
+
+```ruby
+u = User.find(1)
+Project.last(100).each { |p| p.set_create_timestamps && p.add_maintainer(u, current_user: u) } # Change 100 to whatever number of projects you need access to
+```
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 43059d23c79..ec50b1557d4 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -165,6 +165,54 @@ can quickly spiral out of control.
There are some cases where this may be needed. If this is the case this should
be clearly mentioned in the merge request description.
+## Batch process
+
+**Summary:** Iterating a single process to external services (e.g. PostgreSQL, Redis, Object Storage, etc)
+should be executed in a **batch-style** in order to reduce connection overheads.
+
+For fetching rows from various tables in a batch-style, please see [Eager Loading](#eager-loading) section.
+
+### Example: Delete multiple files from Object Storage
+
+When you delete multiple files from object storage (e.g. GCS),
+executing a single REST API call multiple times is a quite expensive
+process. Ideally, this should be done in a batch-style, for example, S3 provides
+[batch deletion API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html),
+so it'd be a good idea to consider such an approach.
+
+The `FastDestroyAll` module might help this situation. It's a
+small framework when you remove a bunch of database rows and its associated data
+in a batch style.
+
+## Timeout
+
+**Summary:** You should set a reasonable timeout when the system invokes HTTP calls
+to external services (e.g. Kubernetes), and it should be executed in Sidekiq, not
+in Puma/Unicorn threads.
+
+Often, GitLab needs to communicate with an external service such as Kubernetes
+clusters. In this case, it's hard to estimate when the external service finishes
+the requested process, for example, if it's a user-owned cluster that is inactive for some reason,
+GitLab might wait for the response forever ([Example](https://gitlab.com/gitlab-org/gitlab/issues/31475)).
+This could result in Puma/Unicorn timeout and should be avoided at all cost.
+
+You should set a reasonable timeout, gracefully handle exceptions and surface the
+errors in UI or logging internally.
+
+Using [`ReactiveCaching`](https://docs.gitlab.com/ee/development/utilities.html#reactivecaching) is one of the best solutions to fetch external data.
+
+## Keep database transaction minimal
+
+**Summary:** You should avoid accessing to external services (e.g. Gitaly) during database
+transactions, otherwise it leads to severe contention problems
+as an open transaction basically blocks the release of a Postgres backend connection.
+
+For keeping transaction as minimal as possible, please consider using `AfterCommitQueue`
+module or `after_commit` AR hook.
+
+Here is [an example](https://gitlab.com/gitlab-org/gitlab/issues/36154#note_247228859)
+that one request to Gitaly instance during transaction triggered a P1 issue.
+
## Eager Loading
**Summary:** always eager load associations when retrieving more than one row.
diff --git a/doc/development/new_fe_guide/index.md b/doc/development/new_fe_guide/index.md
index 227d03bd86f..152ddcdae64 100644
--- a/doc/development/new_fe_guide/index.md
+++ b/doc/development/new_fe_guide/index.md
@@ -15,10 +15,6 @@ Learn about all the dependencies that make up our frontend, including some of ou
Learn about all the internal JavaScript modules that make up our frontend.
-## [Style guides](style/index.md)
-
-Style guides to keep our code consistent.
-
## [Tips](tips.md)
Tips from our frontend team to develop more efficiently and effectively.
diff --git a/doc/development/new_fe_guide/style/html.md b/doc/development/new_fe_guide/style/html.md
index 1445da3f0e1..0b4fce13d90 100644
--- a/doc/development/new_fe_guide/style/html.md
+++ b/doc/development/new_fe_guide/style/html.md
@@ -1,53 +1,5 @@
-# HTML style guide
+---
+redirect_to: '../../fe_guide/style/html.md'
+---
-## Buttons
-
-### Button type
-
-Button tags requires a `type` attribute according to the [W3C HTML specification](https://www.w3.org/TR/2011/WD-html5-20110525/the-button-element.html#dom-button-type).
-
-```html
-// bad
-<button></button>
-
-// good
-<button type="button"></button>
-```
-
-### Button role
-
-If an HTML element has an `onClick` handler but is not a button, it should have `role="button"`. This is [more accessible](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role).
-
-```html
-// bad
-<div onClick="doSomething"></div>
-
-// good
-<div role="button" onClick="doSomething"></div>
-```
-
-## Links
-
-### Blank target
-
-Use `rel="noopener noreferrer"` whenever your links open in a new window, i.e. `target="_blank"`. This prevents a security vulnerability [documented by JitBit](https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/).
-
-```html
-// bad
-<a href="url" target="_blank"></a>
-
-// good
-<a href="url" target="_blank" rel="noopener noreferrer"></a>
-```
-
-### Fake links
-
-**Do not use fake links.** Use a button tag if a link only invokes JavaScript click event handlers, which is more semantic.
-
-```html
-// bad
-<a class="js-do-something" href="#"></a>
-
-// good
-<button class="js-do-something" type="button"></button>
-```
+This document was moved to [another location](../../fe_guide/style/html.md).
diff --git a/doc/development/new_fe_guide/style/index.md b/doc/development/new_fe_guide/style/index.md
index f073dc56f1f..284862a2be9 100644
--- a/doc/development/new_fe_guide/style/index.md
+++ b/doc/development/new_fe_guide/style/index.md
@@ -1,15 +1,5 @@
-# Style guides
+---
+redirect_to: '../../fe_guide/style/index.md'
+---
-## [HTML style guide](html.md)
-
-## [SCSS style guide](scss.md)
-
-## [JavaScript style guide](javascript.md)
-
-## [Vue style guide](vue.md)
-
-## Tooling
-
-## [Prettier](prettier.md)
-
-Our code is automatically formatted with [Prettier](https://prettier.io) to follow our guidelines.
+This document was moved to [another location](../../fe_guide/style/index.md).
diff --git a/doc/development/new_fe_guide/style/javascript.md b/doc/development/new_fe_guide/style/javascript.md
index d31edcb372d..003880c2592 100644
--- a/doc/development/new_fe_guide/style/javascript.md
+++ b/doc/development/new_fe_guide/style/javascript.md
@@ -1,195 +1,5 @@
-# JavaScript style guide
+---
+redirect_to: '../../fe_guide/style/javascript.md'
+---
-We use [Airbnb's JavaScript Style Guide](https://github.com/airbnb/javascript) and it's accompanying
-linter to manage most of our JavaScript style guidelines.
-
-In addition to the style guidelines set by Airbnb, we also have a few specific rules
-listed below.
-
-> **Tip:**
-You can run eslint locally by running `yarn eslint`
-
-## Avoid forEach
-
-Avoid forEach when mutating data. Use `map`, `reduce` or `filter` instead of `forEach`
-when mutating data. This will minimize mutations in functions,
-which aligns with [Airbnb's style guide](https://github.com/airbnb/javascript#testing--for-real).
-
-```javascript
-// bad
-users.forEach((user, index) => {
- user.id = index;
-});
-
-// good
-const usersWithId = users.map((user, index) => {
- return Object.assign({}, user, { id: index });
-});
-```
-
-## Limit number of parameters
-
-If your function or method has more than 3 parameters, use an object as a parameter
-instead.
-
-```javascript
-// bad
-function a(p1, p2, p3) {
- // ...
-};
-
-// good
-function a(p) {
- // ...
-};
-```
-
-## Avoid side effects in constructors
-
-Avoid making asynchronous calls, API requests or DOM manipulations in the `constructor`.
-Move them into separate functions instead. This will make tests easier to write and
-code easier to maintain.
-
-```javascript
-// bad
-class myClass {
- constructor(config) {
- this.config = config;
- axios.get(this.config.endpoint)
- }
-}
-
-// good
-class myClass {
- constructor(config) {
- this.config = config;
- }
-
- makeRequest() {
- axios.get(this.config.endpoint)
- }
-}
-const instance = new myClass();
-instance.makeRequest();
-```
-
-## Avoid classes to handle DOM events
-
-If the only purpose of the class is to bind a DOM event and handle the callback, prefer
-using a function.
-
-```javascript
-// bad
-class myClass {
- constructor(config) {
- this.config = config;
- }
-
- init() {
- document.addEventListener('click', () => {});
- }
-}
-
-// good
-
-const myFunction = () => {
- document.addEventListener('click', () => {
- // handle callback here
- });
-}
-```
-
-## Pass element container to constructor
-
-When your class manipulates the DOM, receive the element container as a parameter.
-This is more maintainable and performant.
-
-```javascript
-// bad
-class a {
- constructor() {
- document.querySelector('.b');
- }
-}
-
-// good
-class a {
- constructor(options) {
- options.container.querySelector('.b');
- }
-}
-```
-
-## Use ParseInt
-
-Use `ParseInt` when converting a numeric string into a number.
-
-```javascript
-// bad
-Number('10')
-
-// good
-parseInt('10', 10);
-```
-
-## CSS Selectors - Use `js-` prefix
-
-If a CSS class is only being used in JavaScript as a reference to the element, prefix
-the class name with `js-`.
-
-```html
-// bad
-<button class="add-user"></button>
-
-// good
-<button class="js-add-user"></button>
-```
-
-## Absolute vs relative paths for modules
-
-Use relative paths if the module you are importing is less than two levels up.
-
-```javascript
-// bad
-import GitLabStyleGuide from '~/guides/GitLabStyleGuide';
-
-// good
-import GitLabStyleGuide from '../GitLabStyleGuide';
-```
-
-If the module you are importing is two or more levels up, use an absolute path instead:
-
-```javascript
-// bad
-import GitLabStyleGuide from '../../../guides/GitLabStyleGuide';
-
-// good
-import GitLabStyleGuide from '~/GitLabStyleGuide';
-```
-
-Additionally, **do not add to global namespace**.
-
-## Do not use `DOMContentLoaded` in non-page modules
-
-Imported modules should act the same each time they are loaded. `DOMContentLoaded`
-events are only allowed on modules loaded in the `/pages/*` directory because those
-are loaded dynamically with webpack.
-
-## Avoid XSS
-
-Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many
-vulnerabilities.
-
-## Disabling ESLint in new files
-
-Do not disable ESLint when creating new files. Existing files may have existing rules
-disabled due to legacy compatibility reasons but they are in the process of being refactored.
-
-Do not disable specific ESLint rules. Due to technical debt, you may disable the following
-rules only if you are invoking/instantiating existing code modules.
-
-- [no-new](https://eslint.org/docs/rules/no-new)
-- [class-method-use-this](https://eslint.org/docs/rules/class-methods-use-this)
-
-> Note: Disable these rules on a per line basis. This makes it easier to refactor
-> in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`.
+This document was moved to [another location](../../fe_guide/style/javascript.md).
diff --git a/doc/development/new_fe_guide/style/prettier.md b/doc/development/new_fe_guide/style/prettier.md
index 17b209d419e..9a95aa96dff 100644
--- a/doc/development/new_fe_guide/style/prettier.md
+++ b/doc/development/new_fe_guide/style/prettier.md
@@ -1,98 +1,5 @@
-# Formatting with Prettier
+---
+redirect_to: '../../fe_guide/tooling.md#formatting-with-prettier'
+---
-Our code is automatically formatted with [Prettier](https://prettier.io) to follow our style guides. Prettier is taking care of formatting .js, .vue, and .scss files based on the standard prettier rules. You can find all settings for Prettier in `.prettierrc`.
-
-## Editor
-
-The easiest way to include prettier in your workflow is by setting up your preferred editor (all major editors are supported) accordingly. We suggest setting up prettier to run automatically when each file is saved. Find [here](https://prettier.io/docs/en/editors.html) the best way to set it up in your preferred editor.
-
-Please take care that you only let Prettier format the same file types as the global Yarn script does (.js, .vue, and .scss). In VSCode by example you can easily exclude file formats in your settings file:
-
-```
- "prettier.disableLanguages": [
- "json",
- "markdown"
- ],
-```
-
-## Yarn Script
-
-The following yarn scripts are available to do global formatting:
-
-```
-yarn prettier-staged-save
-```
-
-Updates all currently staged files (based on `git diff`) with Prettier and saves the needed changes.
-
-```
-yarn prettier-staged
-```
-
-Checks all currently staged files (based on `git diff`) with Prettier and log which files would need manual updating to the console.
-
-```
-yarn prettier-all
-```
-
-Checks all files with Prettier and logs which files need manual updating to the console.
-
-```
-yarn prettier-all-save
-```
-
-Formats all files in the repository with Prettier. (This should only be used to test global rule updates otherwise you would end up with huge MR's).
-
-The source of these Yarn scripts can be found in `/scripts/frontend/prettier.js`.
-
-### Scripts during Conversion period
-
-```
-node ./scripts/frontend/prettier.js check-all ./vendor/
-```
-
-This will go over all files in a specific folder check it.
-
-```
-node ./scripts/frontend/prettier.js save-all ./vendor/
-```
-
-This will go over all files in a specific folder and save it.
-
-## VSCode Settings
-
-### Select Prettier as default formatter
-
-To select Prettier as a formatter, add the following properties to your User or Workspace Settings:
-
-```javascript
-{
- "[html]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
- },
- "[javascript]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
- },
- "[vue]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
- }
-}
-```
-
-### Format on Save
-
-To automatically format your files with Prettier, add the following properties to your User or Workspace Settings:
-
-```javascript
-{
- "[html]": {
- "editor.formatOnSave": true
- },
- "[javascript]": {
- "editor.formatOnSave": true
- },
- "[vue]": {
- "editor.formatOnSave": true
- },
-}
-```
+This document was moved to [another location](../../fe_guide/tooling.md#formatting-with-prettier).
diff --git a/doc/development/new_fe_guide/style/scss.md b/doc/development/new_fe_guide/style/scss.md
deleted file mode 100644
index 6f5e818d7db..00000000000
--- a/doc/development/new_fe_guide/style/scss.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# SCSS style guide
-
-> TODO: Add content
diff --git a/doc/development/new_fe_guide/style/vue.md b/doc/development/new_fe_guide/style/vue.md
deleted file mode 100644
index fd9353e0d3f..00000000000
--- a/doc/development/new_fe_guide/style/vue.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Vue style guide
-
-> TODO: Add content
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 07943c60647..897f69ac0a4 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -23,15 +23,17 @@ The current stages are:
pipeline early (currently used to run Geo tests when the branch name starts
with `geo-`, `geo/`, or ends with `-geo`).
- `test`: This stage includes most of the tests, DB/migration jobs, and static analysis jobs.
+- `post-test`: This stage includes jobs that build reports or gather data from
+ the `test` stage's jobs (e.g. coverage, Knapsack metadata etc.).
- `review-prepare`: This stage includes a job that build the CNG images that are
later used by the (Helm) Review App deployment (see
[Review Apps](testing_guide/review_apps.md) for details).
- `review`: This stage includes jobs that deploy the GitLab and Docs Review Apps.
- `qa`: This stage includes jobs that perform QA tasks against the Review App
that is deployed in the previous stage.
+- `post-qa`: This stage includes jobs that build reports or gather data from
+ the `qa` stage's jobs (e.g. Review App performance report).
- `notification`: This stage includes jobs that sends notifications about pipeline status.
-- `post-test`: This stage includes jobs that build reports or gather data from
- the previous stages' jobs (e.g. coverage, Knapsack metadata etc.).
- `pages`: This stage includes a job that deploys the various reports as
GitLab Pages (e.g. <https://gitlab-org.gitlab.io/gitlab/coverage-ruby/>,
<https://gitlab-org.gitlab.io/gitlab/coverage-javascript/>,
@@ -40,9 +42,9 @@ The current stages are:
## Default image
The default image is currently
-`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
+`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
-It includes Ruby 2.6.3, Go 1.11, Git 2.22, Chrome 73, Node 12, Yarn 1.16,
+It includes Ruby 2.6.3, Go 1.12, Git 2.22, Chrome 73, Node 12, Yarn 1.16,
PostgreSQL 9.6, and Graphics Magick 1.3.33.
The images used in our pipelines are configured in the
diff --git a/doc/development/sql.md b/doc/development/sql.md
index 67ba98e2f31..84ad11effc5 100644
--- a/doc/development/sql.md
+++ b/doc/development/sql.md
@@ -108,15 +108,11 @@ class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- return unless Gitlab::Database.postgresql?
-
execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));'
execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));'
end
def down
- return unless Gitlab::Database.postgresql?
-
remove_index :users, :index_on_users_lower_username
remove_index :users, :index_on_users_lower_email
end
diff --git a/doc/development/testing_guide/end_to_end/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md
index e2a0d267ba1..fc00fcea67e 100644
--- a/doc/development/testing_guide/end_to_end/best_practices.md
+++ b/doc/development/testing_guide/end_to_end/best_practices.md
@@ -54,9 +54,9 @@ In summary:
- **Do**: Split tests across separate files, unless the tests share expensive setup.
- **Don't**: Put new tests in an existing file without considering the impact on parallelization.
-## Limit the use of `before(:all)` hook
+## Limit the use of `before(:all)` and `after` hooks
-Limit the use of `before(:all)` to perform setup tasks with only API calls, non UI operations
+Limit the use of `before(:all)` hook to perform setup tasks with only API calls, non UI operations
or basic UI operations such as login.
We use [`capybara-screenshot`](https://github.com/mattheworiordan/capybara-screenshot) library to automatically save screenshots on failures.
@@ -66,6 +66,10 @@ This library [saves the screenshots in the RSpec's `after` hook](https://github.
Given this fact, we should limit the use of `before(:all)` to only those operations where a screenshot is not
necessary in case of failure and QA logs would be enough for debugging.
+Similarly, the `after` hook should only be used for non-UI operations. Any UI operations in `after` hook in a test file
+would execute before the `after` hook that takes the screenshot. This would result in moving the UI status away from the
+point of failure and so the screenshot would not be captured at the right moment.
+
## Ensure tests do not leave the browser logged in
All QA tests expect to be able to log in at the start of the test.
@@ -74,7 +78,7 @@ That's not possible if a test leaves the browser logged in when it finishes. Nor
For an example see: <https://gitlab.com/gitlab-org/gitlab/issues/34736>
-Ideally, any actions peformed in an `after(:context)` (or [`before(:context)`](#limit-the-use-of-beforeall-hook)) block would be performed via the API. But if it's necessary to do so via the UI (e.g., if API functionality doesn't exist), make sure to log out at the end of the block.
+Ideally, any actions peformed in an `after(:context)` (or [`before(:context)`](#limit-the-use-of-beforeall-and-after-hooks)) block would be performed via the API. But if it's necessary to do so via the UI (e.g., if API functionality doesn't exist), make sure to log out at the end of the block.
```ruby
after(:all) do
diff --git a/doc/development/testing_guide/end_to_end/quick_start_guide.md b/doc/development/testing_guide/end_to_end/quick_start_guide.md
index 2457d8ada5a..16a45bc5ef0 100644
--- a/doc/development/testing_guide/end_to_end/quick_start_guide.md
+++ b/doc/development/testing_guide/end_to_end/quick_start_guide.md
@@ -26,7 +26,17 @@ If you don't exactly understand what we mean by **not everything needs to happen
At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend you check the code coverage of a specific feature before writing end-to-end tests, for both [CE](https://gitlab-org.gitlab.io/gitlab-foss/coverage-ruby/#_AllFiles) and [EE](https://gitlab-org.gitlab.io/gitlab/coverage-ruby/#_AllFiles) projects.
-Sometimes you may notice that there is already good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+Sometimes you may notice that there is already good coverage in lower test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+
+> For analyzing the code coverage, you will also need to understand which application files implement specific functionalities.
+
+#### Some other guidelines are as follows
+
+- Take a look at the [How to test at the correct level?](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md#how-to-test-at-the-correct-level) section of the [Testing levels](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md) document
+
+- Look into the frequency in which such a feature is changed (_Stable features that don't change very often might not be worth covering with end-to-end tests if they're already covered in lower levels_)
+
+- Finally, discuss with the developer(s) involved in developing the feature and the tests themselves, to get their feeling
If after this analysis you still think that end-to-end tests are needed, keep reading.
diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md
index 6b4995aebe2..1413c782c5d 100644
--- a/doc/development/verifying_database_capabilities.md
+++ b/doc/development/verifying_database_capabilities.md
@@ -6,22 +6,16 @@ necessary to add database (version) specific behaviour.
To facilitate this we have the following methods that you can use:
-- `Gitlab::Database.postgresql?`: returns `true` if PostgreSQL is being used.
- You can normally just assume this is the case.
- `Gitlab::Database.version`: returns the PostgreSQL version number as a string
in the format `X.Y.Z`.
This allows you to write code such as:
```ruby
-if Gitlab::Database.postgresql?
- if Gitlab::Database.version.to_f >= 9.6
- run_really_fast_query
- else
- run_fast_query
- end
+if Gitlab::Database.version.to_f >= 9.6
+ run_really_fast_query
else
- run_query
+ run_fast_query
end
```
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 00371057d3c..9e43758a4aa 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -37,11 +37,19 @@ information on how to use this method.
## Dropping Columns
Removing columns is tricky because running GitLab processes may still be using
-the columns. To work around this you will need two separate merge requests and
-releases: one to ignore and then remove the column, and one to remove the ignore
-rule.
+the columns. To work around this safely, you will need three steps in three releases:
-### Step 1: Ignoring The Column
+1. Ignoring the column (release M)
+1. Dropping the column (release M+1)
+1. Removing the ignore rule (release M+2)
+
+The reason we spread this out across three releases is that dropping a column is
+a destructive operation that can't be rolled back easily.
+
+Following this procedure helps us to make sure there are no deployments to GitLab.com
+and upgrade processes for self-hosted installations that lump together any of these steps.
+
+### Step 1: Ignoring the column (release M)
The first step is to ignore the column in the application code. This is
necessary because Rails caches the columns and re-uses this cache in various
@@ -50,18 +58,46 @@ places. This can be done by defining the columns to ignore. For example, to igno
```ruby
class User < ApplicationRecord
- self.ignored_columns += %i[updated_at]
+ include IgnorableColumns
+ ignore_column :updated_at, remove_with: '12.7', remove_after: '2019-12-22'
end
```
-Once added you should create a _post-deployment_ migration that removes the
-column. Both these changes should be submitted in the same merge request.
+Multiple columns can be ignored, too:
+
+```ruby
+ignore_columns %i[updated_at created_at], remove_with: '12.7', remove_after: '2019-12-22'
+```
+
+We require indication of when it is safe to remove the column ignore with:
+
+- `remove_with`: set to a GitLab release typically two releases (M+2) after adding the
+ column ignore.
+- `remove_after`: set to a date after which we consider it safe to remove the column
+ ignore, typically within the development cycle of release M+2.
+
+This information allows us to reason better about column ignores and makes sure we
+don't remove column ignores too early for both regular releases and deployments to GitLab.com. For
+example, this avoids a situation where we deploy a bulk of changes that include both changes
+to ignore the column and subsequently remove the column ignore (which would result in a downtime).
+
+In this example, the change to ignore the column went into release 12.5.
+
+### Step 2: Dropping the column (release M+1)
+
+Continuing our example, dropping the column goes into a _post-deployment_ migration in release 12.6:
+
+```ruby
+ remove_column :user, :updated_at
+```
+
+### Step 3: Removing the ignore rule (release M+2)
-### Step 2: Removing The Ignore Rule
+With the next release, in this example 12.7, we set up another merge request to remove the ignore rule.
+This removes the `ignore_column` line and - if not needed anymore - also the inclusion of `IgnoreableColumns`.
-Once the changes from step 1 have been released & deployed you can set up a
-separate merge request that removes the ignore rule. This merge request can
-simply remove the `self.ignored_columns` line.
+This should only get merged with the release indicated with `remove_with` and once
+the `remove_after` date has passed.
## Renaming Columns
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 23dd67f6891..eaf81b8a1b7 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -166,3 +166,36 @@ via Omnibus, or [restart GitLab] if you installed from source.
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+
+## Troubleshooting
+
+### Error 500 when trying to sign in to GitLab via GitHub Enterprise
+
+Check the [`production.log`](../administration/logs.md#productionlog)
+on your GitLab server to obtain further details. If you are getting the error like
+`Faraday::ConnectionFailed (execution expired)` in the log, there may be a connectivity issue
+between your GitLab instance and GitHub Enterprise. To verify it, [start the rails console](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session)
+and run the commands below replacing <github_url> with the URL of your GitHub Enterprise instance:
+
+```ruby
+uri = URI.parse("https://<github_url>") # replace `GitHub-URL` with the real one here
+http = Net::HTTP.new(uri.host, uri.port)
+http.use_ssl = true
+http.verify_mode = 1
+response = http.request(Net::HTTP::Get.new(uri.request_uri))
+```
+
+If you are getting a similar `execution expired` error, it confirms the theory about the
+network connectivity. In that case, make sure that the GitLab server is able to reach your
+GitHub enterprise instance.
+
+### Signing in using your GitHub account without a pre-existing GitLab account is not allowed
+
+If you're getting the message `Signing in using your GitHub account without a pre-existing
+GitLab account is not allowed. Create a GitLab account first, and then connect it to your
+GitHub account` when signing in, in GitLab:
+
+1. Go to your **Profile > Account**.
+1. Under the "Social sign-in" section, click **Connect** near the GitHub icon.
+
+After that, you should be able to sign in via GitHub successfully.
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 49f2b61a497..672fc5f9355 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -72,7 +72,7 @@ Decision on whether backporting a change will be performed is done at the discre
1. Estimated [severity][severity-labels] of the bug: Highest possible impact to users based on the current definition of severity.
-1. Estimated [priority][priority-labels] of the bug: Immediate impact on all impacted users based on the above estimated severity.
+1. Estimated [priority][priority-definition] of the bug: Immediate impact on all impacted users based on the above estimated severity.
1. Potentially incurring data loss and/or security breach.
@@ -115,16 +115,24 @@ one major version. For example, it is safe to:
- `8.9.0` -> `8.9.7`
- `8.9.0` -> `8.9.1`
- `8.9.2` -> `8.9.6`
+ - `9.5.5` -> `9.5.9`
+ - `10.6.3` -> `10.6.6`
+ - `11.11.1` -> `11.11.8`
+ - `12.0.4` -> `12.0.9`
- Upgrade the minor version:
- `8.9.4` -> `8.12.3`
- `9.2.3` -> `9.5.5`
+ - `10.6.6` -> `10.8.7`
+ - `11.3.4` -> `11.11.8`
Upgrading the major version requires more attention.
We cannot guarantee that upgrading between major versions will be seamless. As previously mentioned, major versions are reserved for backwards incompatible changes.
-
We recommend that you first upgrade to the latest available minor version within
your major version. By doing this, you can address any deprecation messages
that could change behavior in the next major release.
+To ensure background migrations are successful, increment by one minor version during the version jump before installing newer releases.
+
+For example: `11.11.x` -> `12.0.x`
Please see the table below for some examples:
@@ -133,7 +141,7 @@ Please see the table below for some examples:
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
| 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` |
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
-| 12.0.2 | 11.3.4 | `11.3.4` -> `11.11.x` -> `12.0.2` | `11.11.x` is the last version in version `11`
+| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.9` -> `12.5.8` | `11.11.8` is the last version in version `11` |
More information about the release procedures can be found in our
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
@@ -143,4 +151,4 @@ More information about the release procedures can be found in our
[priority-definition]: ../development/contributing/issue_workflow.md#priority-labels
[severity-labels]: ../development/contributing/issue_workflow.html#severity-labels
[managing bugs]: https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md#managing-bugs
-[release/tasks]: https://gitlab.com/gitlab-org/release/tasks/issues
+[release/tasks]: https://gitlab.com/gitlab-org/release/tasks/issues/new?issuable_template=Backporting-request
diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md
index 51b7d7db3e4..9cbb296338a 100644
--- a/doc/security/rack_attack.md
+++ b/doc/security/rack_attack.md
@@ -53,7 +53,8 @@ default['gitlab']['gitlab-rails']['rack_attack_protected_paths'] = [
'/users',
'/users/confirmation',
'/unsubscribes/',
- '/import/github/personal_access_token'
+ '/import/github/personal_access_token',
+ '/admin/session'
]
```
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index f386c5ae42a..674e1193a02 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -651,6 +651,8 @@ procfile exec` to replicate the environment where your application will run.
#### Workers
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30628) in GitLab 12.6, `.gitlab/auto-deploy-values.yaml` will be used by default for Helm upgrades.
+
Some web applications need to run extra deployments for "worker processes". For
example, it is common in a Rails application to have a separate worker process
to run background tasks like sending emails.
@@ -672,13 +674,18 @@ need to:
- Set a CI variable `K8S_SECRET_REDIS_URL`, which the URL of this instance to
ensure it's passed into your deployments.
-Once you have configured your worker to respond to health checks, you will
-need to configure a CI variable `HELM_UPGRADE_EXTRA_ARGS` with the value
-`--values helm-values.yaml`.
+Once you have configured your worker to respond to health checks, run a Sidekiq
+worker for your Rails application. For:
+
+- GitLab 12.6 and later, either:
+ - Add a file named `.gitlab/auto-deploy-values.yaml` to your repository. It will
+ be automatically used if found.
+ - Add a file with a different name or path to the repository, and override the value of the
+ `HELM_UPGRADE_VALUES_FILE` variable with the path and name.
+- GitLab 12.5 and earlier, run the worker with the `--values` parameter that specifies
+ a file in the repository.
-Then you can, for example, run a Sidekiq worker for your Rails application
-by adding a file named `helm-values.yaml` to your repository with the following
-content:
+In any case, the file must contain the following:
```yml
workers:
@@ -983,6 +990,7 @@ applications.
| `CANARY_PRODUCTION_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. Takes precedence over `CANARY_REPLICAS`. Defaults to 1. |
| `CANARY_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md). Defaults to 1. |
| `HELM_RELEASE_NAME` | From GitLab 12.1, allows the `helm` release name to be overridden. Can be used to assign unique release names when deploying multiple projects to a single namespace. |
+| `HELM_UPGRADE_VALUES_FILE` | From GitLab 12.6, allows the `helm upgrade` values file to be overridden. Defaults to `.gitlab/auto-deploy-values.yaml`. |
| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. **Tip:** you can use this variable to [customize the Auto Deploy Helm chart](#custom-helm-chart) by applying custom override values with `--values my-values.yaml`. |
| `INCREMENTAL_ROLLOUT_MODE` | From GitLab 11.4, if present, can be used to enable an [incremental rollout](#incremental-rollout-to-production-premium) of your application for the production environment. Set to `manual` for manual deployment jobs or `timed` for automatic rollout deployments with a 5 minute delay each one. |
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
diff --git a/doc/topics/git/useful_git_commands.md b/doc/topics/git/useful_git_commands.md
index cfe19c89618..abd06b95b1e 100644
--- a/doc/topics/git/useful_git_commands.md
+++ b/doc/topics/git/useful_git_commands.md
@@ -167,6 +167,14 @@ With HTTPS:
GIT_TRACE_PACKET=1 GIT_TRACE=2 GIT_CURL_VERBOSE=1 git clone <url>
```
+### Debugging with Git embedded traces
+
+Git includes a complete set of [traces for debugging Git commands](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_debugging), for example:
+
+- `GIT_TRACE_PERFORMANCE=1`: enables tracing of performance data, showing how long each particular `git` invocation takes.
+- `GIT_TRACE_SETUP=1`: enables tracing of what `git` is discovering about the repository and environment it’s interacting with.
+- `GIT_TRACE_PACKET=1`: enables packet-level tracing for network operations.
+
## Rebasing
### Rebase your branch onto master
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 7314e34666d..b00fc5d90cf 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -12,13 +12,7 @@ You can select the tag in the version dropdown in the top left corner of GitLab
### 0. Backup
-It's useful to make a backup just in case things go south:
-
-```bash
-cd /home/git/gitlab
-
-sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
-```
+It's useful to make a backup just in case things go south. Depending on the installation method, backup commands vary, see the [backing up and restoring GitLab](../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system) documentation.
### 1. Stop server
diff --git a/doc/user/admin_area/appearance.md b/doc/user/admin_area/appearance.md
index 1fea6ab8b02..b9eb9e2a731 100644
--- a/doc/user/admin_area/appearance.md
+++ b/doc/user/admin_area/appearance.md
@@ -46,8 +46,8 @@ of your GitLab instance. These messages will appear on all projects and pages of
instance, including the sign in / sign up page. The default color is white text on
an orange background, but this can be customized by clicking on **Customize colors**.
-Limited [markdown](../markdown.md) is supported, such as bold, italics, and links, for
-example. Other markdown features, including lists, images and quotes, are not supported,
+Limited [Markdown](../markdown.md) is supported, such as bold, italics, and links, for
+example. Other Markdown features, including lists, images and quotes, are not supported,
as the header and footer messages can only be a single line.
![header and footer screenshot](img/appearance_header_footer_v12_3.png)
@@ -61,7 +61,7 @@ to activate it in the GitLab instance.
## Sign in / Sign up pages
You can replace the default message on the sign in / sign up page with your own message
-and logo. You can make full use of [markdown](../markdown.md) in the description:
+and logo. You can make full use of [Markdown](../markdown.md) in the description:
![sign in message screenshot](img/appearance_sign_in_v12_3.png)
@@ -81,7 +81,7 @@ You can add also add a [customized help message](settings/help_page.md) below th
## New project pages
You can add a new project guidelines message to the **New project page** within GitLab.
-You can make full use of [markdown](../markdown.md) in the description:
+You can make full use of [Markdown](../markdown.md) in the description:
![new project message screenshot](img/appearance_new_project_v12_3.png)
diff --git a/doc/user/admin_area/broadcast_messages.md b/doc/user/admin_area/broadcast_messages.md
index b0491499f88..bc51552603d 100644
--- a/doc/user/admin_area/broadcast_messages.md
+++ b/doc/user/admin_area/broadcast_messages.md
@@ -22,6 +22,7 @@ To add a broadcast message:
1. Navigate to the **Admin Area > Messages** page.
1. Add the text for the message to the **Message** field. Markdown and emoji are supported.
1. If required, click the **Customize colors** link to edit the background color and font color of the message.
+1. If required, add a **Target Path** to only show the broadcast message on URLs matching that path. You can use the wildcard character `*` to match multiple URLs, for example `/users/*/issues`.
1. Select a date for the message to start and end.
1. Click the **Add broadcast message** button.
diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
index 103d7ecc573..68767efc72a 100644
--- a/doc/user/admin_area/monitoring/health_check.md
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -103,7 +103,7 @@ This check is being exempt from Rack Attack.
## Liveness
DANGER: **Warning:**
-In Gitlab [12.4](https://about.gitlab.com/upcoming-releases/)
+In GitLab [12.4](https://about.gitlab.com/upcoming-releases/)
the response body of the Liveness check was changed
to match the example below.
diff --git a/doc/user/admin_area/settings/protected_paths.md b/doc/user/admin_area/settings/protected_paths.md
index 21c8d79b138..548352f7bfe 100644
--- a/doc/user/admin_area/settings/protected_paths.md
+++ b/doc/user/admin_area/settings/protected_paths.md
@@ -14,7 +14,8 @@ GitLab protects the following paths with Rack Attack by default:
'/users',
'/users/confirmation',
'/unsubscribes/',
-'/import/github/personal_access_token'
+'/import/github/personal_access_token',
+'/admin/session'
```
GitLab responds with HTTP status code `429` to POST requests at protected paths
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
index 73406fd5037..30f9d4e53a3 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -14,8 +14,11 @@ To access the visibility and access control options:
## Default branch protection
-Branch protection specifies which roles can push to branches and which roles can delete
-branches.
+This global option defines the branch protection that applies to every repository's default branch. [Branch protection](../../project/protected_branches.md) specifies which roles can push to branches and which roles can delete
+branches. In this case _Default_ refers to a repository's default branch, which in most cases is _master_.
+branches. "Default" in this case refers to a repository's default branch, which in most cases would be "master".
+
+This setting applies only to each repositories' default branch. To protect other branches, you must configure branch protection in repository. For details, see [Protected Branches](../../project/protected_branches.md).
To change the default branch protection:
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 931755c6305..2b021264345 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -127,7 +127,7 @@ If you want to whitelist specific vulnerabilities, you'll need to:
[overriding the Container Scanning template](#overriding-the-container-scanning-template) section of this document.
1. Define the whitelisted vulnerabilities in a YAML file named `clair-whitelist.yml` which must use the format described
in the [following whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml).
- 1. Add the `clair-whitelist.yml` file to the git repository of your project
+ 1. Add the `clair-whitelist.yml` file to the Git repository of your project
### Overriding the Container Scanning template
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index d285b5ff585..3a8a81f5f57 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -85,7 +85,7 @@ There are two ways to define the URL to be scanned by DAST:
1. Add it in an `environment_url.txt` file at the root of your project.
This is great for testing in dynamic environments. In order to run DAST against
- an app that is dynamically created during a Gitlab CI pipeline, have the app
+ an app that is dynamically created during a GitLab CI pipeline, have the app
persist its domain in an `environment_url.txt` file, and DAST will
automatically parse that file to find its scan target.
You can see an [example](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
@@ -228,7 +228,7 @@ server {
###### Apache
Apache can also be used as a [reverse proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
-to add the Gitlab-DAST-Permission [header](https://httpd.apache.org/docs/current/mod/mod_headers.html).
+to add the `Gitlab-DAST-Permission` [header](https://httpd.apache.org/docs/current/mod/mod_headers.html).
To do so, add the following lines to `httpd.conf`:
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 4ceefc24e68..a2a0e913136 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -144,6 +144,7 @@ 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. |
| `MAVEN_CLI_OPTS` | List of command line arguments that will be passed to the maven analyzer during the project's build phase (see example for [using private repos](#using-private-maven-repos)). |
+| `BUNDLER_AUDIT_UPDATE_DISABLED` | Disable automatic updates for the `bundler-audit` analyzer (default: `"false"`). Useful if you're running Dependency Scanning in an offline, air-gapped environment.|
### Using private Maven repos
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 7ee1650f698..6498545a4f6 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -429,6 +429,69 @@ administrator to run following command within a Rails console:
Feature.enable(:enable_cluster_application_crossplane)
```
+## Install using GitLab CI (alpha)
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/20822) in GitLab 12.6.
+
+CAUTION: **Warning:**
+This is an _alpha_ feature, and it is subject to change at any time without
+prior notice.
+
+This alternative method allows users to install GitLab-managed
+applications using GitLab CI. It also allows customization of the
+install using Helm `values.yaml` files.
+
+Supported applications:
+
+- [Ingress](#install-ingress-using-gitlab-ci)
+
+### Usage
+
+To install applications using GitLab CI:
+
+1. Connect the cluster to a [cluster management project](management_project.md).
+1. In that project, add a `.gitlab-ci.yml` file with the following content:
+
+ ```yaml
+ include:
+ - template: Managed-Cluster-Applications.gitlab-ci.yml
+ ```
+
+1. Add a `.gitlab/managed-apps/config.yaml` file to define which
+ applications you would like to install. Define the `installed` key as
+ `true` to install the application and `false` to uninstall the
+ application. For example, to install Ingress:
+
+ ```yaml
+ ingress:
+ installed: true
+ ```
+
+1. Optionally, define `.gitlab/managed-apps/<application>/values.yaml` file to
+ customize values for the installed application.
+
+A GitLab CI pipeline will then run on the `master` branch to install the
+applications you have configured.
+
+### Install Ingress using GitLab CI
+
+To install ingress, define the `.gitlab/managed-apps/config.yaml` file
+with:
+
+```yaml
+ingress:
+ installed: true
+```
+
+Ingress will then be installed into the `gitlab-managed-apps` namespace
+of your cluster.
+
+You can customize the installation of Ingress by defining
+`.gitlab/managed-apps/ingress/values.yaml` file in your cluster
+management project. Refer to the
+[chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress)
+for the available configuration options.
+
## Upgrading applications
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24789) in GitLab 11.8.
diff --git a/doc/user/clusters/crossplane.md b/doc/user/clusters/crossplane.md
index 37210b22f6f..ee0bd4c33db 100644
--- a/doc/user/clusters/crossplane.md
+++ b/doc/user/clusters/crossplane.md
@@ -220,9 +220,9 @@ The Resource Classes allow you to define classes of service for a managed servic
The Auto DevOps pipeline can be run with the following options:
-The Environment variables, `AUTO_DEVOPS_POSTGRES_MANAGED` and `AUTO_DEVOPS_POSTGRES_MANAGED_CLASS_SELECTOR` need to be set to provision PostgresQL using Crossplane
+The Environment variables, `AUTO_DEVOPS_POSTGRES_MANAGED` and `AUTO_DEVOPS_POSTGRES_MANAGED_CLASS_SELECTOR` need to be set to provision PostgreSQL using Crossplane
-Alertnatively, the following options can be overridden from the values for the helm chart.
+Alertnatively, the following options can be overridden from the values for the Helm chart.
- `postgres.managed` set to true which will select a default resource class.
The resource class needs to be marked with the annotation
@@ -237,7 +237,7 @@ Alertnatively, the following options can be overridden from the values for the h
The Auto DevOps pipeline should provision a PostgresqlInstance when it runs succesfully.
-Verify creation of the PostgresQL Instance.
+Verify creation of the PostgreSQL Instance.
```sh
kubectl get postgresqlinstance
@@ -286,7 +286,7 @@ serverCACertificateInstance: 41 bytes
serverCACertificateSha1Fingerprint: 40 bytes
```
-## Connect to the PostgresQL instance
+## Connect to the PostgreSQL instance
Follow this [GCP guide](https://cloud.google.com/sql/docs/postgres/connect-kubernetes-engine) if you
would like to connect to the newly provisioned Postgres database instance on CloudSQL.
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index dcb75a19b2a..d4e485d7c32 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -386,7 +386,7 @@ from any device you're logged into.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/18008) in GitLab 11.6.
As a reviewer, you're able to suggest code changes with a simple
-markdown syntax in Merge Request Diff threads. Then, the
+Markdown syntax in Merge Request Diff threads. Then, the
Merge Request author (or other users with appropriate
[permission](../permissions.md)) is able to apply these
suggestions with a click, which will generate a commit in
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index cf98f894e8c..fdf6cb3c7be 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -911,7 +911,7 @@ Here's a sample video:
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#audio).
Similar to videos, link tags for files with an audio extension are automatically converted to
-an audio player. The valid audio extensions are `.mp3`, `.ogg`, and `.wav`:
+an audio player. The valid audio extensions are `.mp3`, `.oga`, `.ogg`, `.spx`, and `.wav`:
```md
Here's a sample audio clip:
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index 980346c3764..2366d1ccc0d 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -27,7 +27,7 @@ get familiar with the package naming convention.
## Authenticating to the GitLab Conan Repository
-You will need to generate a [personal access token](../../../user/profile/personal_access_tokens.md) for repository authentication.
+You will need to generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
Now you can run conan commands using your token.
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 8ed10c09891..70ff26b28b2 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -37,7 +37,7 @@ credentials do not work.
### Authenticating with a personal access token
To authenticate with a [personal access token](../../profile/personal_access_tokens.md),
-add a corresponding section to your
+set the scope to `api` and add a corresponding section to your
[`settings.xml`](https://maven.apache.org/settings.html) file:
```xml
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index edf299d6fdb..e611e4d99fb 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -54,7 +54,7 @@ If a project is private or you want to upload an NPM package to GitLab,
credentials will need to be provided for authentication. Support is available for [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow) or [personal access tokens](../../profile/personal_access_tokens.md).
CAUTION: **2FA is only supported with personal access tokens:**
-If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers. Standard OAuth tokens won't be able to authenticate to the GitLab NPM Registry.
+If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers with the scope set to `api`. Standard OAuth tokens won't be able to authenticate to the GitLab NPM Registry.
### Authenticating with an OAuth token
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 70660e5e22f..a73f465bb22 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -5,7 +5,7 @@ description: 'Understand and explore the user permission levels in GitLab, and w
# Permissions
Users have different abilities depending on the access level they have in a
-particular group or project. If a user is both in a group's project and the
+particular group or project. If a user is both in a project's group and the
project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will
diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md
index fe2eeebdb99..8a70799f5a0 100644
--- a/doc/user/profile/active_sessions.md
+++ b/doc/user/profile/active_sessions.md
@@ -18,6 +18,9 @@ review the sessions, and revoke any you don't recognize.
![Active sessions list](img/active_sessions_list.png)
+CAUTION: **Caution:**
+It is currently possible to have 100 active sessions at once. If the number of active sessions exceed 100, the oldest ones will be deleted.
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md
index a195360aa12..0b74f1e73eb 100644
--- a/doc/user/project/clusters/serverless/aws.md
+++ b/doc/user/project/clusters/serverless/aws.md
@@ -94,7 +94,7 @@ The `events` declaration will create a AWS API Gateway `GET` endpoint to receive
You can read more about the available properties and additional configuration possibilities of the Serverless Framework here: <https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/>
-### Crafting the .gitlab-ci.yml file
+### Crafting the `.gitlab-ci.yml` file
In a `.gitlab-ci.yml` file in the root of your project, place the following code:
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
index b14d7f821bb..98e9188ed9b 100644
--- a/doc/user/project/deploy_boards.md
+++ b/doc/user/project/deploy_boards.md
@@ -14,7 +14,7 @@ With Deploy Boards you can gain more insight into deploys with benefits such as:
- Following a deploy from the start, not just when it's done
- Watching the rollout of a build across multiple servers
-- Finer state detail (Waiting, Deploying, Finished, Unknown)
+- Finer state detail (Succeeded, Running, Failed, Pending, Unknown)
- See [Canary Deployments](canary_deployments.md)
Here's an example of a Deploy Board of the production environment.
diff --git a/doc/user/project/img/service_desk_disabled.png b/doc/user/project/img/service_desk_disabled.png
index edae7e76986..ba11b508682 100644
--- a/doc/user/project/img/service_desk_disabled.png
+++ b/doc/user/project/img/service_desk_disabled.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_enabled.png b/doc/user/project/img/service_desk_enabled.png
index 9c143ff58cd..aee2b53a680 100644
--- a/doc/user/project/img/service_desk_enabled.png
+++ b/doc/user/project/img/service_desk_enabled.png
Binary files differ
diff --git a/doc/user/project/integrations/github.md b/doc/user/project/integrations/github.md
index 9d16748085c..b8b073af2a4 100644
--- a/doc/user/project/integrations/github.md
+++ b/doc/user/project/integrations/github.md
@@ -33,6 +33,9 @@ with `repo:status` access granted:
1. Optionally uncheck **Static status check names** checkbox to disable static status check names.
1. Save or optionally click "Test Settings".
+Once the integration is configured, see [Pipelines for external pull requests](../../../ci/ci_cd_for_external_repos/#pipelines-for-external-pull-requests)
+to configure pipelines to run for open pull requests.
+
#### Static / dynamic status check names
> - Introduced in GitLab 11.5: using static status check names as opt-in option.
diff --git a/doc/user/project/integrations/img/unify_circuit_configuration.png b/doc/user/project/integrations/img/unify_circuit_configuration.png
new file mode 100644
index 00000000000..285d4f92030
--- /dev/null
+++ b/doc/user/project/integrations/img/unify_circuit_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 315039f82b3..f960c59850d 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -54,6 +54,7 @@ Click on the service links to see further configuration instructions and details
| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
+| [Unify Circuit](unify_circuit.md) | Receive events notifications in Unify Circuit |
| [YouTrack](youtrack.md) | YouTrack issue tracker |
## Push hooks limit
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index d3d4afefb59..25ba0a47a4e 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -481,7 +481,7 @@ It is possible to display metrics charts within [GitLab Flavored Markdown](../..
NOTE: **Note:**
Requires [Kubernetes](prometheus_library/kubernetes.md) metrics.
-To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`.
+To display a metric chart, include a link of the form `https://<root_url>/<project>/-/environments/<environment_id>/metrics`.
A single chart may also be embedded. You can generate a link to the chart via the dropdown located on the right side of the chart:
@@ -520,7 +520,7 @@ The sharing dialog within Grafana provides the link, as highlighted below.
NOTE: **Note:**
For this embed to display correctly the Grafana instance must be available to the target user, either as a public dashboard or on the same network.
-Copy the link and add an image tag as [inline HTML](../../markdown.md#inline-html) in your markdown. You may tweak the query parameters as required. For instance, removing the `&from=` and `&to=` parameters will give you a live chart. Here is example markup for a live chart from GitLab's public dashboard:
+Copy the link and add an image tag as [inline HTML](../../markdown.md#inline-html) in your Markdown. You may tweak the query parameters as required. For instance, removing the `&from=` and `&to=` parameters will give you a live chart. Here is example markup for a live chart from GitLab's public dashboard:
```html
<img src="https://dashboards.gitlab.com/render/d-solo/RZmbBr7mk/gitlab-triage?orgId=1&refresh=30s&var-env=gprd&var-environment=gprd&var-prometheus=prometheus-01-inf-gprd&var-prometheus_app=prometheus-app-01-inf-gprd&var-backend=All&var-type=All&var-stage=main&panelId=1247&width=1000&height=300"/>
@@ -534,7 +534,7 @@ This will render like so:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31376) in GitLab 12.5.
-Each project can support integration with one Grafana instance. This configuration allows a user to copy a link to a panel in Grafana, then paste it into a GitLab markdown field. The chart will be rendered in the GitLab chart format.
+Each project can support integration with one Grafana instance. This configuration allows a user to copy a link to a panel in Grafana, then paste it into a GitLab Markdown field. The chart will be rendered in the GitLab chart format.
Prerequisites for embedding from a Grafana instance:
@@ -562,7 +562,7 @@ Prerequisites for embedding from a Grafana instance:
1. If your Prometheus queries use Grafana's custom template variables, ensure that "Template variables" and "Current time range" options are toggled to **On**. Of Grafana global template variables, only `$__interval`, `$__from`, and `$__to` are currently supported.
![Grafana Sharing Dialog](img/grafana_sharing_dialog_v12_5.png)
1. Click **Copy** to copy the URL to the clipboard.
-1. In GitLab, paste the URL into a markdown field and save. The chart will take a few moments to render.
+1. In GitLab, paste the URL into a Markdown field and save. The chart will take a few moments to render.
![GitLab Rendered Grafana Panel](img/rendered_grafana_embed_v12_5.png)
## Troubleshooting
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index 3c6919561fd..cf46456ca42 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -22,7 +22,7 @@ NGINX server metrics are detected, which tracks the pages and content directly s
To get started with NGINX monitoring, you should first enable the [VTS statistics](https://github.com/vozlt/nginx-module-vts)) module for your NGINX server. This will capture and display statistics in an HTML readable form. Next, you should install and configure the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter) which parses these statistics and translates them into a Prometheus monitoring endpoint.
-If you are using NGINX as your Kubernetes ingress, GitLab will [automatically detect](nginx_ingress.md) the metrics once enabled in 0.9.0 and later releases.
+If you are using NGINX as your Kubernetes Ingress, GitLab will [automatically detect](nginx_ingress.md) the metrics once enabled in 0.9.0 and later releases.
## Specifying the Environment label
diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md
new file mode 100644
index 00000000000..e357afb9224
--- /dev/null
+++ b/doc/user/project/integrations/unify_circuit.md
@@ -0,0 +1,27 @@
+# Unify Circuit service
+
+The Unify Circuit service sends notifications from GitLab to the conversation for which the webhook was created.
+
+## On Unify Circuit
+
+1. Open the conversation in which you want to see the notifications.
+1. From the conversation menu, select **Configure Webhooks**.
+1. Click **ADD WEBHOOK** and fill in the name of the bot that will post the messages. Optionally define avatar.
+1. Click **SAVE** and copy the **Webhook URL** of your webhook.
+
+For more information, see the [Unify Circuit documentation for configuring incoming webhooks](https://www.circuit.com/unifyportalfaqdetail?articleId=164448).
+
+## On GitLab
+
+When you have the **Webhook URL** for your Unify Circuit conversation webhook, you can set up the GitLab service.
+
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) in your project's settings, i.e. **Project > Settings > Integrations**.
+1. Select the **Unify Circuit** project service to configure it.
+1. Check the **Active** checkbox to turn on the service.
+1. Check the checkboxes corresponding to the GitLab events you want to receive in Unify Circuit.
+1. Paste the **Webhook URL** that you copied from the Unify Circuit configuration step.
+1. Configure the remaining options and click `Save changes`.
+
+Your Unify Circuit conversation will now start receiving GitLab event notifications as configured.
+
+![Unify Circuit configuration](img/unify_circuit_configuration.png)
diff --git a/doc/user/project/merge_requests/img/merge_request_tab_position_v12_6.png b/doc/user/project/merge_requests/img/merge_request_tab_position_v12_6.png
new file mode 100644
index 00000000000..9284e58f456
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_tab_position_v12_6.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 1ca8c882ac7..203a2949243 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -40,6 +40,40 @@ B. Consider you're a web developer writing a webpage for your company's website:
1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
1. Your production team [cherry picks](cherry_pick_changes.md) the merge commit into production
+## Overview
+
+Merge requests (aka "MRs") display a great deal of information about the changes proposed.
+The body of an MR contains its description, along with its widget (displaying information
+about CI/CD pipelines, when present), followed by the discussion threads of the people
+collaborating with that MR.
+
+MRs also contain navigation tabs from which you can see the discussion happening on the thread,
+the list of commits, the list of pipelines and jobs, the code changes and inline code reviews.
+
+## Merge request navigation tabs at the top
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33813) in GitLab 12.6. This positioning is experimental.
+
+So far, the navigation tabs present in merge requests to display **Discussion**,
+**Commits**, **Pipelines**, and **Changes** were located after the merge request
+widget.
+
+To facilitate this navigation without having to scroll up and down through the page
+to find these tabs, based on user feedback, we are experimenting with a new positioning
+of these tabs. They are now located at the top of the merge request, with a new
+**Overview** tab, containing the description of the merge request followed by the
+widget. Next to **Overview**, you can find **Pipelines**, **Commits**, and **Changes**.
+
+![Merge request tab positions](img/merge_request_tab_position_v12_6.png)
+
+Please note this change is currently behind a feature flag which is enabled by default. For
+self-managed instances, it can be disabled through the Rails console by a GitLab
+administrator with the following command:
+
+```ruby
+Feature.disable(:mr_tabs_position)
+```
+
## Creating merge requests
While making changes to files in the `master` branch of a repository is possible, it is not
diff --git a/doc/user/project/merge_requests/merge_request_dependencies.md b/doc/user/project/merge_requests/merge_request_dependencies.md
index c99e6663093..8b8aa30e75a 100644
--- a/doc/user/project/merge_requests/merge_request_dependencies.md
+++ b/doc/user/project/merge_requests/merge_request_dependencies.md
@@ -9,6 +9,8 @@ type: reference, concepts
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/merge_requests/17291) from
"Cross-project dependencies" to "Merge Requests dependencies" in
[GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
+> - Intra-project MR dependencies were [introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16799)
+in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
Merge request dependencies allows a required order of merging
between merge requests to be expressed. If a merge request "depends on" another,
@@ -20,10 +22,6 @@ only enforced for the dependent merge request. A merge request in a **CORE** or
**STARTER** project can be a dependency of a **PREMIUM** merge request, but not
vice-versa.
-NOTE: **Note:**
-A merge request can only depend on merge requests in a different project. Two
-merge requests in the same project cannot depend on each other.
-
## Use cases
- Ensure changes to a library are merged before changes to a project that
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index 6630179ea47..e1ac8b2183c 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -69,7 +69,7 @@ For example, to that on merge requests there is always a passing job even though
```yaml
enable_merge:
- only: merge_requests
+ only: [merge_requests]
script:
- echo true
```
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index bb3ce3d69fa..eacc1fd12dc 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -105,14 +105,17 @@ The milestone view shows the title and description.
There are also tabs below these that show the following:
-- Issues
- Shows all issues assigned to the milestone. These are displayed in three columns: Unstarted issues, ongoing issues, and completed issues.
-- Merge requests
- Shows all merge requests assigned to the milestone. These are displayed in four columns: Work in progress merge requests, waiting for merge, rejected, and closed.
-- Participants
- Shows all assignees of issues assigned to the milestone.
-- Labels
- Shows all labels that are used in issues assigned to the milestone.
+- **Issues**: Shows all issues assigned to the milestone. These are displayed in three columns named:
+ - Unstarted Issues (open and unassigned)
+ - Ongoing Issues (open and assigned)
+ - Completed Issues (closed)
+- **Merge Requests**: Shows all merge requests assigned to the milestone. These are displayed in four columns named:
+ - Work in progress (open and unassigned)
+ - Waiting for merge (open and unassigned)
+ - Rejected (closed)
+ - Merged
+- **Participants**: Shows all assignees of issues assigned to the milestone.
+- **Labels**: Shows all labels that are used in issues assigned to the milestone.
### Project Burndown Charts **(STARTER)**
diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md
index 04eda026bc3..ed244a622b2 100644
--- a/doc/user/project/operations/error_tracking.md
+++ b/doc/user/project/operations/error_tracking.md
@@ -32,7 +32,7 @@ GitLab provides an easy way to connect Sentry to your project:
1. Click **Save changes** for the changes to take effect.
1. You can now visit **Operations > Error Tracking** in your project's sidebar to [view a list](#error-tracking-list) of Sentry errors.
-### Enabling Gitlab issues links
+### Enabling GitLab issues links
You may also want to enable Sentry's GitLab integration by following the steps in the [Sentry documentation](https://docs.sentry.io/workflow/integrations/global-integrations/#gitlab)
diff --git a/doc/user/project/pages/getting_started/fork_sample_project.md b/doc/user/project/pages/getting_started/fork_sample_project.md
index ac1a29ca2a0..9c58189e984 100644
--- a/doc/user/project/pages/getting_started/fork_sample_project.md
+++ b/doc/user/project/pages/getting_started/fork_sample_project.md
@@ -41,7 +41,7 @@ forking (copying) a [sample project from the most popular Static Site Generators
and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
site to the server.
1. Once the pipeline has finished successfully, find the link to visit your
- website from your project's **Settings > Pages**. It can take aproximatelly
+ website from your project's **Settings > Pages**. It can take approximately
30 minutes to be deployed.
You can also take some **optional** further steps:
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index 9b7b20be98f..54e59a318c3 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -32,7 +32,7 @@ your users to quickly scan the differences between each one you publish.
NOTE: **Note:**
[Git's tagging messages](https://git-scm.com/book/en/v2/Git-Basics-Tagging) and
-Release descriptions are unrelated. Description supports [markdown](../../markdown.md).
+Release descriptions are unrelated. Description supports [Markdown](../../markdown.md).
### Release assets
@@ -126,7 +126,7 @@ following modal window will be then displayed, from which you can select **New r
## Add release notes to Git tags
You can add release notes to any Git tag using the notes feature. Release notes
-behave like any other markdown form in GitLab so you can write text and
+behave like any other Markdown form in GitLab so you can write text and
drag and drop files to it. Release notes are stored in GitLab's database.
There are several ways to add release notes:
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 5a6e011220c..cb7fe63db6f 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -34,7 +34,7 @@ You can either use the user interface (UI), or connect your local computer
with GitLab [through the command line](../../../gitlab-basics/command-line-commands.md#start-working-on-your-project).
To configure [GitLab CI/CD](../../../ci/README.md) to build, test, and deploy
-you code, add a file called [`.gitlab-ci.yml`](../../../ci/quick_start/README.md)
+your code, add a file called [`.gitlab-ci.yml`](../../../ci/quick_start/README.md)
to your repository's root.
**From the user interface:**
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 944720c3863..ef7cfdb8d8e 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -152,7 +152,7 @@ SHA. From a project's files page, choose **New tag** from the dropdown.
Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
would like to create this new tag. You can optionally add a message and
-release notes. The release notes section supports markdown format and you can
+release notes. The release notes section supports Markdown format and you can
also upload an attachment. Click **Create tag** and you will be taken to the tag
list page.
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index 0ca34c4ed02..3a354e1a4f4 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -57,7 +57,7 @@ you can skip the step 1 below; you only need to enable it per project.
support [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing).
1. Navigate to your project's **Settings** and scroll down to the **Service Desk**
section.
-1. If you have the correct access and an Premium license,
+1. If you have the correct access and a Premium license,
you will see an option to set up Service Desk:
![Activate Service Desk option](img/service_desk_disabled.png)
@@ -79,6 +79,9 @@ you can skip the step 1 below; you only need to enable it per project.
However the older format is still supported, allowing existing aliases
or contacts to continue working._
+1. If you have [templates](description_templates.md) in your repository, then you can optionally
+ select one of these templates from the dropdown to append it to all Service Desk issues.
+
1. Service Desk is now enabled for this project! You should be able to access it from your project's navigation **Issue submenu**:
![Service Desk Navigation Item](img/service_desk_nav_item.png)
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 2dc507901d0..810bd2a5937 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -18,7 +18,7 @@ Adjust your project's name, description, avatar, [default branch](../repository/
![general project settings](img/general_settings.png)
-The project description also partially supports [standard markdown](../../markdown.md#standard-markdown-and-extensions-in-gitlab). You can use [emphasis](../../markdown.md#emphasis), [links](../../markdown.md#links), and [line-breaks](../../markdown.md#line-breaks) to add more context to the project description.
+The project description also partially supports [standard Markdown](../../markdown.md#standard-markdown-and-extensions-in-gitlab). You can use [emphasis](../../markdown.md#emphasis), [links](../../markdown.md#links), and [line-breaks](../../markdown.md#line-breaks) to add more context to the project description.
### Sharing and permissions
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 9836255932a..ce129f0663b 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,11 +1,5 @@
---
-comments: false
+redirect_to: '../README.md'
---
-# Workflow (Deprecated)
-
-This page was deprecated, with all content previously stored under the `/workflow` path moved
-to other locations in the documentation site, organized by topic. You can use the search
-box to find the content you are looking for, browse the main [GitLab Documentation page](../README.md),
-or view the [issue that deprecated this page](https://gitlab.com/gitlab-org/gitlab/issues/32940)
-for more details.
+This document was moved to [another location](../README.md).
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index f97200f20b9..84d1d8a0aac 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -17,16 +17,19 @@ module API
end
params do
use :pagination
- optional :order_by, type: String, values: %w[id iid created_at updated_at ref], default: 'id', desc: 'Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref`'
- optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
+ optional :order_by, type: String, values: DeploymentsFinder::ALLOWED_SORT_VALUES, default: DeploymentsFinder::DEFAULT_SORT_VALUE, desc: 'Return deployments ordered by specified value'
+ optional :sort, type: String, values: DeploymentsFinder::ALLOWED_SORT_DIRECTIONS, default: DeploymentsFinder::DEFAULT_SORT_DIRECTION, desc: 'Sort by asc (ascending) or desc (descending)'
+ optional :updated_after, type: DateTime, desc: 'Return deployments updated after the specified date'
+ optional :updated_before, type: DateTime, desc: 'Return deployments updated before the specified date'
end
- # rubocop: disable CodeReuse/ActiveRecord
+
get ':id/deployments' do
authorize! :read_deployment, user_project
- present paginate(user_project.deployments.order(params[:order_by] => params[:sort])), with: Entities::Deployment
+ deployments = DeploymentsFinder.new(user_project, params).execute
+
+ present paginate(deployments), with: Entities::Deployment
end
- # rubocop: enable CodeReuse/ActiveRecord
desc 'Gets a specific deployment' do
detail 'This feature was introduced in GitLab 8.11.'
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 22c88a7f7f0..d16786e47b2 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -532,7 +532,7 @@ module API
class PersonalSnippet < Snippet
expose :raw_url do |snippet|
- Gitlab::UrlBuilder.build(snippet) + "/raw"
+ Gitlab::UrlBuilder.build(snippet, raw: true)
end
end
diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb
index 7551ca50a7f..32a15381f27 100644
--- a/lib/api/helpers/common_helpers.rb
+++ b/lib/api/helpers/common_helpers.rb
@@ -15,3 +15,5 @@ module API
end
end
end
+
+API::Helpers::CommonHelpers.prepend_if_ee('EE::API::Helpers::CommonHelpers')
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 74051f2d69f..b77be6edcf7 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -134,6 +134,12 @@ module API
},
{
required: false,
+ name: :confidential_note_events,
+ type: Boolean,
+ desc: 'Enable notifications for confidential_note_events'
+ },
+ {
+ required: false,
name: :tag_push_events,
type: Boolean,
desc: 'Enable notifications for tag_push_events'
@@ -696,7 +702,16 @@ module API
type: String,
desc: 'The password of the user'
}
- ]
+ ],
+ 'unify-circuit' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Unify Circuit webhook. e.g. https://circuit.com/rest/v2/webhooks/incoming/…'
+ },
+ chat_notification_events
+ ].flatten
}
end
diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb
index 907c6e1b605..64ab5db4fcd 100644
--- a/lib/gitaly/server.rb
+++ b/lib/gitaly/server.rb
@@ -2,6 +2,8 @@
module Gitaly
class Server
+ SHA_VERSION_REGEX = /\A\d+\.\d+\.\d+-\d+-g([a-f0-9]{8})\z/.freeze
+
class << self
def all
Gitlab.config.repositories.storages.keys.map { |s| Gitaly::Server.new(s) }
@@ -30,9 +32,10 @@ module Gitaly
info.git_version
end
- def up_to_date?
- server_version == Gitlab::GitalyClient.expected_server_version
+ def expected_version?
+ server_version == Gitlab::GitalyClient.expected_server_version || matches_sha?
end
+ alias_method :up_to_date?, :expected_version?
def read_writeable?
readable? && writeable?
@@ -62,6 +65,13 @@ module Gitaly
@storage_status ||= info.storage_statuses.find { |s| s.storage_name == storage }
end
+ def matches_sha?
+ match = server_version.match(SHA_VERSION_REGEX)
+ return false unless match
+
+ Gitlab::GitalyClient.expected_server_version.start_with?(match[1])
+ end
+
def info
@info ||=
begin
diff --git a/lib/gitlab/background_migration/migrate_legacy_artifacts.rb b/lib/gitlab/background_migration/migrate_legacy_artifacts.rb
index 4377ec2987c..23d99274232 100644
--- a/lib/gitlab/background_migration/migrate_legacy_artifacts.rb
+++ b/lib/gitlab/background_migration/migrate_legacy_artifacts.rb
@@ -123,8 +123,6 @@ module Gitlab
end
def add_missing_db_timezone
- return '' unless Gitlab::Database.postgresql?
-
'at time zone \'UTC\''
end
end
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
index cbda3808b86..0373a12ab69 100644
--- a/lib/gitlab/ci/ansi2json/converter.rb
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -37,16 +37,13 @@ module Gitlab
flush_current_line
- # TODO: replace OpenStruct with a better type
- # https://gitlab.com/gitlab-org/gitlab/issues/34305
- OpenStruct.new(
+ Gitlab::Ci::Ansi2json::Result.new(
lines: @lines,
state: @state.encode,
append: append,
truncated: truncated,
offset: start_offset,
- size: stream.tell - start_offset,
- total: stream.size
+ stream: stream
)
end
diff --git a/lib/gitlab/ci/ansi2json/result.rb b/lib/gitlab/ci/ansi2json/result.rb
new file mode 100644
index 00000000000..9b573882a52
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/result.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# Convertion result object class
+module Gitlab
+ module Ci
+ module Ansi2json
+ class Result
+ attr_reader :lines, :state, :append, :truncated, :offset, :size, :total
+
+ def initialize(lines:, state:, append:, truncated:, offset:, stream:)
+ @lines = lines
+ @state = state
+ @append = append
+ @truncated = truncated
+ @offset = offset
+ @size = stream.tell - offset
+ @total = stream.size
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
index b0a79950667..426f0238f9d 100644
--- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
@@ -15,15 +15,15 @@ performance:
fi
- 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/10-5/index.js
+ - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.0/index.js
- mkdir sitespeed-results
- |
if [ -f .gitlab-urls.txt ]
then
sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
+ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:11.2.0 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
else
- 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"
+ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:11.2.0 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
fi
- mv sitespeed-results/data/performance.json performance.json
artifacts:
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 738be44d5f4..cb45c12c2b0 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.7.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.0"
review:
extends: .auto-deploy
diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
new file mode 100644
index 00000000000..a6b09e38ce8
--- /dev/null
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -0,0 +1,14 @@
+apply:
+ stage: deploy
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.1.0"
+ environment:
+ name: production
+ variables:
+ TILLER_NAMESPACE: gitlab-managed-apps
+ GITLAB_MANAGED_APPS_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/config.yaml
+ INGRESS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/ingress/values.yaml
+ script:
+ - gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml
+ only:
+ refs:
+ - master
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index 424dff4e1ae..3f9d61a79f5 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -52,6 +52,7 @@ dependency_scanning:
PIP_INDEX_URL \
PIP_EXTRA_INDEX_URL \
MAVEN_CLI_OPTS \
+ BUNDLER_AUDIT_UPDATE_DISABLED \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
index eced181e966..e6097ae322e 100644
--- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml
@@ -11,7 +11,7 @@ performance:
image: docker:git
variables:
URL: https://example.com
- SITESPEED_VERSION: 6.3.1
+ SITESPEED_VERSION: 11.2.0
SITESPEED_OPTIONS: ''
services:
- docker:stable-dind
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 7ea7565f758..03bde611451 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -155,6 +155,7 @@ module Gitlab
# column - The name of the column to create the foreign key on.
# on_delete - The action to perform when associated data is removed,
# defaults to "CASCADE".
+ # name - The name of the foreign key.
#
# rubocop:disable Gitlab/RailsLogger
def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, name: nil)
@@ -164,25 +165,31 @@ module Gitlab
raise 'add_concurrent_foreign_key can not be run inside a transaction'
end
- on_delete = 'SET NULL' if on_delete == :nullify
+ options = {
+ column: column,
+ on_delete: on_delete,
+ name: name.presence || concurrent_foreign_key_name(source, column)
+ }
- key_name = name || concurrent_foreign_key_name(source, column)
-
- unless foreign_key_exists?(source, target, column: column)
- Rails.logger.warn "Foreign key not created because it exists already " \
+ if foreign_key_exists?(source, target, options)
+ warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \
- "source: #{source}, target: #{target}, column: #{column}"
+ "source: #{source}, target: #{target}, column: #{options[:column]}, "\
+ "name: #{options[:name]}, on_delete: #{options[:on_delete]}"
+ Rails.logger.warn warning_message
+ else
# Using NOT VALID allows us to create a key without immediately
# validating it. This means we keep the ALTER TABLE lock only for a
# short period of time. The key _is_ enforced for any newly created
# data.
+
execute <<-EOF.strip_heredoc
ALTER TABLE #{source}
- ADD CONSTRAINT #{key_name}
- FOREIGN KEY (#{column})
+ ADD CONSTRAINT #{options[:name]}
+ FOREIGN KEY (#{options[:column]})
REFERENCES #{target} (id)
- #{on_delete ? "ON DELETE #{on_delete.upcase}" : ''}
+ #{on_delete_statement(options[:on_delete])}
NOT VALID;
EOF
end
@@ -193,18 +200,15 @@ module Gitlab
#
# Note this is a no-op in case the constraint is VALID already
disable_statement_timeout do
- execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
+ execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{options[:name]};")
end
end
# rubocop:enable Gitlab/RailsLogger
- def foreign_key_exists?(source, target = nil, column: nil)
- foreign_keys(source).any? do |key|
- if column
- key.options[:column].to_s == column.to_s
- else
- key.to_table.to_s == target.to_s
- end
+ def foreign_key_exists?(source, target = nil, **options)
+ foreign_keys(source).any? do |foreign_key|
+ tables_match?(target.to_s, foreign_key.to_table.to_s) &&
+ options_match?(foreign_key.options, options)
end
end
@@ -1050,6 +1054,21 @@ into similar problems in the future (e.g. when new tables are created).
private
+ def tables_match?(target_table, foreign_key_table)
+ target_table.blank? || foreign_key_table == target_table
+ end
+
+ def options_match?(foreign_key_options, options)
+ options.all? { |k, v| foreign_key_options[k].to_s == v.to_s }
+ end
+
+ def on_delete_statement(on_delete)
+ return '' if on_delete.blank?
+ return 'ON DELETE SET NULL' if on_delete == :nullify
+
+ "ON DELETE #{on_delete.upcase}"
+ end
+
def create_column_from(table, old, new, type: nil)
old_col = column_for(table, old)
new_type = type || old_col.type
diff --git a/lib/gitlab/database/obsolete_ignored_columns.rb b/lib/gitlab/database/obsolete_ignored_columns.rb
index 6266b6a4b65..ad5473f1b74 100644
--- a/lib/gitlab/database/obsolete_ignored_columns.rb
+++ b/lib/gitlab/database/obsolete_ignored_columns.rb
@@ -23,8 +23,15 @@ module Gitlab
private
def ignored_columns_safe_to_remove_for(klass)
- ignored = klass.ignored_columns.map(&:to_s)
+ ignores = ignored_and_not_present(klass).each_with_object({}) do |col, h|
+ h[col] = klass.ignored_columns_details[col.to_sym]
+ end
+
+ ignores.select { |_, i| i&.safe_to_remove? }
+ end
+ def ignored_and_not_present(klass)
+ ignored = klass.ignored_columns.map(&:to_s)
return [] if ignored.empty?
schema = klass.connection.schema_cache.columns_hash(klass.table_name)
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index c4288ca6408..3d661111f13 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -4,12 +4,16 @@ module Gitlab
module Diff
module FileCollection
class MergeRequestDiff < MergeRequestDiffBase
+ include Gitlab::Utils::StrongMemoize
+
def diff_files
- diff_files = super
+ strong_memoize(:diff_files) do
+ diff_files = super
- diff_files.each { |diff_file| cache.decorate(diff_file) }
+ diff_files.each { |diff_file| cache.decorate(diff_file) }
- diff_files
+ diff_files
+ end
end
override :write_cache
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index efddda0ec65..17d9cf08367 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -23,6 +23,10 @@ module Gitlab
'issue_notes'
),
Gitlab::EtagCaching::Router::Route.new(
+ %r(#{RESERVED_WORDS_PREFIX}/noteable/merge_request/\d+/notes\z),
+ 'merge_request_notes'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
%r(#{RESERVED_WORDS_PREFIX}/issues/\d+/realtime_changes\z),
'issue_title'
),
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c401f96b5c1..c89b8f563dc 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -85,13 +85,16 @@ module Gitlab
# we do not care if we process array or hash
data_hashes = [data_hashes] unless data_hashes.is_a?(Array)
+ relation_index = 0
+
# consume and remove objects from memory
while data_hash = data_hashes.shift
- process_project_relation_item!(relation_key, relation_definition, data_hash)
+ process_project_relation_item!(relation_key, relation_definition, relation_index, data_hash)
+ relation_index += 1
end
end
- def process_project_relation_item!(relation_key, relation_definition, data_hash)
+ def process_project_relation_item!(relation_key, relation_definition, relation_index, data_hash)
relation_object = build_relation(relation_key, relation_definition, data_hash)
return unless relation_object
return if group_model?(relation_object)
@@ -100,6 +103,25 @@ module Gitlab
relation_object.save!
save_id_mapping(relation_key, data_hash, relation_object)
+ rescue => e
+ # re-raise if not enabled
+ raise e unless Feature.enabled?(:import_graceful_failures, @project.group)
+
+ log_import_failure(relation_key, relation_index, e)
+ end
+
+ def log_import_failure(relation_key, relation_index, exception)
+ Gitlab::Sentry.track_acceptable_exception(exception,
+ extra: { project_id: @project.id, relation_key: relation_key, relation_index: relation_index })
+
+ ImportFailure.create(
+ project: @project,
+ relation_key: relation_key,
+ relation_index: relation_index,
+ exception_class: exception.class.to_s,
+ exception_message: exception.message.truncate(255),
+ correlation_id_value: Labkit::Correlation::CorrelationId.current_id
+ )
end
# Older, serialized CI pipeline exports may only have a
diff --git a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
index cddd4f18cc3..805283b0f93 100644
--- a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
+++ b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb
@@ -5,7 +5,7 @@ module Gitlab
module PerformanceBar
module RedisAdapterWhenPeekEnabled
def save(request_id)
- super if ::Gitlab::PerformanceBar.enabled_for_request? && request_id.present?
+ super if ::Gitlab::PerformanceBar.enabled_for_request?
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb
index fbc3cf2e049..c9c5c6da3bf 100644
--- a/lib/gitlab/slash_commands/presenters/access.rb
+++ b/lib/gitlab/slash_commands/presenters/access.rb
@@ -34,8 +34,8 @@ module Gitlab
def authorize
message =
- if @resource
- ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
+ if resource
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{resource})."
else
":sweat_smile: Couldn't identify you, nor can I authorize you!"
end
diff --git a/lib/gitlab/slash_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb
index 73814aa180f..54d74ed3998 100644
--- a/lib/gitlab/slash_commands/presenters/base.rb
+++ b/lib/gitlab/slash_commands/presenters/base.rb
@@ -18,6 +18,8 @@ module Gitlab
private
+ attr_reader :resource
+
def header_with_list(header, items)
message = [header]
@@ -67,12 +69,51 @@ module Gitlab
def resource_url
url_for(
[
- @resource.project.namespace.becomes(Namespace),
- @resource.project,
- @resource
+ resource.project.namespace.becomes(Namespace),
+ resource.project,
+ resource
]
)
end
+
+ def project_link
+ "[#{project.full_name}](#{project.web_url})"
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+
+ def response_message(custom_pretext: pretext)
+ {
+ attachments: [
+ {
+ title: "#{issue.title} · #{issue.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: fallback_message,
+ pretext: custom_pretext,
+ text: text,
+ color: color(resource),
+ fields: fields,
+ mrkdwn_in: fields_with_markdown
+ }
+ ]
+ }
+ end
+
+ def pretext
+ ''
+ end
+
+ def text
+ ''
+ end
+
+ def fields_with_markdown
+ %i(title pretext fields)
+ end
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb
index 0be31e234b5..4bc05d1f318 100644
--- a/lib/gitlab/slash_commands/presenters/issue_base.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_base.rb
@@ -42,17 +42,11 @@ module Gitlab
]
end
- def project_link
- "[#{project.full_name}](#{project.web_url})"
- end
-
- def author_profile_link
- "[#{author.to_reference}](#{url_for(author)})"
- end
-
private
attr_reader :resource
+
+ alias_method :issue, :resource
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/issue_close.rb b/lib/gitlab/slash_commands/presenters/issue_close.rb
index b3f24f4296a..f8d9af2c3c6 100644
--- a/lib/gitlab/slash_commands/presenters/issue_close.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_close.rb
@@ -7,43 +7,25 @@ module Gitlab
include Presenters::IssueBase
def present
- if @resource.confidential?
- ephemeral_response(close_issue)
+ if resource.confidential?
+ ephemeral_response(response_message)
else
- in_channel_response(close_issue)
+ in_channel_response(response_message)
end
end
def already_closed
- ephemeral_response(text: "Issue #{@resource.to_reference} is already closed.")
+ ephemeral_response(text: "Issue #{resource.to_reference} is already closed.")
end
private
- def close_issue
- {
- attachments: [
- {
- title: "#{@resource.title} · #{@resource.to_reference}",
- title_link: resource_url,
- author_name: author.name,
- author_icon: author.avatar_url,
- fallback: "Closed issue #{@resource.to_reference}: #{@resource.title}",
- pretext: pretext,
- color: color(@resource),
- fields: fields,
- mrkdwn_in: [
- :title,
- :pretext,
- :fields
- ]
- }
- ]
- }
+ def fallback_message
+ "Closed issue #{issue.to_reference}: #{issue.title}"
end
def pretext
- "I closed an issue on #{author_profile_link}'s behalf: *#{@resource.to_reference}* in #{project_link}"
+ "I closed an issue on #{author_profile_link}'s behalf: *#{issue.to_reference}* in #{project_link}"
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/issue_comment.rb b/lib/gitlab/slash_commands/presenters/issue_comment.rb
index cce71e23b21..6ad56dd3682 100644
--- a/lib/gitlab/slash_commands/presenters/issue_comment.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_comment.rb
@@ -7,31 +7,13 @@ module Gitlab
include Presenters::NoteBase
def present
- ephemeral_response(new_note)
+ ephemeral_response(response_message)
end
private
- def new_note
- {
- attachments: [
- {
- title: "#{issue.title} · #{issue.to_reference}",
- title_link: resource_url,
- author_name: author.name,
- author_icon: author.avatar_url,
- fallback: "New comment on #{issue.to_reference}: #{issue.title}",
- pretext: pretext,
- color: color,
- fields: fields,
- mrkdwn_in: [
- :title,
- :pretext,
- :fields
- ]
- }
- ]
- }
+ def fallback_message
+ "New comment on #{issue.to_reference}: #{issue.title}"
end
def pretext
diff --git a/lib/gitlab/slash_commands/presenters/issue_move.rb b/lib/gitlab/slash_commands/presenters/issue_move.rb
index 01f2025ee10..5b9ca89c063 100644
--- a/lib/gitlab/slash_commands/presenters/issue_move.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_move.rb
@@ -19,30 +19,15 @@ module Gitlab
private
def moved_issue(old_issue)
- {
- attachments: [
- {
- title: "#{@resource.title} · #{@resource.to_reference}",
- title_link: resource_url,
- author_name: author.name,
- author_icon: author.avatar_url,
- fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
- pretext: pretext(old_issue),
- color: color(@resource),
- fields: fields,
- mrkdwn_in: [
- :title,
- :pretext,
- :text,
- :fields
- ]
- }
- ]
- }
+ response_message(custom_pretext: custom_pretext(old_issue))
end
- def pretext(old_issue)
- "Moved issue *#{issue_link(old_issue)}* to *#{issue_link(@resource)}*"
+ def fallback_message
+ "Issue #{issue.to_reference}: #{issue.title}"
+ end
+
+ def custom_pretext(old_issue)
+ "Moved issue *#{issue_link(old_issue)}* to *#{issue_link(issue)}*"
end
def issue_link(issue)
diff --git a/lib/gitlab/slash_commands/presenters/issue_new.rb b/lib/gitlab/slash_commands/presenters/issue_new.rb
index 1424a4ac381..552456f5836 100644
--- a/lib/gitlab/slash_commands/presenters/issue_new.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_new.rb
@@ -7,36 +7,21 @@ module Gitlab
include Presenters::IssueBase
def present
- in_channel_response(new_issue)
+ in_channel_response(response_message)
end
private
- def new_issue
- {
- attachments: [
- {
- title: "#{@resource.title} · #{@resource.to_reference}",
- title_link: resource_url,
- author_name: author.name,
- author_icon: author.avatar_url,
- fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
- pretext: pretext,
- color: color(@resource),
- fields: fields,
- mrkdwn_in: [
- :title,
- :pretext,
- :text,
- :fields
- ]
- }
- ]
- }
+ def fallback_message
+ "New issue #{issue.to_reference}: #{issue.title}"
+ end
+
+ def fields_with_markdown
+ %i(title pretext text fields)
end
def pretext
- "I created an issue on #{author_profile_link}'s behalf: *#{@resource.to_reference}* in #{project_link}"
+ "I created an issue on #{author_profile_link}'s behalf: *#{issue.to_reference}* in #{project_link}"
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/issue_search.rb b/lib/gitlab/slash_commands/presenters/issue_search.rb
index 0d497efec0e..fffa082baac 100644
--- a/lib/gitlab/slash_commands/presenters/issue_search.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_search.rb
@@ -7,12 +7,12 @@ module Gitlab
include Presenters::IssueBase
def present
- text = if @resource.count >= 5
+ text = if resource.count >= 5
"Here are the first 5 issues I found:"
- elsif @resource.one?
+ elsif resource.one?
"Here is the only issue I found:"
else
- "Here are the #{@resource.count} issues I found:"
+ "Here are the #{resource.count} issues I found:"
end
ephemeral_response(text: text, attachments: attachments)
@@ -21,7 +21,7 @@ module Gitlab
private
def attachments
- @resource.map do |issue|
+ resource.map do |issue|
url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
{
@@ -37,7 +37,7 @@ module Gitlab
end
def project
- @project ||= @resource.first.project
+ @project ||= resource.first.project
end
def namespace
diff --git a/lib/gitlab/slash_commands/presenters/issue_show.rb b/lib/gitlab/slash_commands/presenters/issue_show.rb
index 5a2c79a928e..448381b64ed 100644
--- a/lib/gitlab/slash_commands/presenters/issue_show.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_show.rb
@@ -7,55 +7,36 @@ module Gitlab
include Presenters::IssueBase
def present
- if @resource.confidential?
- ephemeral_response(show_issue)
+ if resource.confidential?
+ ephemeral_response(response_message)
else
- in_channel_response(show_issue)
+ in_channel_response(response_message)
end
end
private
- def show_issue
- {
- attachments: [
- {
- title: "#{@resource.title} · #{@resource.to_reference}",
- title_link: resource_url,
- author_name: author.name,
- author_icon: author.avatar_url,
- fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
- pretext: pretext,
- text: text,
- color: color(@resource),
- fields: fields,
- mrkdwn_in: [
- :pretext,
- :text,
- :fields
- ]
- }
- ]
- }
+ def fallback_message
+ "Issue #{resource.to_reference}: #{resource.title}"
end
def text
- message = ["**#{status_text(@resource)}**"]
+ message = ["**#{status_text(resource)}**"]
- if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+ if resource.upvotes.zero? && resource.downvotes.zero? && resource.user_notes_count.zero?
return message.join
end
message << " · "
- message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
- message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
- message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+ message << ":+1: #{resource.upvotes} " unless resource.upvotes.zero?
+ message << ":-1: #{resource.downvotes} " unless resource.downvotes.zero?
+ message << ":speech_balloon: #{resource.user_notes_count}" unless resource.user_notes_count.zero?
message.join
end
def pretext
- "Issue *#{@resource.to_reference}* from #{project.full_name}"
+ "Issue *#{resource.to_reference}* from #{project.full_name}"
end
end
end
diff --git a/lib/gitlab/slash_commands/presenters/note_base.rb b/lib/gitlab/slash_commands/presenters/note_base.rb
index 7758fc740de..71a9b99d0fd 100644
--- a/lib/gitlab/slash_commands/presenters/note_base.rb
+++ b/lib/gitlab/slash_commands/presenters/note_base.rb
@@ -6,7 +6,7 @@ module Gitlab
module NoteBase
GREEN = '#38ae67'
- def color
+ def color(_)
GREEN
end
@@ -18,18 +18,10 @@ module Gitlab
issue.project
end
- def project_link
- "[#{project.full_name}](#{project.web_url})"
- end
-
def author
resource.author
end
- def author_profile_link
- "[#{author.to_reference}](#{url_for(author)})"
- end
-
def fields
[
{
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index f6edbfced7f..ca7ae429986 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -35,7 +35,7 @@ module Gitlab
query.length >= min_chars_for_partial_matching
end
- # column - The column name to search in.
+ # column - The column name / Arel column to search in.
# query - The text to search for.
# lower_exact_match - When set to `true` we'll fall back to using
# `LOWER(column) = query` instead of using `ILIKE`.
@@ -43,19 +43,21 @@ module Gitlab
query = query.squish
return unless query.present?
+ arel_column = column.is_a?(Arel::Attributes::Attribute) ? column : arel_table[column]
+
words = select_fuzzy_words(query, use_minimum_char_limit: use_minimum_char_limit)
if words.any?
- words.map { |word| arel_table[column].matches(to_pattern(word, use_minimum_char_limit: use_minimum_char_limit)) }.reduce(:and)
+ words.map { |word| arel_column.matches(to_pattern(word, use_minimum_char_limit: use_minimum_char_limit)) }.reduce(:and)
else
# No words of at least 3 chars, but we can search for an exact
# case insensitive match with the query as a whole
if lower_exact_match
Arel::Nodes::NamedFunction
- .new('LOWER', [arel_table[column]])
+ .new('LOWER', [arel_column])
.eq(query)
else
- arel_table[column].matches(sanitize_sql_like(query))
+ arel_column.matches(sanitize_sql_like(query))
end
end
end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 038067eeae4..5a9eef8288f 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -6,10 +6,10 @@ module Gitlab
include GitlabRoutingHelper
include ActionView::RecordIdentifier
- attr_reader :object
+ attr_reader :object, :opts
- def self.build(object)
- new(object).url
+ def self.build(object, opts = {})
+ new(object, opts).url
end
def url
@@ -24,10 +24,8 @@ module Gitlab
note_url
when WikiPage
wiki_page_url
- when ProjectSnippet
- project_snippet_url(object.project, object)
when Snippet
- snippet_url(object)
+ opts[:raw].present? ? raw_snippet_url(object) : snippet_url(object)
when Milestone
milestone_url(object)
when ::Ci::Build
@@ -41,8 +39,9 @@ module Gitlab
private
- def initialize(object)
+ def initialize(object, opts = {})
@object = object
+ @opts = opts
end
def commit_url(opts = {})
@@ -66,13 +65,7 @@ module Gitlab
merge_request_url(object.noteable, anchor: dom_id(object))
elsif object.for_snippet?
- snippet = object.noteable
-
- if snippet.is_a?(PersonalSnippet)
- snippet_url(snippet, anchor: dom_id(object))
- else
- project_snippet_url(snippet.project, snippet, anchor: dom_id(object))
- end
+ snippet_url(object.noteable, anchor: dom_id(object))
end
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index e2787744f09..a1d462ea9f5 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -115,6 +115,18 @@ module Gitlab
end
end
+ def visibility_level_decreased?
+ return false unless visibility_level_previous_changes
+
+ before, after = visibility_level_previous_changes
+
+ before && after && after < before
+ end
+
+ def visibility_level_previous_changes
+ previous_changes[:visibility_level]
+ end
+
def private?
visibility_level_value == PRIVATE
end
diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb
index cc899bf9374..db21c0b013b 100644
--- a/lib/quality/kubernetes_client.rb
+++ b/lib/quality/kubernetes_client.rb
@@ -4,6 +4,7 @@ require_relative '../gitlab/popen' unless defined?(Gitlab::Popen)
module Quality
class KubernetesClient
+ RESOURCE_LIST = 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd'
CommandFailedError = Class.new(StandardError)
attr_reader :namespace
@@ -13,6 +14,13 @@ module Quality
end
def cleanup(release_name:, wait: true)
+ delete_by_selector(release_name: release_name, wait: wait)
+ delete_by_matching_name(release_name: release_name)
+ end
+
+ private
+
+ def delete_by_selector(release_name:, wait:)
selector = case release_name
when String
%(-l release="#{release_name}")
@@ -23,9 +31,9 @@ module Quality
end
command = [
- %(--namespace "#{namespace}"),
'delete',
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa',
+ RESOURCE_LIST,
+ %(--namespace "#{namespace}"),
'--now',
'--ignore-not-found',
'--include-uninitialized',
@@ -36,7 +44,29 @@ module Quality
run_command(command)
end
- private
+ def delete_by_matching_name(release_name:)
+ resource_names = raw_resource_names
+ command = [
+ 'delete',
+ %(--namespace "#{namespace}")
+ ]
+
+ Array(release_name).each do |release|
+ resource_names
+ .select { |resource_name| resource_name.include?(release) }
+ .each { |matching_resource| run_command(command + [matching_resource]) }
+ end
+ end
+
+ def raw_resource_names
+ command = [
+ 'get',
+ RESOURCE_LIST,
+ %(--namespace "#{namespace}"),
+ '-o custom-columns=NAME:.metadata.name'
+ ]
+ run_command(command).lines.map(&:strip)
+ end
def run_command(command)
final_command = ['kubectl', *command].join(' ')
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index b7822adf6ed..90a8096cc2b 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -36,6 +36,10 @@ module Quality
workers
elastic_integration
],
+ migration: %w[
+ migrations
+ lib/gitlab/background_migration
+ ],
integration: %w[
controllers
mailers
@@ -62,6 +66,10 @@ module Quality
def level_for(file_path)
case file_path
+ # Detect migration first since some background migration tests are under
+ # spec/lib/gitlab/background_migration and tests under spec/lib are unit by default
+ when regexp(:migration)
+ :migration
when regexp(:unit)
:unit
when regexp(:integration)
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 3aa3eb45d85..708ace53f5b 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -34,12 +34,18 @@ module Sentry
end
def list_issues(**keyword_args)
- issues = get_issues(keyword_args)
+ response = get_issues(keyword_args)
+
+ issues = response[:issues]
+ pagination = response[:pagination]
validate_size(issues)
handle_mapping_exceptions do
- map_to_errors(issues)
+ {
+ issues: map_to_errors(issues),
+ pagination: pagination
+ }
end
end
@@ -83,36 +89,40 @@ module Sentry
end
def get_issues(**keyword_args)
- http_get(
+ response = http_get(
issues_api_url,
query: list_issue_sentry_query(keyword_args)
)
+
+ {
+ issues: response[:body],
+ pagination: Sentry::PaginationParser.parse(response[:headers])
+ }
end
- def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '')
+ def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '', cursor: nil)
unless SENTRY_API_SORT_VALUE_MAP.key?(sort)
raise BadRequestError, 'Invalid value for sort param'
end
- query_params = {
+ {
query: "is:#{issue_status} #{search_term}".strip,
limit: limit,
- sort: SENTRY_API_SORT_VALUE_MAP[sort]
- }
-
- query_params.compact
+ sort: SENTRY_API_SORT_VALUE_MAP[sort],
+ cursor: cursor
+ }.compact
end
def get_issue(issue_id:)
- http_get(issue_api_url(issue_id))
+ http_get(issue_api_url(issue_id))[:body]
end
def get_issue_latest_event(issue_id:)
- http_get(issue_latest_event_api_url(issue_id))
+ http_get(issue_latest_event_api_url(issue_id))[:body]
end
def get_projects
- http_get(projects_api_url)
+ http_get(projects_api_url)[:body]
end
def handle_request_exceptions
@@ -138,7 +148,7 @@ module Sentry
raise_error "Sentry response status code: #{response.code}"
end
- response.parsed_response
+ { body: response.parsed_response, headers: response.headers }
end
def raise_error(message)
diff --git a/lib/sentry/pagination_parser.rb b/lib/sentry/pagination_parser.rb
new file mode 100644
index 00000000000..fa9c1dd8694
--- /dev/null
+++ b/lib/sentry/pagination_parser.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Sentry
+ module PaginationParser
+ PATTERN = /rel=\"(?<direction>\w+)\";\sresults=\"(?<results>\w+)\";\scursor=\"(?<cursor>.+)\"/.freeze
+
+ def self.parse(headers)
+ links = headers['link'].to_s.split(',')
+
+ links.map { |link| parse_link(link) }.compact.to_h
+ end
+
+ def self.parse_link(link)
+ match = link.match(PATTERN)
+
+ return unless match
+ return if match['results'] != "true"
+
+ [match['direction'], { 'cursor' => match['cursor'] }]
+ end
+ private_class_method :parse_link
+ end
+end
diff --git a/lib/tasks/db_obsolete_ignored_columns.rake b/lib/tasks/db_obsolete_ignored_columns.rake
index 184e407f28c..00f60231f4f 100644
--- a/lib/tasks/db_obsolete_ignored_columns.rake
+++ b/lib/tasks/db_obsolete_ignored_columns.rake
@@ -8,7 +8,10 @@ task 'db:obsolete_ignored_columns' => :environment do
puts 'The following `ignored_columns` are obsolete and can be removed:'
list.each do |name, ignored_columns|
- puts "- #{name}: #{ignored_columns.join(', ')}"
+ puts "#{name}:"
+ ignored_columns.each do |column, removal|
+ puts " - #{column.ljust(30)} Remove after #{removal.remove_after} with #{removal.remove_with}"
+ end
end
puts <<~TEXT
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 0a0ee7b4bfa..63f5d7f2740 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -92,7 +92,7 @@ namespace :gitlab do
lookup_key_count = redis.scard(key)
session_ids = ActiveSession.session_ids_for_user(user_id)
- entries = ActiveSession.raw_active_session_entries(session_ids, user_id)
+ entries = ActiveSession.raw_active_session_entries(redis, session_ids, user_id)
session_ids_and_entries = session_ids.zip(entries)
inactive_session_ids = session_ids_and_entries.map do |session_id, session|
diff --git a/lib/tasks/gitlab/import_export/import.rake b/lib/tasks/gitlab/import_export/import.rake
new file mode 100644
index 00000000000..d15749d8285
--- /dev/null
+++ b/lib/tasks/gitlab/import_export/import.rake
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+# Import large project archives
+#
+# This task:
+# 1. Disables ObjectStorage for archive upload
+# 2. Performs Sidekiq job synchronously
+#
+# @example
+# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz]"
+#
+require 'sidekiq/testing'
+
+namespace :gitlab do
+ namespace :import_export do
+ desc 'EXPERIMENTAL | Import large project archives'
+ task :import, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args|
+ warn_user_is_not_gitlab
+
+ GitlabProjectImport.new(
+ namespace_path: args.namespace_path,
+ project_path: args.project_path,
+ username: args.username,
+ file_path: args.archive_path
+ ).import
+ end
+ end
+end
+
+class GitlabProjectImport
+ def initialize(opts)
+ @project_path = opts.fetch(:project_path)
+ @file_path = opts.fetch(:file_path)
+ @namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path))
+ @current_user = User.find_by_username(opts.fetch(:username))
+ end
+
+ def import
+ show_import_start_message
+
+ run_isolated_sidekiq_job
+
+ show_import_failures_count
+
+ if @project&.import_state&.last_error
+ puts "ERROR: #{@project.import_state.last_error}"
+ exit 1
+ elsif @project.errors.any?
+ puts "ERROR: #{@project.errors.full_messages.join(', ')}"
+ exit 1
+ else
+ puts 'Done!'
+ end
+ rescue StandardError => e
+ puts "Exception: #{e.message}"
+ puts e.backtrace
+ exit 1
+ end
+
+ private
+
+ # We want to ensure that all Sidekiq jobs are executed
+ # synchronously as part of that process.
+ # This ensures that all expensive operations do not escape
+ # to general Sidekiq clusters/nodes.
+ def run_isolated_sidekiq_job
+ Sidekiq::Testing.fake! do
+ @project = create_project
+
+ execute_sidekiq_job
+
+ true
+ end
+ end
+
+ def create_project
+ # We are disabling ObjectStorage for `import`
+ # as it is too slow to handle big archives:
+ # 1. DB transaction timeouts on upload
+ # 2. Download of archive before unpacking
+ disable_upload_object_storage do
+ service = Projects::GitlabProjectsImportService.new(
+ @current_user,
+ {
+ namespace_id: @namespace.id,
+ path: @project_path,
+ file: File.open(@file_path)
+ }
+ )
+
+ service.execute
+ end
+ end
+
+ def execute_sidekiq_job
+ Sidekiq::Worker.drain_all
+ end
+
+ def disable_upload_object_storage
+ overwrite_uploads_setting('background_upload', false) do
+ overwrite_uploads_setting('direct_upload', false) do
+ yield
+ end
+ end
+ end
+
+ def overwrite_uploads_setting(key, value)
+ old_value = Settings.uploads.object_store[key]
+ Settings.uploads.object_store[key] = value
+
+ yield
+
+ ensure
+ Settings.uploads.object_store[key] = old_value
+ end
+
+ def full_path
+ "#{@namespace.full_path}/#{@project_path}"
+ end
+
+ def show_import_start_message
+ puts "Importing GitLab export: #{@file_path} into GitLab" \
+ " #{full_path}" \
+ " as #{@current_user.name}"
+ end
+
+ def show_import_failures_count
+ return unless @project.import_failures.exists?
+
+ puts "Total number of not imported relations: #{@project.import_failures.count}"
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0518b7450b3..405254c6278 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1702,6 +1702,9 @@ msgstr ""
msgid "An error occurred while saving the approval settings"
msgstr ""
+msgid "An error occurred while saving the template. Please check if the template exists."
+msgstr ""
+
msgid "An error occurred while subscribing to notifications."
msgstr ""
@@ -2171,6 +2174,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one of group_id or project_id must be specified"
+msgstr ""
+
msgid "Attach a file"
msgstr ""
@@ -3358,6 +3364,9 @@ msgstr ""
msgid "Clear"
msgstr ""
+msgid "Clear chart filters"
+msgstr ""
+
msgid "Clear input"
msgstr ""
@@ -3586,9 +3595,6 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
-msgid "ClusterIntegration|Choose a prefix to be used for your namespaces. Defaults to your project path."
-msgstr ""
-
msgid "ClusterIntegration|Choose the %{startLink}security group %{externalLinkIcon} %{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets."
msgstr ""
@@ -4123,6 +4129,9 @@ msgstr ""
msgid "ClusterIntegration|Service token is required."
msgstr ""
+msgid "ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared."
+msgstr ""
+
msgid "ClusterIntegration|Show"
msgstr ""
@@ -5272,12 +5281,21 @@ msgstr ""
msgid "CycleAnalytics|All stages"
msgstr ""
+msgid "CycleAnalytics|Date"
+msgstr ""
+
+msgid "CycleAnalytics|Days to completion"
+msgstr ""
+
msgid "CycleAnalytics|No stages selected"
msgstr ""
msgid "CycleAnalytics|Stages"
msgstr ""
+msgid "CycleAnalytics|Total days to completion"
+msgstr ""
+
msgid "CycleAnalytics|group dropdown filter"
msgstr ""
@@ -9635,6 +9653,9 @@ msgstr ""
msgid "Issue"
msgstr ""
+msgid "Issue %{issue_reference} has already been added to epic %{epic_reference}."
+msgstr ""
+
msgid "Issue Boards"
msgstr ""
@@ -11191,6 +11212,9 @@ msgstr ""
msgid "More information"
msgstr ""
+msgid "More information and share feedback"
+msgstr ""
+
msgid "More information is available|here"
msgstr ""
@@ -11607,6 +11631,15 @@ msgstr ""
msgid "No value set by top-level parent group."
msgstr ""
+msgid "No vulnerabilities found for this group"
+msgstr ""
+
+msgid "No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "No vulnerabilities found for this project"
+msgstr ""
+
msgid "No, directly import the existing email addresses and usernames."
msgstr ""
@@ -11793,6 +11826,9 @@ msgstr ""
msgid "November"
msgstr ""
+msgid "Now you can access the merge request navigation tabs at the top, where they’re easier to find."
+msgstr ""
+
msgid "Number of Elasticsearch replicas"
msgstr ""
@@ -12053,6 +12089,18 @@ msgstr ""
msgid "Package was removed"
msgstr ""
+msgid "PackageRegistry|Copy Maven XML"
+msgstr ""
+
+msgid "PackageRegistry|Copy Maven command"
+msgstr ""
+
+msgid "PackageRegistry|Copy Maven registry XML"
+msgstr ""
+
+msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
+msgstr ""
+
msgid "PackageRegistry|Copy npm command"
msgstr ""
@@ -12071,12 +12119,24 @@ msgstr ""
msgid "PackageRegistry|Delete package"
msgstr ""
+msgid "PackageRegistry|For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|Installation"
msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Maven Command"
+msgstr ""
+
+msgid "PackageRegistry|Maven XML"
+msgstr ""
+
msgid "PackageRegistry|Package installation"
msgstr ""
@@ -12215,6 +12275,9 @@ msgstr ""
msgid "Path:"
msgstr ""
+msgid "Paths can contain wildcards, like */welcome"
+msgstr ""
+
msgid "Pause"
msgstr ""
@@ -12245,6 +12308,9 @@ msgstr ""
msgid "PerformanceBar|Download"
msgstr ""
+msgid "PerformanceBar|Frontend resources"
+msgstr ""
+
msgid "PerformanceBar|Gitaly calls"
msgstr ""
@@ -14997,6 +15063,9 @@ msgstr ""
msgid "Save pipeline schedule"
msgstr ""
+msgid "Save template"
+msgstr ""
+
msgid "Save variables"
msgstr ""
@@ -15319,12 +15388,6 @@ msgstr ""
msgid "Security Reports|Undo dismiss"
msgstr ""
-msgid "Security Reports|We've found no vulnerabilities for your group"
-msgstr ""
-
-msgid "Security Reports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "Security configuration help link"
msgstr ""
@@ -15664,6 +15727,12 @@ msgstr ""
msgid "Service Desk is enabled but not yet active"
msgstr ""
+msgid "Service Desk is off"
+msgstr ""
+
+msgid "Service Desk is on"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -16791,7 +16860,7 @@ msgstr ""
msgid "Subscriptions"
msgstr ""
-msgid "Subscriptions allow successfully completed pipelines on the %{default_branch_docs} of the subscribed project to trigger a new pipeline on thee default branch of this project."
+msgid "Subscriptions allow successfully completed pipelines on the %{default_branch_docs} of the subscribed project to trigger a new pipeline on the default branch of this project."
msgstr ""
msgid "Subtracted"
@@ -17064,6 +17133,9 @@ msgstr ""
msgid "Target Branch"
msgstr ""
+msgid "Target Path"
+msgstr ""
+
msgid "Target branch"
msgstr ""
@@ -17079,6 +17151,12 @@ msgstr ""
msgid "Template"
msgstr ""
+msgid "Template to append to all Service Desk issues"
+msgstr ""
+
+msgid "Template was successfully saved."
+msgstr ""
+
msgid "Templates"
msgstr ""
@@ -17618,6 +17696,9 @@ msgstr ""
msgid "There was an error while fetching cycle analytics data."
msgstr ""
+msgid "There was an error while fetching cycle analytics duration data."
+msgstr ""
+
msgid "There was an error while fetching cycle analytics summary data."
msgstr ""
@@ -19568,6 +19649,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We've found no vulnerabilities"
+msgstr ""
+
msgid "Web IDE"
msgstr ""
@@ -19636,6 +19720,18 @@ msgstr ""
msgid "When:"
msgstr ""
+msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
msgid "White helpers give contextual information."
msgstr ""
@@ -20350,6 +20446,9 @@ msgstr ""
msgid "assign yourself"
msgstr ""
+msgid "at line %{errorLine}%{errorColumn}"
+msgstr ""
+
msgid "attach a new file"
msgstr ""
@@ -20830,6 +20929,9 @@ msgstr ""
msgid "importing"
msgstr ""
+msgid "in %{errorFn} "
+msgstr ""
+
msgid "in group %{link_to_group}"
msgstr ""
@@ -20868,6 +20970,9 @@ msgstr ""
msgid "is not an email you own"
msgstr ""
+msgid "is too long (%{current_value}). The maximum size is %{max_size}."
+msgstr ""
+
msgid "is too long (maximum is 100 entries)"
msgstr ""
diff --git a/package.json b/package.json
index b92b2c9b06e..d105fd2c210 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"check-dependencies": "scripts/frontend/check_dependencies.sh",
"clean": "rm -rf public/assets tmp/cache/*-loader",
"dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
- "eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
+ "eslint": "eslint --max-warnings 900 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
"file-coverage": "scripts/frontend/file_test_coverage.js",
@@ -38,7 +38,7 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.6.2",
"@gitlab/svgs": "^1.82.0",
- "@gitlab/ui": "7.16.1",
+ "@gitlab/ui": "8.0.1",
"@gitlab/visual-review-tools": "1.2.0",
"@sentry/browser": "^5.7.1",
"@sourcegraph/code-host-integration": "^0.0.14",
@@ -156,7 +156,7 @@
"eslint": "~5.9.0",
"eslint-import-resolver-jest": "^2.1.1",
"eslint-import-resolver-webpack": "^0.10.1",
- "eslint-plugin-import": "^2.14.0",
+ "eslint-plugin-import": "^2.18.2",
"eslint-plugin-jasmine": "^2.10.1",
"eslint-plugin-jest": "^22.3.0",
"eslint-plugin-no-jquery": "^2.1.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index 5266fc57b0a..3575ecf13e9 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -2,8 +2,8 @@ source 'https://rubygems.org'
gem 'gitlab-qa'
gem 'activesupport', '5.2.3' # This should stay in sync with the root's Gemfile
-gem 'capybara', '~> 2.16.1'
-gem 'capybara-screenshot', '~> 1.0.18'
+gem 'capybara', '~> 3.29.0'
+gem 'capybara-screenshot', '~> 1.0.23'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.12'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 84eab990c95..a84b353170e 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -6,8 +6,8 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- addressable (2.5.2)
- public_suffix (>= 2.0.2, < 4.0)
+ addressable (2.7.0)
+ public_suffix (>= 2.0.2, < 5.0)
airborne (0.2.13)
activesupport
rack
@@ -15,18 +15,18 @@ GEM
rest-client (>= 1.7.3, < 3.0)
rspec (~> 3.1)
byebug (9.1.0)
- capybara (2.16.1)
+ capybara (3.29.0)
addressable
mini_mime (>= 0.1.3)
- nokogiri (>= 1.3.3)
- rack (>= 1.0.0)
- rack-test (>= 0.5.4)
- xpath (~> 2.0)
- capybara-screenshot (1.0.18)
- capybara (>= 1.0, < 3)
+ nokogiri (~> 1.8)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ regexp_parser (~> 1.5)
+ xpath (~> 3.2)
+ capybara-screenshot (1.0.23)
+ capybara (>= 1.0, < 4)
launchy
- childprocess (0.9.0)
- ffi (~> 1.0, >= 1.0.11)
+ childprocess (3.0.0)
coderay (1.1.2)
concurrent-ruby (1.1.5)
debase (0.2.4.1)
@@ -51,7 +51,7 @@ GEM
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
- mini_mime (1.0.0)
+ mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
netrc (0.11.0)
@@ -66,11 +66,12 @@ GEM
pry-byebug (3.5.1)
byebug (~> 9.1)
pry (~> 0.10)
- public_suffix (3.0.1)
- rack (2.0.6)
- rack-test (0.8.2)
+ public_suffix (4.0.1)
+ rack (2.0.7)
+ rack-test (0.8.3)
rack (>= 1.0, < 3)
- rake (12.3.3)
+ rake (12.3.0)
+ regexp_parser (1.6.0)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
@@ -95,17 +96,17 @@ GEM
ruby-debug-ide (0.7.0)
rake (>= 0.8.1)
rubyzip (1.2.2)
- selenium-webdriver (3.141.0)
- childprocess (~> 0.5)
- rubyzip (~> 1.2, >= 1.2.2)
+ selenium-webdriver (3.142.6)
+ childprocess (>= 0.5, < 4.0)
+ rubyzip (>= 1.2.2)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.4)
- xpath (2.1.0)
- nokogiri (~> 1.3)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
PLATFORMS
ruby
@@ -113,8 +114,8 @@ PLATFORMS
DEPENDENCIES
activesupport (= 5.2.3)
airborne (~> 0.2.13)
- capybara (~> 2.16.1)
- capybara-screenshot (~> 1.0.18)
+ capybara (~> 3.29.0)
+ capybara-screenshot (~> 1.0.23)
debase (~> 0.2.4.1)
faker (~> 1.6, >= 1.6.6)
gitlab-qa
diff --git a/qa/qa.rb b/qa/qa.rb
index 902963a6ed2..178771a0275 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -15,6 +15,7 @@ module QA
#
module Flow
autoload :Login, 'qa/flow/login'
+ autoload :Project, 'qa/flow/project'
end
##
@@ -33,6 +34,7 @@ module QA
autoload :Fixtures, 'qa/runtime/fixtures'
autoload :Logger, 'qa/runtime/logger'
autoload :GPG, 'qa/runtime/gpg'
+ autoload :MailHog, 'qa/runtime/mail_hog'
module API
autoload :Client, 'qa/runtime/api/client'
@@ -130,6 +132,7 @@ module QA
autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes'
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage'
+ autoload :SMTP, 'qa/scenario/test/integration/smtp'
end
module Sanity
@@ -422,6 +425,7 @@ module QA
autoload :Maven, 'qa/service/docker_run/maven'
autoload :NodeJs, 'qa/service/docker_run/node_js'
autoload :GitlabRunner, 'qa/service/docker_run/gitlab_runner'
+ autoload :MailHog, 'qa/service/docker_run/mail_hog'
end
end
diff --git a/qa/qa/flow/project.rb b/qa/qa/flow/project.rb
new file mode 100644
index 00000000000..72b9357a604
--- /dev/null
+++ b/qa/qa/flow/project.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module QA
+ module Flow
+ module Project
+ module_function
+
+ def add_member(project:, username:)
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_members_settings)
+
+ Page::Project::Settings::Members.perform do |member_settings|
+ member_settings.add_member(username)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index a080c4071ed..b28664b2921 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -141,6 +141,10 @@ module QA
page.has_no_text? text
end
+ def has_normalized_ws_text?(text, wait: Capybara.default_max_wait_time)
+ page.has_text?(text.gsub(/\s+/, " "), wait: wait)
+ end
+
def finished_loading?
has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time)
end
diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb
index cfd8ac1e7c8..9ecc8f73bdb 100644
--- a/qa/qa/page/component/issuable/common.rb
+++ b/qa/qa/page/component/issuable/common.rb
@@ -8,6 +8,7 @@ module QA
def self.included(base)
base.view 'app/assets/javascripts/issue_show/components/title.vue' do
element :edit_button
+ element :title, required: true
end
base.view 'app/assets/javascripts/issue_show/components/fields/title.vue' do
diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb
index 6353895ffd4..2b3b872aff4 100644
--- a/qa/qa/page/group/menu.rb
+++ b/qa/qa/page/group/menu.rb
@@ -10,6 +10,7 @@ module QA
element :group_settings_item
element :group_members_item
element :general_settings_link
+ element :contribution_analytics_link
end
def click_group_members_item
@@ -18,6 +19,12 @@ module QA
end
end
+ def click_group_analytics_item
+ within_sidebar do
+ click_element(:contribution_analytics_link)
+ end
+ end
+
def click_group_general_settings_item
hover_element(:group_settings_item) do
within_submenu(:group_sidebar_submenu) do
diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb
index e531ace8529..eea5c4b527e 100644
--- a/qa/qa/page/mattermost/main.rb
+++ b/qa/qa/page/mattermost/main.rb
@@ -4,11 +4,6 @@ module QA
module Page
module Mattermost
class Main < Page::Base
- ##
- # TODO, define all selectors required by this page object
- #
- # See gitlab-org/gitlab-qa#154
- #
view 'app/views/projects/mattermosts/new.html.haml'
def initialize
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 54a08d911cb..9ad53636c42 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -26,7 +26,7 @@ module QA
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do
- element :merged_status, 'The changes were merged into' # rubocop:disable QA/ElementWithPattern
+ element :merged_status_content
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do
@@ -86,13 +86,31 @@ module QA
has_element?(:merge_moment_dropdown)
end
+ def merged?
+ has_element? :merged_status_content, text: 'The changes were merged into'
+ end
+
def merge_immediately
+ wait(reload: false, max: 60) do
+ has_merge_options?
+ end
+
if has_merge_options?
- click_element :merge_moment_dropdown
+ if has_no_element? :merge_immediately_option
+ retry_until do
+ click_element :merge_moment_dropdown
+ has_element? :merge_immediately_option
+ end
+ end
+
click_element :merge_immediately_option
else
click_element :merge_button
end
+
+ wait(reload: false, max: 60) do
+ merged?
+ end
end
def rebase!
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 6ec80b7c9cc..0622cb925f9 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -44,6 +44,7 @@ module QA
end
view 'app/views/shared/issuable/_close_reopen_button.html.haml' do
+ element :close_issue_button
element :reopen_issue_button
end
@@ -84,6 +85,10 @@ module QA
click_element(:remove_related_issue_button)
end
+ def click_close_issue_button
+ click_element :close_issue_button
+ end
+
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment: nil, filter: :all_activities)
@@ -157,7 +162,7 @@ module QA
def select_filter_with_text(text)
retry_on_exception do
- click_body
+ click_element(:title)
click_element :discussion_filter
find_element(:filter_options, text: text).click
end
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index b52f3e99a36..269d4dfc411 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -14,11 +14,7 @@ module QA::Page
def click_on_latest_pipeline
css = '.js-pipeline-url-link'
- link = wait(reload: false) do
- first(css)
- end
-
- link.click
+ first(css, wait: 60).click
end
def wait_for_latest_pipeline_success
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index b8d961274a9..602bfc64710 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -12,7 +12,7 @@ module QA
view 'app/assets/javascripts/deploy_keys/components/app.vue' do
element :deploy_keys_section, /class=".*deploy\-keys.*"/ # rubocop:disable QA/ElementWithPattern
- element :project_deploy_keys, 'class="qa-project-deploy-keys"' # rubocop:disable QA/ElementWithPattern
+ element :project_deploy_keys
end
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 7e45e5e86ea..69ba90702be 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -132,6 +132,10 @@ module QA
config.default_max_wait_time = CAPYBARA_MAX_WAIT_TIME
# https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = ::File.expand_path('../../tmp', __dir__)
+
+ # Cabybara 3 does not normalize text by default, so older tests
+ # fail because of unexpected line breaks and other white space
+ config.default_normalize_ws = true
end
end
@@ -152,6 +156,8 @@ module QA
def perform(&block)
visit(url)
+ simulate_slow_connection if Runtime::Env.simulate_slow_connection?
+
page_class.validate_elements_present!
if QA::Runtime::Env.qa_cookies
@@ -174,6 +180,28 @@ module QA
def clear!
visit(url)
reset_session!
+ @network_conditions_configured = false
+ end
+
+ private
+
+ def simulate_slow_connection
+ return if @network_conditions_configured
+
+ QA::Runtime::Logger.info(
+ <<~MSG.tr("\n", " ")
+ Simulating a slow connection with additional latency
+ of #{Runtime::Env.slow_connection_latency} ms and a maximum
+ throughput of #{Runtime::Env.slow_connection_throughput} kbps
+ MSG
+ )
+
+ Capybara.current_session.driver.browser.network_conditions = {
+ latency: Runtime::Env.slow_connection_latency,
+ throughput: Runtime::Env.slow_connection_throughput * 1000
+ }
+
+ @network_conditions_configured = true
end
end
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index bcd2a225469..9cf72457fa6 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -261,10 +261,30 @@ module QA
ENV['QA_RUNTIME_SCENARIO_ATTRIBUTES']
end
+ def disable_rspec_retry?
+ enabled?(ENV['QA_DISABLE_RSPEC_RETRY'], default: false)
+ end
+
+ def simulate_slow_connection?
+ enabled?(ENV['QA_SIMULATE_SLOW_CONNECTION'], default: false)
+ end
+
+ def slow_connection_latency
+ ENV.fetch('QA_SLOW_CONNECTION_LATENCY_MS', 2000).to_i
+ end
+
+ def slow_connection_throughput
+ ENV.fetch('QA_SLOW_CONNECTION_THROUGHPUT_KBPS', 32).to_i
+ end
+
def gitlab_qa_loop_runner_minutes
ENV.fetch('GITLAB_QA_LOOP_RUNNER_MINUTES', 1).to_i
end
+ def mailhog_hostname
+ ENV['MAILHOG_HOSTNAME']
+ end
+
private
def remote_grid_credentials
diff --git a/qa/qa/runtime/mail_hog.rb b/qa/qa/runtime/mail_hog.rb
new file mode 100644
index 00000000000..899450a1540
--- /dev/null
+++ b/qa/qa/runtime/mail_hog.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+module QA
+ module Runtime
+ module MailHog
+ def self.base_url
+ host = QA::Runtime::Env.mailhog_hostname || 'localhost'
+ "http://#{host}:8025"
+ end
+
+ def self.api_messages_url
+ "#{base_url}/api/v2/messages"
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/smtp.rb b/qa/qa/scenario/test/integration/smtp.rb
new file mode 100644
index 00000000000..a27bb5f9368
--- /dev/null
+++ b/qa/qa/scenario/test/integration/smtp.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class SMTP < Test::Instance::All
+ tags :smtp
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/create_group_with_mattermost_team_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/create_group_with_mattermost_team_spec.rb
index 66c56c86fc8..7143cc574b8 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/create_group_with_mattermost_team_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/create_group_with_mattermost_team_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Configure', :orchestrated, :mattermost do
describe 'Mattermost support' do
it 'user creates a group with a mattermost team' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
Page::Main::Menu.perform(&:go_to_groups)
Page::Dashboard::Groups.perform do |groups|
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb
index c9acd7df776..6f75940e1f0 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage' do
describe 'Project transfer between groups' do
it 'user transfers a project between groups' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
source_group = Resource::Group.fabricate_via_api! do |group|
group.path = 'source-group'
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
index d8233fc5586..1050005a231 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage', :smoke do
describe 'basic user login' do
it 'user logs in using basic credentials and logs out' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
index 10cd8470a8f..46a0f1a4c8b 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do
describe 'LDAP login' do
it 'user logs into GitLab using LDAP credentials' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
index e66c12c1301..a680cfa96bd 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage', :orchestrated, :mattermost do
describe 'Mattermost login' do
it 'user logs into Mattermost using GitLab OAuth' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
Support::Retrier.retry_on_exception do
Runtime::Browser.visit(:mattermost, Page::Mattermost::Login)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/mail/trigger_mail_notification_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/mail/trigger_mail_notification_spec.rb
new file mode 100644
index 00000000000..d3bb269bcb3
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/mail/trigger_mail_notification_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Manage', :orchestrated, :smtp do
+ describe 'mail notification' do
+ let(:user) do
+ Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
+ end
+
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |resource|
+ resource.name = 'email-notification-test'
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ it 'user receives email for project invitation' do
+ Flow::Project.add_member(project: project, username: user.username)
+
+ expect(page).to have_content(/@#{user.username}(\n| )?Given access/)
+
+ # Wait for Action Mailer to deliver messages
+ mailhog_json = Support::Retrier.retry_until(sleep_interval: 1) do
+ Runtime::Logger.debug(%Q[retrieving "#{QA::Runtime::MailHog.api_messages_url}"]) if Runtime::Env.debug?
+
+ mailhog_response = get QA::Runtime::MailHog.api_messages_url
+
+ mailhog_data = JSON.parse(mailhog_response.body)
+
+ # Expect at least two invitation messages: group and project
+ mailhog_data if mailhog_data.dig('total') >= 2
+ end
+
+ # Check json result from mailhog
+ mailhog_items = mailhog_json.dig('items')
+ expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === o.dig('Content', 'Headers', 'Subject', 0) })
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index abb46463ed2..9a273e9cd1c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage' do
describe 'Add project member' do
it 'user adds project member' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index fbe857dc2a5..9ca933a957f 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage', :smoke do
describe 'Project creation' do
it 'user creates a new project' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
created_project = Resource::Project.fabricate_via_browser_ui! do |project|
project.name = 'awesome-project'
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index 4f68500974e..7a47194c3a4 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -23,8 +23,7 @@ module QA
end
it 'user imports a GitHub repo' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
imported_project # import the project
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
index fe92fbd3ffe..5f3b492ea81 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Manage' do
describe 'Project activity' do
it 'user creates an event in the activity page upon Git push' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
index 6bdec232bb0..f48fe78c530 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create' do
describe 'Create a new merge request' do
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
@project = Resource::Project.fabricate_via_api! do |project|
project.name = 'project'
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index 6ca7af8a3af..370bf30f3a4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create' do
describe 'Merge request creation from fork' do
it 'user forks a project, submits a merge request and maintainer merges it' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request|
merge_request.fork_branch = 'feature-branch'
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index c7b5e40d0be..c41cb24f4c2 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -5,8 +5,7 @@ module QA
context 'Create', :quarantine do
describe 'Merge request rebasing' do
it 'user rebases source branch of merge request' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project = Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
index a93f2695ec2..f1ba726ddec 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create' do
describe 'Merge request squashing' do
it 'user squashes commits while merging' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project = Resource::Project.fabricate! do |project|
project.name = "squash-before-merge"
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
index d2fd1d743fb..c119e447097 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -52,16 +52,16 @@ module QA
Page::Project::Show.perform(&:create_new_file!)
Page::File::Form.perform do |form|
form.select_template template[:file_name], template[:name]
- end
- expect(page).to have_content(content[0..100])
+ expect(form).to have_normalized_ws_text(content[0..100])
- Page::File::Form.perform(&:commit_changes)
+ form.commit_changes
- expect(page).to have_content('The file has been successfully created.')
- expect(page).to have_content(template[:file_name])
- expect(page).to have_content('Add new file')
- expect(page).to have_content(content[0..100])
+ expect(form).to have_content('The file has been successfully created.')
+ expect(form).to have_content(template[:file_name])
+ expect(form).to have_content('Add new file')
+ expect(form).to have_normalized_ws_text(content[0..100])
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index 3306c5f5c50..7b1c2a71158 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -16,8 +16,7 @@ module QA
commit_message_of_third_branch = "Add #{file_third_branch}"
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project = Resource::Project.fabricate! do |proj|
proj.name = 'project-qa-test'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
index 56a7a04e840..474a7904fea 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
@@ -6,8 +6,7 @@ module QA
let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
it 'user adds and then removes an SSH key', :smoke do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
index ec3c4c1ae94..3771be1a8d1 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb
@@ -6,8 +6,7 @@ module QA
context 'Create', :quarantine do
describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do
it 'user pushes to the repository' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
# Create a project to push to
project = Resource::Project.fabricate! do |project|
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
index 58f402a19ce..e2b2ee801c3 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
@@ -17,20 +17,15 @@ module QA
end
end
- def login
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- end
-
around do |example|
# Create an SSH key to be used with Git
- login
+ Flow::Login.sign_in
ssh_key
example.run
# Remove the SSH key
- login
+ Flow::Login.sign_in
Page::Main::Menu.perform(&:click_settings_link)
Page::Profile::Menu.perform(&:click_ssh_keys)
Page::Profile::SSHKeys.perform do |ssh_keys|
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 1f4fb08accc..c713f11af7d 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create' do
describe 'Git push over HTTP', :ldap_no_tls do
it 'user using a personal access token pushes code to the repository' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
access_token = Resource::PersonalAccessToken.fabricate!.access_token
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index 58e6c160a3a..2bd54d763a6 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create' do
describe 'Git push over HTTP', :ldap_no_tls do
it 'user pushes code to the repository' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
index a0251e1c385..1837a110d79 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb
@@ -9,8 +9,7 @@ module QA
let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
it 'user adds an ssh key and pushes code to the repository' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
key = Resource::SSHKey.fabricate! do |resource|
resource.title = key_title
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
index cbc9f63f772..277e7364ada 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create', :smoke do
describe 'Snippet creation' do
it 'User creates a snippet' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
Page::Main::Menu.perform(&:go_to_snippets)
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index 71b9971a0af..b91f944a162 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -54,15 +54,15 @@ module QA
ide.create_new_file_from_template template[:file_name], template[:name]
expect(ide.has_file?(template[:file_name])).to be_truthy
- end
- expect(page).to have_button('Undo')
- expect(page).to have_content(content[0..100])
+ expect(ide).to have_button('Undo')
+ expect(ide).to have_normalized_ws_text(content[0..100])
- Page::Project::WebIDE::Edit.perform(&:commit_changes)
+ ide.commit_changes
- expect(page).to have_content(template[:file_name])
- expect(page).to have_content(content[0..100])
+ expect(ide).to have_content(template[:file_name])
+ expect(ide).to have_normalized_ws_text(content[0..100])
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
index 770d66141eb..42aa527da85 100644
--- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Create' do
describe 'Wiki management' do
it 'user creates, edits, clones, and pushes to the wiki' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
wiki = Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 6f39a755392..9c964c726f1 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Release' do
describe 'Deploy key creation' do
it 'user adds a deploy key' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
key = Runtime::Key::RSA.new
deploy_key_title = 'deploy key title'
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index 9dc4bcc8a03..3badaa983cb 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -10,8 +10,7 @@ module QA
@job_log_json_flag_enabled = Runtime::Feature.enabled?('job_log_json')
Runtime::Feature.disable('job_log_json') if @job_log_json_flag_enabled
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
@runner_name = "qa-runner-#{Time.now.to_i}"
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
index ec0c45652fd..9cb9f9ba529 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Release' do
describe 'Deploy token creation' do
it 'user adds a deploy token' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
deploy_token_name = 'deploy token name'
one_week_from_now = Date.today + 7
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 42f1e6f292a..36a1b901c65 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -62,7 +62,7 @@ RSpec.configure do |config|
# show exception that triggers a retry if verbose_retry is set to true
config.display_try_failure_messages = true
- if ENV['CI']
+ if ENV['CI'] && !QA::Runtime::Env.disable_rspec_retry?
config.around do |example|
retry_times = example.metadata.key?(:quarantine) ? 1 : 2
example.run_with_retry retry: retry_times
diff --git a/rubocop/cop/ignored_columns.rb b/rubocop/cop/ignored_columns.rb
new file mode 100644
index 00000000000..14bcfa04ae1
--- /dev/null
+++ b/rubocop/cop/ignored_columns.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Cop that blacklists the usage of Group.public_or_visible_to_user
+ class IgnoredColumns < RuboCop::Cop::Cop
+ MSG = 'Use `IgnoredColumns` concern instead of adding to `self.ignored_columns`.'
+
+ def_node_matcher :ignored_columns?, <<~PATTERN
+ (send (self) :ignored_columns)
+ PATTERN
+
+ def on_send(node)
+ return unless ignored_columns?(node)
+
+ add_offense(node, location: :expression)
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/put_project_routes_under_scope.rb b/rubocop/cop/put_project_routes_under_scope.rb
new file mode 100644
index 00000000000..02189f43ea0
--- /dev/null
+++ b/rubocop/cop/put_project_routes_under_scope.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Checks for a project routes outside '/-/' scope.
+ # For more information see: https://gitlab.com/gitlab-org/gitlab/issues/29572
+ class PutProjectRoutesUnderScope < RuboCop::Cop::Cop
+ MSG = 'Put new project routes under /-/ scope'
+
+ def_node_matcher :dash_scope?, <<~PATTERN
+ (:send nil? :scope (:str "-"))
+ PATTERN
+
+ def on_send(node)
+ return unless in_project_routes?(node)
+ return unless resource?(node)
+ return unless outside_scope?(node)
+
+ add_offense(node)
+ end
+
+ def outside_scope?(node)
+ node.each_ancestor(:block).none? do |parent|
+ dash_scope?(parent.to_a.first)
+ end
+ end
+
+ def in_project_routes?(node)
+ path = node.location.expression.source_buffer.name
+ dirname = File.dirname(path)
+ filename = File.basename(path)
+
+ dirname.end_with?('config/routes') &&
+ filename.end_with?('project.rb')
+ end
+
+ def resource?(node)
+ node.method_name == :resource ||
+ node.method_name == :resources
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 159892ae0c1..9c9948e2a61 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -14,6 +14,7 @@ require_relative 'cop/avoid_break_from_strong_memoize'
require_relative 'cop/avoid_route_redirect_leading_slash'
require_relative 'cop/line_break_around_conditional_block'
require_relative 'cop/prefer_class_methods_over_module'
+require_relative 'cop/put_project_routes_under_scope'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
@@ -53,3 +54,4 @@ require_relative 'cop/group_public_or_visible_to_user'
require_relative 'cop/inject_enterprise_edition_module'
require_relative 'cop/graphql/authorize_types'
require_relative 'cop/graphql/descriptions'
+require_relative 'cop/ignored_columns'
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 407014858b4..1014bd9a89f 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -52,10 +52,10 @@ gitlab:
resources:
requests:
cpu: 650m
- memory: 880M
+ memory: 970M
limits:
cpu: 975m
- memory: 1320M
+ memory: 1450M
task-runner:
resources:
requests:
@@ -68,10 +68,10 @@ gitlab:
resources:
requests:
cpu: 500m
- memory: 1540M
+ memory: 1630M
limits:
cpu: 750m
- memory: 2310M
+ memory: 2450M
deployment:
readinessProbe:
initialDelaySeconds: 5 # Default is 0
@@ -92,18 +92,18 @@ gitlab:
gitlab-runner:
resources:
requests:
- cpu: 450m
+ cpu: 675m
memory: 100M
limits:
- cpu: 675m
+ cpu: 1015m
memory: 150M
minio:
resources:
requests:
- cpu: 5m
+ cpu: 9m
memory: 128M
limits:
- cpu: 10m
+ cpu: 15m
memory: 280M
nginx-ingress:
controller:
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 1c33bff719d..62360dfe298 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -48,11 +48,31 @@ function delete_release() {
return
fi
- echoinfo "Deleting release '${release}'..." true
+ helm_delete_release "${namespace}" "${release}"
+ kubectl_cleanup_release "${namespace}" "${release}"
+}
+
+function helm_delete_release() {
+ local namespace="${1}"
+ local release="${2}"
+
+ echoinfo "Deleting Helm release '${release}'..." true
helm delete --tiller-namespace "${namespace}" --purge "${release}"
}
+function kubectl_cleanup_release() {
+ local namespace="${1}"
+ local release="${2}"
+
+ echoinfo "Deleting all K8s resources matching '${release}'..." true
+ kubectl --namespace "${namespace}" get ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd 2>&1 \
+ | grep "${release}" \
+ | awk '{print $1}' \
+ | xargs kubectl --namespace "${namespace}" delete \
+ || true
+}
+
function delete_failed_release() {
local namespace="${KUBE_NAMESPACE}"
local release="${CI_ENVIRONMENT_SLUG}"
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 74c1df258c0..537b2692b27 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -71,7 +71,7 @@ module Trigger
# Can be overridden
def version_param_value(version_file)
- File.read(version_file).strip
+ ENV[version_file]&.strip || File.read(version_file).strip
end
def variables
diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb
index 256aafe09f8..f483c88d18d 100644
--- a/spec/controllers/admin/identities_controller_spec.rb
+++ b/spec/controllers/admin/identities_controller_spec.rb
@@ -13,7 +13,7 @@ describe Admin::IdentitiesController do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
it 'repairs ldap blocks' do
- expect_next_instance_of(RepairLdapBlockedUserService) do |instance|
+ expect_next_instance_of(::Users::RepairLdapBlockedService) do |instance|
expect(instance).to receive(:execute)
end
@@ -25,7 +25,7 @@ describe Admin::IdentitiesController do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
it 'repairs ldap blocks' do
- expect_next_instance_of(RepairLdapBlockedUserService) do |instance|
+ expect_next_instance_of(::Users::RepairLdapBlockedService) do |instance|
expect(instance).to receive(:execute)
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 6cdd61e7abd..56c27b4e5eb 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -365,35 +365,67 @@ describe AutocompleteController do
expect(json_response[3]).to match('name' => 'thumbsdown')
end
end
+ end
- context 'Get merge_request_target_branches' do
- let(:user2) { create(:user) }
- let!(:merge_request1) { create(:merge_request, source_project: project, target_branch: 'feature') }
+ context 'Get merge_request_target_branches' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_branch: 'feature') }
- context 'unauthorized user' do
- it 'returns empty json' do
- get :merge_request_target_branches
+ context 'anonymous user' do
+ it 'returns empty json' do
+ get :merge_request_target_branches, params: { project_id: project.id }
- expect(json_response).to be_empty
- end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_empty
end
+ end
- context 'sign in as user without any accesible merge requests' do
- it 'returns empty json' do
- sign_in(user2)
- get :merge_request_target_branches
+ context 'user without any accessible merge requests' do
+ it 'returns empty json' do
+ sign_in(create(:user))
- expect(json_response).to be_empty
- end
+ get :merge_request_target_branches, params: { project_id: project.id }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_empty
end
+ end
- context 'sign in as user with a accesible merge request' do
- it 'returns json' do
- sign_in(user)
- get :merge_request_target_branches
+ context 'user with an accessible merge request but no scope' do
+ it 'returns an error' do
+ sign_in(user)
- expect(json_response).to contain_exactly({ 'title' => 'feature' })
- end
+ get :merge_request_target_branches
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response).to eq({ 'error' => 'At least one of group_id or project_id must be specified' })
+ end
+ end
+
+ context 'user with an accessible merge request by project' do
+ it 'returns json' do
+ sign_in(user)
+
+ get :merge_request_target_branches, params: { project_id: project.id }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ end
+ end
+
+ context 'user with an accessible merge request by group' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:user) { create(:user) }
+
+ it 'returns json' do
+ group.add_owner(user)
+
+ sign_in(user)
+
+ get :merge_request_target_branches, params: { group_id: group.id }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to contain_exactly({ 'title' => 'feature' })
end
end
end
diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb
index 45f3188baae..ab99e44e4ca 100644
--- a/spec/controllers/projects/error_tracking_controller_spec.rb
+++ b/spec/controllers/projects/error_tracking_controller_spec.rb
@@ -50,8 +50,6 @@ describe Projects::ErrorTrackingController do
let(:external_url) { 'http://example.com' }
context 'no data' do
- let(:params) { project_params(format: :json) }
-
let(:permitted_params) do
ActionController::Parameters.new({}).permit!
end
@@ -72,11 +70,13 @@ describe Projects::ErrorTrackingController do
end
end
- context 'with a search_term and sort params' do
- let(:params) { project_params(format: :json, search_term: 'something', sort: 'last_seen') }
-
+ context 'with extra params' do
+ let(:cursor) { '1572959139000:0:0' }
+ let(:search_term) { 'something' }
+ let(:sort) { 'last_seen' }
+ let(:params) { project_params(format: :json, search_term: search_term, sort: sort, cursor: cursor) }
let(:permitted_params) do
- ActionController::Parameters.new(search_term: 'something', sort: 'last_seen').permit!
+ ActionController::Parameters.new(search_term: search_term, sort: sort, cursor: cursor).permit!
end
before do
@@ -88,7 +88,7 @@ describe Projects::ErrorTrackingController do
context 'service result is successful' do
before do
expect(list_issues_service).to receive(:execute)
- .and_return(status: :success, issues: [error])
+ .and_return(status: :success, issues: [error], pagination: {})
expect(list_issues_service).to receive(:external_url)
.and_return(external_url)
end
@@ -100,13 +100,16 @@ describe Projects::ErrorTrackingController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index')
- expect(json_response['external_url']).to eq(external_url)
- expect(json_response['errors']).to eq([error].as_json)
+ expect(json_response).to eq(
+ 'errors' => [error].as_json,
+ 'pagination' => {},
+ 'external_url' => external_url
+ )
end
end
end
- context 'without params' do
+ context 'without extra params' do
before do
expect(ErrorTracking::ListIssuesService)
.to receive(:new).with(project, user, {})
@@ -116,7 +119,7 @@ describe Projects::ErrorTrackingController do
context 'service result is successful' do
before do
expect(list_issues_service).to receive(:execute)
- .and_return(status: :success, issues: [error])
+ .and_return(status: :success, issues: [error], pagination: {})
expect(list_issues_service).to receive(:external_url)
.and_return(external_url)
end
@@ -128,8 +131,11 @@ describe Projects::ErrorTrackingController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index')
- expect(json_response['external_url']).to eq(external_url)
- expect(json_response['errors']).to eq([error].as_json)
+ expect(json_response).to eq(
+ 'errors' => [error].as_json,
+ 'pagination' => {},
+ 'external_url' => external_url
+ )
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 9f7fde2f0da..4519cd014a1 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1226,9 +1226,9 @@ describe Projects::MergeRequestsController do
environment2 = create(:environment, project: forked)
create(:deployment, :succeed, environment: environment2, sha: sha, ref: 'master', deployable: build)
- # TODO address the last 5 queries
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/63952 (5 queries)
- leeway = 5
+ # TODO address the last 3 queries
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/63952 (3 queries)
+ leeway = 3
expect { get_ci_environments_status }.not_to exceed_all_query_limit(control_count + leeway)
end
end
@@ -1280,6 +1280,28 @@ describe Projects::MergeRequestsController do
end
end
+ it 'uses the explicitly linked deployments' do
+ expect(EnvironmentStatus)
+ .to receive(:for_deployed_merge_request)
+ .with(merge_request, user)
+ .and_call_original
+
+ get_ci_environments_status(environment_target: 'merge_commit')
+ end
+
+ context 'when the deployment_merge_requests_widget feature flag is disabled' do
+ it 'uses the deployments retrieved using CI builds' do
+ stub_feature_flags(deployment_merge_requests_widget: false)
+
+ expect(EnvironmentStatus)
+ .to receive(:after_merge_request)
+ .with(merge_request, user)
+ .and_call_original
+
+ get_ci_environments_status(environment_target: 'merge_commit')
+ end
+ end
+
def get_ci_environments_status(extra_params = {})
params = {
namespace_id: merge_request.project.namespace.to_param,
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 054d448c28d..510db4374c0 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -53,6 +53,16 @@ describe SnippetsController do
expect(response).to have_gitlab_http_status(200)
end
+
+ context 'when user is not allowed to create a personal snippet' do
+ let(:user) { create(:user, :external) }
+
+ it 'responds with status 404' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
context 'when not signed in' do
@@ -215,6 +225,20 @@ describe SnippetsController do
expect(snippet.description).to eq('Description')
end
+ context 'when user is not allowed to create a personal snippet' do
+ let(:user) { create(:user, :external) }
+
+ it 'responds with status 404' do
+ aggregate_failures do
+ expect do
+ create_snippet(visibility_level: Snippet::PUBLIC)
+ end.not_to change { Snippet.count }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
context 'when the snippet description contains a file' do
include FileMoverHelpers
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index c8a697cfed4..d4fab48b426 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -43,6 +43,7 @@ describe 'Database schema' do
geo_nodes: %w[oauth_application_id],
geo_repository_deleted_events: %w[project_id],
geo_upload_deleted_events: %w[upload_id model_id],
+ import_failures: %w[project_id],
identities: %w[user_id],
issues: %w[last_edited_by_id state_id],
jira_tracker_data: %w[jira_issue_transition_id],
diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/dev_ops_score_metrics.rb
index f039bac81d0..0d9d7059e7f 100644
--- a/spec/factories/conversational_development_index_metrics.rb
+++ b/spec/factories/dev_ops_score_metrics.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do
+ factory :dev_ops_score_metric, class: DevOpsScore::Metric do
leader_issues { 9.256 }
instance_issues { 1.234 }
percentage_issues { 13.331 }
diff --git a/spec/factories/merge_request_diff_commits.rb b/spec/factories/merge_request_diff_commits.rb
new file mode 100644
index 00000000000..55626253e34
--- /dev/null
+++ b/spec/factories/merge_request_diff_commits.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_diff_commit do
+ association :merge_request_diff
+
+ sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
+ relative_order { 0 }
+ end
+end
diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb
index dfc7c89840a..c5a302ce78b 100644
--- a/spec/features/admin/admin_broadcast_messages_spec.rb
+++ b/spec/features/admin/admin_broadcast_messages_spec.rb
@@ -16,12 +16,14 @@ describe 'Admin Broadcast Messages' do
it 'Create a customized broadcast message' do
fill_in 'broadcast_message_message', with: 'Application update from **4:00 CST to 5:00 CST**'
fill_in 'broadcast_message_color', with: '#f2dede'
+ fill_in 'broadcast_message_target_path', with: '*/user_onboarded'
fill_in 'broadcast_message_font', with: '#b94a48'
select Date.today.next_year.year, from: 'broadcast_message_ends_at_1i'
click_button 'Add broadcast message'
expect(current_path).to eq admin_broadcast_messages_path
expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST'
+ expect(page).to have_content '*/user_onboarded'
expect(page).to have_selector 'strong', text: '4:00 CST to 5:00 CST'
expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"])
end
diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb
index 713cd944f8c..6d05682fcd5 100644
--- a/spec/features/instance_statistics/conversational_development_index_spec.rb
+++ b/spec/features/instance_statistics/conversational_development_index_spec.rb
@@ -48,7 +48,7 @@ describe 'Conversational Development Index' do
context 'when there is data to display' do
it 'shows numbers for each metric' do
stub_application_setting(usage_ping_enabled: true)
- create(:conversational_development_index_metric)
+ create(:dev_ops_score_metric)
visit instance_statistics_conversational_development_index_index_path
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index abf159949db..5b14450a289 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -8,6 +8,7 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
+ let(:enable_mr_tabs_position_flag) { true }
let(:config) do
{
@@ -26,6 +27,7 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
before do
+ stub_feature_flags(mr_tabs_position: enable_mr_tabs_position_flag)
stub_application_setting(auto_devops_enabled: false)
stub_feature_flags(ci_merge_request_pipeline: true)
stub_ci_pipeline_yaml_file(YAML.dump(config))
@@ -51,6 +53,7 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
end
+ let(:enable_mr_tabs_position_flag) { false }
before do
visit project_merge_request_path(project, merge_request)
@@ -67,9 +70,23 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
end
- it 'sees the latest detached merge request pipeline as the head pipeline', :sidekiq_might_not_need_inline do
- page.within('.ci-widget-content') do
- expect(page).to have_content("##{detached_merge_request_pipeline.id}")
+ context 'when merge request tabs feature flag is disabled' do
+ it 'sees the latest detached merge request pipeline as the head pipeline', :sidekiq_might_not_need_inline do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{detached_merge_request_pipeline.id}")
+ end
+ end
+ end
+
+ context 'when merge request tabs feature flag is enabled' do
+ let(:enable_mr_tabs_position_flag) { true }
+
+ it 'sees the latest detached merge request pipeline as the head pipeline', :sidekiq_might_not_need_inline do
+ click_link "Overview"
+
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{detached_merge_request_pipeline.id}")
+ end
end
end
@@ -243,9 +260,23 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
end
- it 'sees the latest detached merge request pipeline as the head pipeline' do
- page.within('.ci-widget-content') do
- expect(page).to have_content("##{detached_merge_request_pipeline.id}")
+ context 'when merge request tabs feature flag is enabled' do
+ it 'sees the latest detached merge request pipeline as the head pipeline' do
+ click_link "Overview"
+
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{detached_merge_request_pipeline.id}")
+ end
+ end
+ end
+
+ context 'when merge request tabs feature flag is disabled' do
+ let(:enable_mr_tabs_position_flag) { false }
+
+ it 'sees the latest detached merge request pipeline as the head pipeline' do
+ page.within('.ci-widget-content') do
+ expect(page).to have_content("##{detached_merge_request_pipeline.id}")
+ end
end
end
@@ -309,6 +340,8 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
+ click_link "Overview"
+
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
end
@@ -323,6 +356,8 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
context 'when a user merges a merge request from a forked project to the parent project' do
before do
+ click_link("Overview")
+
click_button 'Merge when pipeline succeeds'
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index 3d25611e1ea..e28d2ca5536 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -25,15 +25,16 @@ describe 'Merge request > User sees MR with deleted source branch', :js do
it 'still contains Discussion, Commits and Changes tabs' do
within '.merge-request-details' do
- expect(page).to have_content('Discussion')
+ expect(page).to have_content('Overview')
expect(page).to have_content('Commits')
expect(page).to have_content('Changes')
end
+ expect(page).to have_content('Source branch does not exist.')
+
click_on 'Changes'
wait_for_requests
expect(page).to have_selector('.diffs.tab-pane .file-holder')
- expect(page).to have_content('Source branch does not exist.')
end
end
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index edf7d37fd6d..450e520e293 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -158,4 +158,21 @@ describe 'Snippet', :js do
subject { visit snippet_path(snippet) }
end
+
+ context 'when user cannot create snippets' do
+ let(:user) { create(:user, :external) }
+ let(:snippet) { create(:personal_snippet, :public) }
+
+ before do
+ sign_in(user)
+
+ visit snippet_path(snippet)
+
+ wait_for_requests
+ end
+
+ it 'does not show the "New Snippet" button' do
+ expect(page).not_to have_link('New snippet')
+ end
+ end
end
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
new file mode 100644
index 00000000000..f21bb068c24
--- /dev/null
+++ b/spec/finders/deployments_finder_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeploymentsFinder do
+ subject { described_class.new(project, params).execute }
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:params) { {} }
+
+ describe "#execute" do
+ it 'returns all deployments by default' do
+ deployments = create_list(:deployment, 2, :success, project: project)
+ is_expected.to match_array(deployments)
+ end
+
+ describe 'filtering' do
+ context 'when updated_at filters are specified' do
+ let(:params) { { updated_before: 1.day.ago, updated_after: 3.days.ago } }
+ let!(:deployment_1) { create(:deployment, :success, project: project, updated_at: 2.days.ago) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, updated_at: 4.days.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, updated_at: 1.hour.ago) }
+
+ it 'returns deployments with matched updated_at' do
+ is_expected.to match_array([deployment_1])
+ end
+ end
+ end
+
+ describe 'ordering' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:params) { { order_by: order_by, sort: sort } }
+
+ let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now, updated_at: Time.now) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'patch', created_at: 2.days.ago, updated_at: 1.hour.ago) }
+
+ where(:order_by, :sort, :ordered_deployments) do
+ 'created_at' | 'asc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'created_at' | 'desc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
+ 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
+ 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ 'invalid' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
+ 'iid' | 'err' | [:deployment_3, :deployment_1, :deployment_2]
+ end
+
+ with_them do
+ it 'returns the deployments ordered' do
+ expect(subject).to eq(ordered_deployments.map { |name| public_send(name) })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index bcb762664f7..8f83cb77709 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -60,10 +60,20 @@ describe SnippetsFinder do
end
context 'filter by author' do
- it 'returns all public and internal snippets' do
- snippets = described_class.new(create(:user), author: user).execute
+ context 'when the author is a User object' do
+ it 'returns all public and internal snippets' do
+ snippets = described_class.new(create(:user), author: user).execute
- expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet)
+ expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet)
+ end
+ end
+
+ context 'when the author is the User id' do
+ it 'returns all public and internal snippets' do
+ snippets = described_class.new(create(:user), author: user.id).execute
+
+ expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet)
+ end
end
it 'returns internal snippets' do
@@ -101,13 +111,33 @@ describe SnippetsFinder do
expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
end
+
+ context 'when author is not valid' do
+ it 'returns quickly' do
+ finder = described_class.new(admin, author: 1234)
+
+ expect(finder).not_to receive(:init_collection)
+ expect(Snippet).to receive(:none).and_call_original
+ expect(finder.execute).to be_empty
+ end
+ end
end
- context 'project snippets' do
- it 'returns public personal and project snippets for unauthorized user' do
- snippets = described_class.new(nil, project: project).execute
+ context 'filter by project' do
+ context 'when project is a Project object' do
+ it 'returns public personal and project snippets for unauthorized user' do
+ snippets = described_class.new(nil, project: project).execute
- expect(snippets).to contain_exactly(public_project_snippet)
+ expect(snippets).to contain_exactly(public_project_snippet)
+ end
+ end
+
+ context 'when project is a Project id' do
+ it 'returns public personal and project snippets for unauthorized user' do
+ snippets = described_class.new(nil, project: project.id).execute
+
+ expect(snippets).to contain_exactly(public_project_snippet)
+ end
end
it 'returns public and internal snippets for non project members' do
@@ -175,6 +205,49 @@ describe SnippetsFinder do
)
end
end
+
+ context 'when project is not valid' do
+ it 'returns quickly' do
+ finder = described_class.new(admin, project: 1234)
+
+ expect(finder).not_to receive(:init_collection)
+ expect(Snippet).to receive(:none).and_call_original
+ expect(finder.execute).to be_empty
+ end
+ end
+ end
+
+ context 'filter by snippet type' do
+ context 'when filtering by only_personal snippet' do
+ it 'returns only personal snippet' do
+ snippets = described_class.new(admin, only_personal: true).execute
+
+ expect(snippets).to contain_exactly(private_personal_snippet,
+ internal_personal_snippet,
+ public_personal_snippet)
+ end
+ end
+
+ context 'when filtering by only_project snippet' do
+ it 'returns only project snippet' do
+ snippets = described_class.new(admin, only_project: true).execute
+
+ expect(snippets).to contain_exactly(private_project_snippet,
+ internal_project_snippet,
+ public_project_snippet)
+ end
+ end
+ end
+
+ context 'filtering by ids' do
+ it 'returns only personal snippet' do
+ snippets = described_class.new(
+ admin, ids: [private_personal_snippet.id,
+ internal_personal_snippet.id]
+ ).execute
+
+ expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet)
+ end
end
context 'explore snippets' do
diff --git a/spec/fixtures/api/schemas/error_tracking/index.json b/spec/fixtures/api/schemas/error_tracking/index.json
index d3abc29ffa7..7a570641211 100644
--- a/spec/fixtures/api/schemas/error_tracking/index.json
+++ b/spec/fixtures/api/schemas/error_tracking/index.json
@@ -2,6 +2,7 @@
"type": "object",
"required": [
"external_url",
+ "pagination",
"errors"
],
"properties": {
@@ -9,6 +10,9 @@
"errors": {
"type": "array",
"items": { "$ref": "error.json" }
+ },
+ "pagination": {
+ "type": "object"
}
},
"additionalProperties": false
diff --git a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
new file mode 100644
index 00000000000..cac16cf9cd8
--- /dev/null
+++ b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
new file mode 100644
index 00000000000..c01402954dd
--- /dev/null
+++ b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json
new file mode 100644
index 00000000000..52b5649ae59
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/project.json
@@ -0,0 +1,38 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
+ "visibility_level": 10,
+ "archived": false,
+ "milestones": [
+ {
+ "id": 1,
+ "title": null,
+ "project_id": 8,
+ "description": 123,
+ "due_date": null,
+ "created_at": "NOT A DATE",
+ "updated_at": "NOT A DATE",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
+ {
+ "id": 42,
+ "title": "A valid milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ ],
+ "labels": [],
+ "issues": [],
+ "services": [],
+ "snippets": [],
+ "hooks": []
+}
diff --git a/spec/fixtures/project_export.tar.gz b/spec/fixtures/project_export.tar.gz
index 72ab2d71f35..5ba3bfd4f48 100644
--- a/spec/fixtures/project_export.tar.gz
+++ b/spec/fixtures/project_export.tar.gz
Binary files differ
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
new file mode 100644
index 00000000000..9900fcdb6e1
--- /dev/null
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -0,0 +1,156 @@
+import { trimText } from 'helpers/text_helper';
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { createStore } from '~/mr_notes/stores';
+import diffsMockData from '../mock_data/merge_request_diffs';
+import getDiffWithCommit from '../mock_data/diff_with_commit';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('CompareVersions', () => {
+ let wrapper;
+ const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
+
+ const createWrapper = props => {
+ const store = createStore();
+
+ store.state.diffs.addedLines = 10;
+ store.state.diffs.removedLines = 20;
+ store.state.diffs.diffFiles.push('test');
+
+ wrapper = mount(CompareVersionsComponent, {
+ sync: false,
+ attachToDocument: true,
+ localVue,
+ store,
+ propsData: {
+ mergeRequestDiffs: diffsMockData,
+ mergeRequestDiff: diffsMockData[0],
+ targetBranch,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('template', () => {
+ it('should render Tree List toggle button with correct attribute values', () => {
+ const treeListBtn = wrapper.find('.js-toggle-tree-list');
+
+ expect(treeListBtn.exists()).toBe(true);
+ expect(treeListBtn.attributes('data-original-title')).toBe('Hide file browser');
+ expect(treeListBtn.findAll(Icon).length).not.toBe(0);
+ expect(treeListBtn.find(Icon).props('name')).toBe('collapse-left');
+ });
+
+ it('should render comparison dropdowns with correct values', () => {
+ const sourceDropdown = wrapper.find('.mr-version-dropdown');
+ const targetDropdown = wrapper.find('.mr-version-compare-dropdown');
+
+ expect(sourceDropdown.exists()).toBe(true);
+ expect(targetDropdown.exists()).toBe(true);
+ expect(sourceDropdown.find('a span').html()).toContain('latest version');
+ expect(targetDropdown.find('a span').html()).toContain(targetBranch.branchName);
+ });
+
+ it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
+ createWrapper({ mergeRequestDiffs: [] });
+
+ const sourceDropdown = wrapper.find('.mr-version-dropdown');
+ const targetDropdown = wrapper.find('.mr-version-compare-dropdown');
+
+ expect(sourceDropdown.exists()).toBe(false);
+ expect(targetDropdown.exists()).toBe(false);
+ });
+
+ it('should render view types buttons with correct values', () => {
+ const inlineBtn = wrapper.find('#inline-diff-btn');
+ const parallelBtn = wrapper.find('#parallel-diff-btn');
+
+ expect(inlineBtn.exists()).toBe(true);
+ expect(parallelBtn.exists()).toBe(true);
+ expect(inlineBtn.attributes('data-view-type')).toEqual('inline');
+ expect(parallelBtn.attributes('data-view-type')).toEqual('parallel');
+ expect(inlineBtn.html()).toContain('Inline');
+ expect(parallelBtn.html()).toContain('Side-by-side');
+ });
+
+ it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
+ createWrapper({ isLimitedContainer: true });
+
+ const limitedContainer = wrapper.find('.container-limited.limit-container-width');
+
+ expect(limitedContainer.exists()).toBe(true);
+ });
+
+ it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
+ createWrapper({ isLimitedContainer: false });
+
+ const limitedContainer = wrapper.find('.container-limited.limit-container-width');
+
+ expect(limitedContainer.exists()).toBe(false);
+ });
+ });
+
+ describe('setInlineDiffViewType', () => {
+ it('should persist the view type in the url', () => {
+ const viewTypeBtn = wrapper.find('#inline-diff-btn');
+ viewTypeBtn.trigger('click');
+
+ expect(window.location.toString()).toContain('?view=inline');
+ });
+ });
+
+ describe('setParallelDiffViewType', () => {
+ it('should persist the view type in the url', () => {
+ const viewTypeBtn = wrapper.find('#parallel-diff-btn');
+ viewTypeBtn.trigger('click');
+
+ expect(window.location.toString()).toContain('?view=parallel');
+ });
+ });
+
+ describe('comparableDiffs', () => {
+ it('should not contain the first item in the mergeRequestDiffs property', () => {
+ const { comparableDiffs } = wrapper.vm;
+ const comparableDiffsMock = diffsMockData.slice(1);
+
+ expect(comparableDiffs).toEqual(comparableDiffsMock);
+ });
+ });
+
+ describe('baseVersionPath', () => {
+ it('should be set correctly from mergeRequestDiff', () => {
+ expect(wrapper.vm.baseVersionPath).toEqual(wrapper.vm.mergeRequestDiff.base_version_path);
+ });
+ });
+
+ describe('commit', () => {
+ beforeEach(done => {
+ wrapper.vm.$store.state.diffs.commit = getDiffWithCommit().commit;
+ wrapper.mergeRequestDiffs = [];
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('renders latest version button', () => {
+ expect(trimText(wrapper.find('.js-latest-version').text())).toBe('Show latest version');
+ });
+
+ it('renders short commit ID', () => {
+ expect(wrapper.text()).toContain('Viewing commit');
+ expect(wrapper.text()).toContain(wrapper.vm.commit.short_id);
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index ac770c896bd..48fd6dd6f58 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -92,6 +92,7 @@ describe('DiffFileHeader component', () => {
localVue,
store,
sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
index 48ee5c63f35..b2debe36b89 100644
--- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -15,10 +15,11 @@ describe('DiffGutterAvatars', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(DiffGutterAvatars, {
localVue,
- sync: false,
propsData: {
...props,
},
+ sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js
index ccdae4cb312..4e2cfc75212 100644
--- a/spec/frontend/diffs/components/edit_button_spec.js
+++ b/spec/frontend/diffs/components/edit_button_spec.js
@@ -10,8 +10,9 @@ describe('EditButton', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(EditButton, {
localVue,
- sync: false,
propsData: { ...props },
+ sync: false,
+ attachToDocument: true,
});
};
diff --git a/spec/frontend/diffs/mock_data/diff_with_commit.js b/spec/frontend/diffs/mock_data/diff_with_commit.js
new file mode 100644
index 00000000000..d646294ee84
--- /dev/null
+++ b/spec/frontend/diffs/mock_data/diff_with_commit.js
@@ -0,0 +1,7 @@
+const FIXTURE = 'merge_request_diffs/with_commit.json';
+
+preloadFixtures(FIXTURE);
+
+export default function getDiffWithCommit() {
+ return getJSONFixture(FIXTURE);
+}
diff --git a/spec/frontend/diffs/mock_data/merge_request_diffs.js b/spec/frontend/diffs/mock_data/merge_request_diffs.js
new file mode 100644
index 00000000000..4bbef146336
--- /dev/null
+++ b/spec/frontend/diffs/mock_data/merge_request_diffs.js
@@ -0,0 +1,46 @@
+export default [
+ {
+ base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
+ version_index: 4,
+ created_at: '2018-10-23T11:49:16.611Z',
+ commits_count: 4,
+ latest: true,
+ short_commit_sha: 'de7a8f7f',
+ version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
+ compare_path:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995',
+ },
+ {
+ base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36',
+ version_index: 3,
+ created_at: '2018-10-23T11:46:40.617Z',
+ commits_count: 3,
+ latest: false,
+ short_commit_sha: 'e78fc18f',
+ version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36',
+ compare_path:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e',
+ },
+ {
+ base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35',
+ version_index: 2,
+ created_at: '2018-10-04T09:57:39.648Z',
+ commits_count: 2,
+ latest: false,
+ short_commit_sha: '48da7e7e',
+ version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35',
+ compare_path:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e',
+ },
+ {
+ base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20',
+ version_index: 1,
+ created_at: '2018-09-25T20:30:39.493Z',
+ commits_count: 1,
+ latest: false,
+ short_commit_sha: '47bac2ed',
+ version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20',
+ compare_path:
+ '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0',
+ },
+];
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 4edc2a647c3..80f5b2ccb9f 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -9,6 +9,7 @@ import {
GlLink,
GlSearchBoxByClick,
} from '@gitlab/ui';
+import errorsList from './list_mock.json';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -18,11 +19,17 @@ describe('ErrorTrackingList', () => {
let wrapper;
let actions;
+ const findErrorListTable = () => wrapper.find('table');
+ const findErrorListRows = () => wrapper.findAll('tbody tr');
+ const findButton = () => wrapper.find(GlButton);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
function mountComponent({
errorTrackingEnabled = true,
userCanEnableErrorTracking = true,
stubs = {
'gl-link': GlLink,
+ 'gl-table': GlTable,
},
} = {}) {
wrapper = shallowMount(ErrorTrackingList, {
@@ -47,7 +54,7 @@ describe('ErrorTrackingList', () => {
};
const state = {
- errors: [],
+ errors: errorsList,
loading: true,
};
@@ -75,61 +82,74 @@ describe('ErrorTrackingList', () => {
});
it('shows spinner', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
- expect(wrapper.find(GlTable).exists()).toBeFalsy();
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findErrorListTable().exists()).toBe(false);
});
});
describe('results', () => {
beforeEach(() => {
store.state.list.loading = false;
-
mountComponent();
});
it('shows table', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
- expect(wrapper.find(GlTable).exists()).toBeTruthy();
- expect(wrapper.find(GlButton).exists()).toBeTruthy();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findErrorListTable().exists()).toBe(true);
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it('shows list of errors in a table', () => {
+ expect(findErrorListRows().length).toEqual(store.state.list.errors.length);
+ });
+
+ it('each error in a list should have a link to the error page', () => {
+ const errorTitle = wrapper.findAll('tbody tr a');
+
+ errorTitle.wrappers.forEach((_, index) => {
+ expect(errorTitle.at(index).attributes('href')).toEqual(
+ expect.stringMatching(/error_tracking\/\d+\/details$/),
+ );
+ });
});
describe('filtering', () => {
+ const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
+
it('shows search box', () => {
- expect(wrapper.find(GlSearchBoxByClick).exists()).toBeTruthy();
+ expect(findSearchBox().exists()).toBe(true);
});
it('makes network request on submit', () => {
expect(actions.startPolling).toHaveBeenCalledTimes(1);
-
- wrapper.find(GlSearchBoxByClick).vm.$emit('submit');
-
+ findSearchBox().vm.$emit('submit');
expect(actions.startPolling).toHaveBeenCalledTimes(2);
});
});
});
describe('no results', () => {
+ const findRefreshLink = () => wrapper.find('.js-try-again');
+
beforeEach(() => {
store.state.list.loading = false;
+ store.state.list.errors = [];
mountComponent();
});
it('shows empty table', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
- expect(wrapper.find(GlTable).exists()).toBeTruthy();
- expect(wrapper.find(GlButton).exists()).toBeTruthy();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findErrorListRows().length).toEqual(1);
+ expect(findButton().exists()).toBe(true);
});
it('shows a message prompting to refresh', () => {
- const refreshLink = wrapper.vm.$refs.empty.querySelector('a');
-
- expect(refreshLink.textContent.trim()).toContain('Check again');
+ expect(findRefreshLink().text()).toContain('Check again');
});
it('restarts polling', () => {
- wrapper.find('.js-try-again').trigger('click');
-
+ findRefreshLink().trigger('click');
expect(actions.restartPolling).toHaveBeenCalled();
});
});
@@ -140,10 +160,10 @@ describe('ErrorTrackingList', () => {
});
it('shows empty state', () => {
- expect(wrapper.find(GlEmptyState).exists()).toBeTruthy();
- expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
- expect(wrapper.find(GlTable).exists()).toBeFalsy();
- expect(wrapper.find(GlButton).exists()).toBeFalsy();
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findErrorListTable().exists()).toBe(false);
+ expect(findButton().exists()).toBe(false);
});
});
diff --git a/spec/frontend/error_tracking/components/list_mock.json b/spec/frontend/error_tracking/components/list_mock.json
new file mode 100644
index 00000000000..a6e94c1a026
--- /dev/null
+++ b/spec/frontend/error_tracking/components/list_mock.json
@@ -0,0 +1,29 @@
+[
+ {
+ "id": "1",
+ "title": "PG::ConnectionBad: FATAL",
+ "type": "error",
+ "userCount": 0,
+ "count": "52",
+ "firstSeen": "2019-05-30T07:21:46Z",
+ "lastSeen": "2019-11-06T03:21:39Z"
+ },
+ {
+ "id": "2",
+ "title": "ActiveRecord::StatementInvalid",
+ "type": "error",
+ "userCount": 0,
+ "count": "12",
+ "firstSeen": "2019-10-19T03:53:56Z",
+ "lastSeen": "2019-11-05T03:51:54Z"
+ },
+ {
+ "id": "3",
+ "title": "Command has failed",
+ "type": "default",
+ "userCount": 0,
+ "count": "275",
+ "firstSeen": "2019-02-12T07:22:36Z",
+ "lastSeen": "2019-10-22T03:20:48Z"
+ }
+] \ No newline at end of file
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index 95958408770..942585d5370 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -7,26 +7,23 @@ import Icon from '~/vue_shared/components/icon.vue';
describe('Stacktrace Entry', () => {
let wrapper;
+ const lines = [
+ [22, ' def safe_thread(name, \u0026block)\n'],
+ [23, ' Thread.new do\n'],
+ [24, " Thread.current['sidekiq_label'] = name\n"],
+ [25, ' watchdog(name, \u0026block)\n'],
+ ];
+
function mountComponent(props) {
wrapper = shallowMount(StackTraceEntry, {
propsData: {
filePath: 'sidekiq/util.rb',
- lines: [
- [22, ' def safe_thread(name, \u0026block)\n'],
- [23, ' Thread.new do\n'],
- [24, " Thread.current['sidekiq_label'] = name\n"],
- [25, ' watchdog(name, \u0026block)\n'],
- ],
errorLine: 24,
...props,
},
});
}
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -34,16 +31,47 @@ describe('Stacktrace Entry', () => {
});
it('should render stacktrace entry collapsed', () => {
+ mountComponent({ lines });
expect(wrapper.find(StackTraceEntry).exists()).toBe(true);
expect(wrapper.find(ClipboardButton).exists()).toBe(true);
expect(wrapper.find(Icon).exists()).toBe(true);
expect(wrapper.find(FileIcon).exists()).toBe(true);
- expect(wrapper.element.querySelectorAll('table').length).toBe(0);
+ expect(wrapper.find('table').exists()).toBe(false);
});
it('should render stacktrace entry table expanded', () => {
- mountComponent({ expanded: true });
- expect(wrapper.element.querySelectorAll('tr.line_holder').length).toBe(4);
- expect(wrapper.element.querySelectorAll('.line_content.old').length).toBe(1);
+ mountComponent({ expanded: true, lines });
+ expect(wrapper.find('table').exists()).toBe(true);
+ expect(wrapper.findAll('tr.line_holder').length).toBe(4);
+ expect(wrapper.findAll('.line_content.old').length).toBe(1);
+ });
+
+ describe('no code block', () => {
+ const findFileHeaderContent = () => wrapper.find('.file-header-content').html();
+
+ it('should hide collapse icon and render error fn name and error line when there is no code block', () => {
+ const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: 77 };
+ mountComponent({ expanded: false, lines: [], ...extraInfo });
+ expect(wrapper.find(Icon).exists()).toBe(false);
+ expect(findFileHeaderContent()).toContain(
+ `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}:${extraInfo.errorColumn}`,
+ );
+ });
+
+ it('should render only lineNo:columnNO when there is no errorFn ', () => {
+ const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 };
+ mountComponent({ expanded: false, lines: [], ...extraInfo });
+ expect(findFileHeaderContent()).not.toContain(`in ${extraInfo.errorFn}`);
+ expect(findFileHeaderContent()).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`);
+ });
+
+ it('should render only lineNo when there is no errorColumn ', () => {
+ const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null };
+ mountComponent({ expanded: false, lines: [], ...extraInfo });
+ expect(findFileHeaderContent()).toContain(
+ `in ${extraInfo.errorFn} at line ${extraInfo.errorLine}`,
+ );
+ expect(findFileHeaderContent()).not.toContain(`:${extraInfo.errorColumn}`);
+ });
});
});
diff --git a/spec/frontend/error_tracking/store/details/getters_spec.js b/spec/frontend/error_tracking/store/details/getters_spec.js
index ea57de5872b..aba080790da 100644
--- a/spec/frontend/error_tracking/store/details/getters_spec.js
+++ b/spec/frontend/error_tracking/store/details/getters_spec.js
@@ -1,12 +1,18 @@
import * as getters from '~/error_tracking/store/details/getters';
describe('Sentry error details store getters', () => {
- const state = {
- stacktraceData: { stack_trace_entries: [1, 2] },
- };
-
describe('stacktrace', () => {
+ it('should return empty stacktrace when there are no entries', () => {
+ const state = {
+ stacktraceData: { stack_trace_entries: null },
+ };
+ expect(getters.stacktrace(state)).toEqual([]);
+ });
+
it('should get stacktrace', () => {
+ const state = {
+ stacktraceData: { stack_trace_entries: [1, 2] },
+ };
expect(getters.stacktrace(state)).toEqual([2, 1]);
});
});
diff --git a/spec/frontend/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js
index 7bb8e26b81a..41860202750 100644
--- a/spec/frontend/issuable_suggestions/components/app_spec.js
+++ b/spec/frontend/issuable_suggestions/components/app_spec.js
@@ -3,25 +3,31 @@ import App from '~/issuable_suggestions/components/app.vue';
import Suggestion from '~/issuable_suggestions/components/item.vue';
describe('Issuable suggestions app component', () => {
- let vm;
+ let wrapper;
function createComponent(search = 'search') {
- vm = shallowMount(App, {
+ wrapper = shallowMount(App, {
propsData: {
search,
projectPath: 'project',
},
+ sync: false,
+ attachToDocument: true,
});
}
+ beforeEach(() => {
+ createComponent();
+ });
+
afterEach(() => {
- vm.destroy();
+ wrapper.destroy();
});
it('does not render with empty search', () => {
- createComponent('');
+ wrapper.setProps({ search: '' });
- expect(vm.isVisible()).toBe(false);
+ expect(wrapper.isVisible()).toBe(false);
});
describe('with data', () => {
@@ -32,65 +38,65 @@ describe('Issuable suggestions app component', () => {
});
it('renders component', () => {
- createComponent();
- vm.setData(data);
+ wrapper.setData(data);
- expect(vm.isEmpty()).toBe(false);
+ expect(wrapper.isEmpty()).toBe(false);
});
it('does not render with empty search', () => {
- createComponent('');
- vm.setData(data);
+ wrapper.setProps({ search: '' });
+ wrapper.setData(data);
- expect(vm.isVisible()).toBe(false);
+ expect(wrapper.isVisible()).toBe(false);
});
it('does not render when loading', () => {
- createComponent();
- vm.setData({
+ wrapper.setData({
...data,
loading: 1,
});
- expect(vm.isVisible()).toBe(false);
+ expect(wrapper.isVisible()).toBe(false);
});
it('does not render with empty issues data', () => {
- createComponent();
- vm.setData({ issues: [] });
+ wrapper.setData({ issues: [] });
- expect(vm.isVisible()).toBe(false);
+ expect(wrapper.isVisible()).toBe(false);
});
it('renders list of issues', () => {
- createComponent();
- vm.setData(data);
+ wrapper.setData(data);
- expect(vm.findAll(Suggestion).length).toBe(2);
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.findAll(Suggestion).length).toBe(2);
+ });
});
it('adds margin class to first item', () => {
- createComponent();
- vm.setData(data);
-
- expect(
- vm
- .findAll('li')
- .at(0)
- .is('.append-bottom-default'),
- ).toBe(true);
+ wrapper.setData(data);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper
+ .findAll('li')
+ .at(0)
+ .is('.append-bottom-default'),
+ ).toBe(true);
+ });
});
it('does not add margin class to last item', () => {
- createComponent();
- vm.setData(data);
-
- expect(
- vm
- .findAll('li')
- .at(1)
- .is('.append-bottom-default'),
- ).toBe(false);
+ wrapper.setData(data);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper
+ .findAll('li')
+ .at(1)
+ .is('.append-bottom-default'),
+ ).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 7bd1fe678f4..10fba238506 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -16,6 +16,8 @@ describe('Issuable suggestions suggestion component', () => {
...suggestion,
},
},
+ sync: false,
+ attachToDocument: true,
});
}
diff --git a/spec/javascripts/lib/utils/accessor_spec.js b/spec/frontend/lib/utils/accessor_spec.js
index 0045330e470..752a88296e6 100644
--- a/spec/javascripts/lib/utils/accessor_spec.js
+++ b/spec/frontend/lib/utils/accessor_spec.js
@@ -1,14 +1,18 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
describe('AccessorUtilities', () => {
+ useLocalStorageSpy();
+
const testError = new Error('test error');
describe('isPropertyAccessSafe', () => {
let base;
it('should return `true` if access is safe', () => {
- base = { testProp: 'testProp' };
-
+ base = {
+ testProp: 'testProp',
+ };
expect(AccessorUtilities.isPropertyAccessSafe(base, 'testProp')).toBe(true);
});
@@ -54,17 +58,12 @@ describe('AccessorUtilities', () => {
});
describe('isLocalStorageAccessSafe', () => {
- beforeEach(() => {
- spyOn(window.localStorage, 'setItem');
- spyOn(window.localStorage, 'removeItem');
- });
-
it('should return `true` if access is safe', () => {
expect(AccessorUtilities.isLocalStorageAccessSafe()).toBe(true);
});
it('should return `false` if access to .setItem isnt safe', () => {
- window.localStorage.setItem.and.callFake(() => {
+ window.localStorage.setItem.mockImplementation(() => {
throw testError;
});
diff --git a/spec/javascripts/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index 2bcf37f35c7..10b4a10a8ff 100644
--- a/spec/javascripts/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -25,7 +25,7 @@ describe('DOM Utils', () => {
addClassIfElementExists(childElement, className);
- expect(childElement.classList).toContain(className);
+ expect(childElement.classList).toContainEqual(className);
});
it('does not throw if element does not exist', () => {
@@ -40,22 +40,44 @@ describe('DOM Utils', () => {
describe('canScrollUp', () => {
[1, 100].forEach(scrollTop => {
it(`is true if scrollTop is > 0 (${scrollTop})`, () => {
- expect(canScrollUp({ scrollTop })).toBe(true);
+ expect(
+ canScrollUp({
+ scrollTop,
+ }),
+ ).toBe(true);
});
});
[0, -10].forEach(scrollTop => {
it(`is false if scrollTop is <= 0 (${scrollTop})`, () => {
- expect(canScrollUp({ scrollTop })).toBe(false);
+ expect(
+ canScrollUp({
+ scrollTop,
+ }),
+ ).toBe(false);
});
});
it('is true if scrollTop is > margin', () => {
- expect(canScrollUp({ scrollTop: TEST_MARGIN + 1 }, TEST_MARGIN)).toBe(true);
+ expect(
+ canScrollUp(
+ {
+ scrollTop: TEST_MARGIN + 1,
+ },
+ TEST_MARGIN,
+ ),
+ ).toBe(true);
});
it('is false if scrollTop is <= margin', () => {
- expect(canScrollUp({ scrollTop: TEST_MARGIN }, TEST_MARGIN)).toBe(false);
+ expect(
+ canScrollUp(
+ {
+ scrollTop: TEST_MARGIN,
+ },
+ TEST_MARGIN,
+ ),
+ ).toBe(false);
});
});
@@ -63,7 +85,11 @@ describe('DOM Utils', () => {
let element;
beforeEach(() => {
- element = { scrollTop: 7, offsetHeight: 22, scrollHeight: 30 };
+ element = {
+ scrollTop: 7,
+ offsetHeight: 22,
+ scrollHeight: 30,
+ };
});
it('is true if element can be scrolled down', () => {
diff --git a/spec/javascripts/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index 8f7092f63de..1255d6fc14f 100644
--- a/spec/javascripts/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -20,7 +20,7 @@ describe('File upload', () => {
const btn = document.querySelector('.js-button');
const input = document.querySelector('.js-input');
- spyOn(input, 'click');
+ jest.spyOn(input, 'click').mockReturnValue();
btn.click();
@@ -43,7 +43,7 @@ describe('File upload', () => {
const btn = document.querySelector('.js-button');
fileUpload('.js-not-button', '.js-input');
- spyOn(input, 'click');
+ jest.spyOn(input, 'click').mockReturnValue();
btn.click();
@@ -55,7 +55,7 @@ describe('File upload', () => {
const btn = document.querySelector('.js-button');
fileUpload('.js-button', '.js-not-input');
- spyOn(input, 'click');
+ jest.spyOn(input, 'click').mockReturnValue();
btn.click();
diff --git a/spec/javascripts/lib/utils/higlight_spec.js b/spec/frontend/lib/utils/highlight_spec.js
index 638bbf65ae9..638bbf65ae9 100644
--- a/spec/javascripts/lib/utils/higlight_spec.js
+++ b/spec/frontend/lib/utils/highlight_spec.js
diff --git a/spec/javascripts/lib/utils/icon_utils_spec.js b/spec/frontend/lib/utils/icon_utils_spec.js
index 3fd3940efe8..816d634ad15 100644
--- a/spec/javascripts/lib/utils/icon_utils_spec.js
+++ b/spec/frontend/lib/utils/icon_utils_spec.js
@@ -17,51 +17,44 @@ describe('Icon utils', () => {
let axiosMock;
let mockEndpoint;
- let getIcon;
const mockName = 'mockIconName';
const mockPath = 'mockPath';
+ const getIcon = () => iconUtils.getSvgIconPathContent(mockName);
beforeEach(() => {
axiosMock = new MockAdapter(axios);
mockEndpoint = axiosMock.onGet(gon.sprite_icons);
- getIcon = iconUtils.getSvgIconPathContent(mockName);
});
afterEach(() => {
axiosMock.restore();
});
- it('extracts svg icon path content from sprite icons', done => {
+ it('extracts svg icon path content from sprite icons', () => {
mockEndpoint.replyOnce(
200,
`<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`,
);
- getIcon
- .then(path => {
- expect(path).toBe(mockPath);
- done();
- })
- .catch(done.fail);
+
+ return getIcon().then(path => {
+ expect(path).toBe(mockPath);
+ });
});
- it('returns null if icon path content does not exist', done => {
+ it('returns null if icon path content does not exist', () => {
mockEndpoint.replyOnce(200, ``);
- getIcon
- .then(path => {
- expect(path).toBe(null);
- done();
- })
- .catch(done.fail);
+
+ return getIcon().then(path => {
+ expect(path).toBe(null);
+ });
});
- it('returns null if an http error occurs', done => {
+ it('returns null if an http error occurs', () => {
mockEndpoint.replyOnce(500);
- getIcon
- .then(path => {
- expect(path).toBe(null);
- done();
- })
- .catch(done.fail);
+
+ return getIcon().then(path => {
+ expect(path).toBe(null);
+ });
});
});
});
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index df4029555bb..ba3e4020e66 100644
--- a/spec/javascripts/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -243,7 +243,7 @@ describe('init markdown', () => {
});
it('uses ace editor insert text when editor is passed in', () => {
- spyOn(editor, 'insert');
+ jest.spyOn(editor, 'insert').mockReturnValue();
insertMarkdownText({
text: editor.getValue,
@@ -258,7 +258,7 @@ describe('init markdown', () => {
});
it('adds block tags on line above and below selection', () => {
- spyOn(editor, 'insert');
+ jest.spyOn(editor, 'insert').mockReturnValue();
const selected = 'this text \n is multiple \n lines';
const text = `before \n ${selected} \n after`;
@@ -276,7 +276,7 @@ describe('init markdown', () => {
});
it('uses ace editor to navigate back tag length when nothing is selected', () => {
- spyOn(editor, 'navigateLeft');
+ jest.spyOn(editor, 'navigateLeft').mockReturnValue();
insertMarkdownText({
text: editor.getValue,
@@ -291,7 +291,7 @@ describe('init markdown', () => {
});
it('ace editor does not navigate back when there is selected text', () => {
- spyOn(editor, 'navigateLeft');
+ jest.spyOn(editor, 'navigateLeft').mockReturnValue();
insertMarkdownText({
text: editor.getValue,
diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/frontend/lib/utils/users_cache_spec.js
index acb5e024acd..7ed87123482 100644
--- a/spec/javascripts/lib/utils/users_cache_spec.js
+++ b/spec/frontend/lib/utils/users_cache_spec.js
@@ -4,7 +4,10 @@ import UsersCache from '~/lib/utils/users_cache';
describe('UsersCache', () => {
const dummyUsername = 'win';
const dummyUserId = 123;
- const dummyUser = { name: 'has a farm', username: 'farmer' };
+ const dummyUser = {
+ name: 'has a farm',
+ username: 'farmer',
+ };
const dummyUserStatus = 'my status';
beforeEach(() => {
@@ -68,7 +71,6 @@ describe('UsersCache', () => {
it('does nothing if cache contains no matching data', () => {
UsersCache.internalStorage['no body'] = 'no data';
-
UsersCache.remove(dummyUsername);
expect(UsersCache.internalStorage['no body']).toBe('no data');
@@ -76,7 +78,6 @@ describe('UsersCache', () => {
it('removes matching data', () => {
UsersCache.internalStorage[dummyUsername] = dummyUser;
-
UsersCache.remove(dummyUsername);
expect(UsersCache.internalStorage).toEqual({});
@@ -87,13 +88,16 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
- spyOn(Api, 'users').and.callFake((query, options) => apiSpy(query, options));
+ jest.spyOn(Api, 'users').mockImplementation((query, options) => apiSpy(query, options));
});
it('stores and returns data from API call if cache is empty', done => {
apiSpy = (query, options) => {
expect(query).toBe('');
- expect(options).toEqual({ username: dummyUsername });
+ expect(options).toEqual({
+ username: dummyUsername,
+ });
+
return Promise.resolve({
data: [dummyUser],
});
@@ -110,14 +114,18 @@ describe('UsersCache', () => {
it('returns undefined if Ajax call fails and cache is empty', done => {
const dummyError = new Error('server exploded');
+
apiSpy = (query, options) => {
expect(query).toBe('');
- expect(options).toEqual({ username: dummyUsername });
+ expect(options).toEqual({
+ username: dummyUsername,
+ });
+
return Promise.reject(dummyError);
};
UsersCache.retrieve(dummyUsername)
- .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`))
+ .then(user => done.fail(`Received unexpected user: ${JSON.stringify(user)}`))
.catch(error => {
expect(error).toBe(dummyError);
})
@@ -127,7 +135,8 @@ describe('UsersCache', () => {
it('makes no Ajax call if matching data exists', done => {
UsersCache.internalStorage[dummyUsername] = dummyUser;
- apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ apiSpy = () => done.fail(new Error('expected no Ajax call!'));
UsersCache.retrieve(dummyUsername)
.then(user => {
@@ -142,12 +151,13 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
- spyOn(Api, 'user').and.callFake(id => apiSpy(id));
+ jest.spyOn(Api, 'user').mockImplementation(id => apiSpy(id));
});
it('stores and returns data from API call if cache is empty', done => {
apiSpy = id => {
expect(id).toBe(dummyUserId);
+
return Promise.resolve({
data: dummyUser,
});
@@ -164,13 +174,15 @@ describe('UsersCache', () => {
it('returns undefined if Ajax call fails and cache is empty', done => {
const dummyError = new Error('server exploded');
+
apiSpy = id => {
expect(id).toBe(dummyUserId);
+
return Promise.reject(dummyError);
};
UsersCache.retrieveById(dummyUserId)
- .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`))
+ .then(user => done.fail(`Received unexpected user: ${JSON.stringify(user)}`))
.catch(error => {
expect(error).toBe(dummyError);
})
@@ -180,7 +192,8 @@ describe('UsersCache', () => {
it('makes no Ajax call if matching data exists', done => {
UsersCache.internalStorage[dummyUserId] = dummyUser;
- apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ apiSpy = () => done.fail(new Error('expected no Ajax call!'));
UsersCache.retrieveById(dummyUserId)
.then(user => {
@@ -195,12 +208,13 @@ describe('UsersCache', () => {
let apiSpy;
beforeEach(() => {
- spyOn(Api, 'userStatus').and.callFake(id => apiSpy(id));
+ jest.spyOn(Api, 'userStatus').mockImplementation(id => apiSpy(id));
});
it('stores and returns data from API call if cache is empty', done => {
apiSpy = id => {
expect(id).toBe(dummyUserId);
+
return Promise.resolve({
data: dummyUserStatus,
});
@@ -217,13 +231,15 @@ describe('UsersCache', () => {
it('returns undefined if Ajax call fails and cache is empty', done => {
const dummyError = new Error('server exploded');
+
apiSpy = id => {
expect(id).toBe(dummyUserId);
+
return Promise.reject(dummyError);
};
UsersCache.retrieveStatusById(dummyUserId)
- .then(userStatus => fail(`Received unexpected user: ${JSON.stringify(userStatus)}`))
+ .then(userStatus => done.fail(`Received unexpected user: ${JSON.stringify(userStatus)}`))
.catch(error => {
expect(error).toBe(dummyError);
})
@@ -232,8 +248,11 @@ describe('UsersCache', () => {
});
it('makes no Ajax call if matching data exists', done => {
- UsersCache.internalStorage[dummyUserId] = { status: dummyUserStatus };
- apiSpy = () => fail(new Error('expected no Ajax call!'));
+ UsersCache.internalStorage[dummyUserId] = {
+ status: dummyUserStatus,
+ };
+
+ apiSpy = () => done.fail(new Error('expected no Ajax call!'));
UsersCache.retrieveStatusById(dummyUserId)
.then(userStatus => {
diff --git a/spec/frontend/monitoring/charts/time_series_spec.js b/spec/frontend/monitoring/charts/time_series_spec.js
index c561c5edf3c..6fa2d15ccbf 100644
--- a/spec/frontend/monitoring/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/charts/time_series_spec.js
@@ -48,7 +48,7 @@ describe('Time series component', () => {
// Mock data contains 2 panels, pick the first one
store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload);
- [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].metrics;
+ [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].panels;
makeTimeSeriesChart = (graphData, type) =>
shallowMount(TimeSeries, {
@@ -235,7 +235,7 @@ describe('Time series component', () => {
});
it('utilizes all data points', () => {
- const { values } = mockGraphData.queries[0].result[0];
+ const { values } = mockGraphData.metrics[0].result[0];
expect(chartData.length).toBe(1);
expect(seriesData().data.length).toBe(values.length);
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index 6707d0b1fe8..38aef6e6052 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -17,8 +17,8 @@ const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent
const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => {
- const queries = anomalyMockResultValues[datasetName].map((values, index) => ({
- ...template.queries[index],
+ const metrics = anomalyMockResultValues[datasetName].map((values, index) => ({
+ ...template.metrics[index],
result: [
{
metrics: {},
@@ -26,7 +26,7 @@ const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => {
},
],
}));
- return { ...template, queries };
+ return { ...template, metrics };
};
describe('Anomaly chart component', () => {
@@ -67,19 +67,19 @@ describe('Anomaly chart component', () => {
describe('graph-data', () => {
it('receives a single "metric" series', () => {
const { graphData } = getTimeSeriesProps();
- expect(graphData.queries.length).toBe(1);
+ expect(graphData.metrics.length).toBe(1);
});
it('receives "metric" with all data', () => {
const { graphData } = getTimeSeriesProps();
- const query = graphData.queries[0];
- const expectedQuery = makeAnomalyGraphData(dataSetName).queries[0];
+ const query = graphData.metrics[0];
+ const expectedQuery = makeAnomalyGraphData(dataSetName).metrics[0];
expect(query).toEqual(expectedQuery);
});
it('receives the "metric" results', () => {
const { graphData } = getTimeSeriesProps();
- const { result } = graphData.queries[0];
+ const { result } = graphData.metrics[0];
const { values } = result[0];
const [metricDataset] = dataSet;
expect(values).toEqual(expect.any(Array));
@@ -266,12 +266,12 @@ describe('Anomaly chart component', () => {
describe('graph-data', () => {
it('receives a single "metric" series', () => {
const { graphData } = getTimeSeriesProps();
- expect(graphData.queries.length).toBe(1);
+ expect(graphData.metrics.length).toBe(1);
});
it('receives "metric" results and applies the offset to them', () => {
const { graphData } = getTimeSeriesProps();
- const { result } = graphData.queries[0];
+ const { result } = graphData.metrics[0];
const { values } = result[0];
const [metricDataset] = dataSet;
expect(values).toEqual(expect.any(Array));
diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js
index 3e22b0858e6..c5219f6130e 100644
--- a/spec/frontend/monitoring/embed/embed_spec.js
+++ b/spec/frontend/monitoring/embed/embed_spec.js
@@ -62,7 +62,7 @@ describe('Embed', () => {
describe('metrics are available', () => {
beforeEach(() => {
store.state.monitoringDashboard.dashboard.panel_groups = groups;
- store.state.monitoringDashboard.dashboard.panel_groups[0].metrics = metricsData;
+ store.state.monitoringDashboard.dashboard.panel_groups[0].panels = metricsData;
store.state.monitoringDashboard.metricsWithData = metricsWithData;
mountComponent();
diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js
index 1685021fd4b..8941c183807 100644
--- a/spec/frontend/monitoring/embed/mock_data.js
+++ b/spec/frontend/monitoring/embed/mock_data.js
@@ -42,38 +42,34 @@ export const metrics = [
},
];
-const queries = [
+const result = [
{
- result: [
- {
- values: [
- ['Mon', 1220],
- ['Tue', 932],
- ['Wed', 901],
- ['Thu', 934],
- ['Fri', 1290],
- ['Sat', 1330],
- ['Sun', 1320],
- ],
- },
+ values: [
+ ['Mon', 1220],
+ ['Tue', 932],
+ ['Wed', 901],
+ ['Thu', 934],
+ ['Fri', 1290],
+ ['Sat', 1330],
+ ['Sun', 1320],
],
},
];
export const metricsData = [
{
- queries,
metrics: [
{
metric_id: 15,
+ result,
},
],
},
{
- queries,
metrics: [
{
metric_id: 16,
+ result,
},
],
},
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index c42366ab484..758e86235be 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -110,9 +110,6 @@ export const anomalyMockGraphData = {
type: 'anomaly-chart',
weight: 3,
metrics: [
- // Not used
- ],
- queries: [
{
metricId: '90',
id: 'metric',
diff --git a/spec/frontend/monitoring/panel_type_spec.js b/spec/frontend/monitoring/panel_type_spec.js
index 54a63e7f61f..b30ad747a12 100644
--- a/spec/frontend/monitoring/panel_type_spec.js
+++ b/spec/frontend/monitoring/panel_type_spec.js
@@ -33,7 +33,7 @@ describe('Panel Type component', () => {
let glEmptyChart;
// Deep clone object before modifying
const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange));
- graphDataNoResult.queries[0].result = [];
+ graphDataNoResult.metrics[0].result = [];
beforeEach(() => {
panelType = shallowMount(PanelType, {
@@ -143,7 +143,7 @@ describe('Panel Type component', () => {
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
const header = `timestamp,${graphDataPrometheusQueryRange.y_label}`;
- const data = graphDataPrometheusQueryRange.queries[0].result[0].values;
+ const data = graphDataPrometheusQueryRange.metrics[0].result[0].values;
const firstRow = `${data[0][0]},${data[0][1]}`;
const secondRow = `${data[1][0]},${data[1][1]}`;
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index fdad290a8d6..42031e01878 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -27,7 +27,7 @@ describe('Monitoring mutations', () => {
it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
const expectedLabel = 'Pod average';
- const { label, query_range } = stateCopy.dashboard.panel_groups[0].metrics[0].metrics[0];
+ const { label, query_range } = stateCopy.dashboard.panel_groups[0].panels[0].metrics[0];
expect(label).toEqual(expectedLabel);
expect(query_range.length).toBeGreaterThan(0);
});
@@ -39,23 +39,12 @@ describe('Monitoring mutations', () => {
expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics.length).toEqual(1);
expect(stateCopy.dashboard.panel_groups[0].panels[1].metrics.length).toEqual(1);
});
- it('assigns queries a metric id', () => {
+ it('assigns metrics a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
- expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries[0].metricId).toEqual(
+ expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics[0].metricId).toEqual(
'17_system_metrics_kubernetes_container_memory_average',
);
});
- describe('dashboard endpoint', () => {
- const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
- it('aliases group panels to metrics for backwards compatibility', () => {
- mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
- expect(stateCopy.dashboard.panel_groups[0].metrics[0]).toBeDefined();
- });
- it('aliases panel metrics to queries for backwards compatibility', () => {
- mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
- expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries).toBeDefined();
- });
- });
});
describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 98388ac19f8..d562aaaefe9 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -1,44 +1,4 @@
-import { groupQueriesByChartInfo, normalizeMetric, uniqMetricsId } from '~/monitoring/stores/utils';
-
-describe('groupQueriesByChartInfo', () => {
- let input;
- let output;
-
- it('groups metrics with the same chart title and y_axis label', () => {
- input = [
- { title: 'title', y_label: 'MB', queries: [{}] },
- { title: 'title', y_label: 'MB', queries: [{}] },
- { title: 'new title', y_label: 'MB', queries: [{}] },
- ];
-
- output = [
- {
- title: 'title',
- y_label: 'MB',
- queries: [{ metricId: null }, { metricId: null }],
- },
- { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] },
- ];
-
- expect(groupQueriesByChartInfo(input)).toEqual(output);
- });
-
- // Functionality associated with the /additional_metrics endpoint
- it("associates a chart's stringified metric_id with the metric", () => {
- input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }];
- output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }];
-
- expect(groupQueriesByChartInfo(input)).toEqual(output);
- });
-
- // Functionality associated with the /metrics_dashboard endpoint
- it('aliases a stringified metrics_id on the metric to the metricId key', () => {
- input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }];
- output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }];
-
- expect(groupQueriesByChartInfo(input)).toEqual(output);
- });
-});
+import { normalizeMetric, uniqMetricsId } from '~/monitoring/stores/utils';
describe('normalizeMetric', () => {
[
@@ -54,7 +14,7 @@ describe('normalizeMetric', () => {
},
].forEach(({ args, expected }) => {
it(`normalizes metric to "${expected}" with args=${JSON.stringify(args)}`, () => {
- expect(normalizeMetric(...args)).toEqual({ metric_id: expected });
+ expect(normalizeMetric(...args)).toEqual({ metric_id: expected, metricId: expected });
});
});
});
diff --git a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
index 686029a28a9..6b7893cb523 100644
--- a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
+++ b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
@@ -42,4 +42,21 @@ describe('PerformanceBarStore', () => {
expect(findUrl('id')).toEqual('html5-boilerplate');
});
});
+
+ describe('setRequestDetailsData', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PerformanceBarStore();
+ });
+
+ it('updates correctly specific details', () => {
+ store.addRequest('id', 'https://gitlab.com/');
+ store.setRequestDetailsData('id', 'test', {
+ calls: 123,
+ });
+
+ expect(store.findRequest('id').details.test.calls).toEqual(123);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 38ffe98c79b..a8fddd5fff2 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -20,6 +20,7 @@ describe('pipeline graph action component', () => {
actionIcon: 'cancel',
},
sync: false,
+ attachToDocument: true,
});
});
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index 45ac278dd38..e211852f74b 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -1,9 +1,16 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import pipelineTriggerer from '~/pipelines/components/pipeline_triggerer.vue';
describe('Pipelines Triggerer', () => {
let wrapper;
+ const expectComponentWithProps = (Component, props = {}) => {
+ const componentWrapper = wrapper.find(Component);
+ expect(componentWrapper.isVisible()).toBe(true);
+ expect(componentWrapper.props()).toEqual(expect.objectContaining(props));
+ };
+
const mockData = {
pipeline: {
user: {
@@ -15,9 +22,10 @@ describe('Pipelines Triggerer', () => {
};
const createComponent = () => {
- wrapper = mount(pipelineTriggerer, {
+ wrapper = shallowMount(pipelineTriggerer, {
propsData: mockData,
sync: false,
+ attachToDocument: true,
});
};
@@ -33,14 +41,12 @@ describe('Pipelines Triggerer', () => {
expect(wrapper.contains('.table-section')).toBe(true);
});
- it('should render triggerer information when triggerer is provided', () => {
- const link = wrapper.find('.js-pipeline-url-user');
-
- expect(link.attributes('href')).toEqual(mockData.pipeline.user.path);
- expect(link.find('.js-user-avatar-image-toolip').text()).toEqual(mockData.pipeline.user.name);
- expect(link.find('img.avatar').attributes('src')).toEqual(
- `${mockData.pipeline.user.avatar_url}?width=26`,
- );
+ it('should pass triggerer information when triggerer is provided', () => {
+ expectComponentWithProps(UserAvatarLink, {
+ linkHref: mockData.pipeline.user.path,
+ tooltipText: mockData.pipeline.user.name,
+ imgSrc: mockData.pipeline.user.avatar_url,
+ });
});
it('should render "API" when no triggerer is provided', () => {
@@ -50,7 +56,7 @@ describe('Pipelines Triggerer', () => {
},
});
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
});
});
diff --git a/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/list/components/__snapshots__/group_empty_state_spec.js.snap
index 3f13b7d4d76..3f13b7d4d76 100644
--- a/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/registry/list/components/__snapshots__/group_empty_state_spec.js.snap
diff --git a/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap
index 3084462f5ae..3084462f5ae 100644
--- a/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
+++ b/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap
diff --git a/spec/frontend/registry/components/app_spec.js b/spec/frontend/registry/list/components/app_spec.js
index 63ef28c64f2..0013b27bd01 100644
--- a/spec/frontend/registry/components/app_spec.js
+++ b/spec/frontend/registry/list/components/app_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
-import registry from '~/registry/components/app.vue';
-import { TEST_HOST } from '../../helpers/test_constants';
+import registry from '~/registry/list/components/app.vue';
+import { TEST_HOST } from 'helpers/test_constants';
import { reposServerResponse, parsedReposServerResponse } from '../mock_data';
describe('Registry List', () => {
diff --git a/spec/frontend/registry/components/collapsible_container_spec.js b/spec/frontend/registry/list/components/collapsible_container_spec.js
index 2e471548a1b..cba49e72588 100644
--- a/spec/frontend/registry/components/collapsible_container_spec.js
+++ b/spec/frontend/registry/list/components/collapsible_container_spec.js
@@ -3,8 +3,8 @@ import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
import Tracking from '~/tracking';
-import collapsibleComponent from '~/registry/components/collapsible_container.vue';
-import * as getters from '~/registry/stores/getters';
+import collapsibleComponent from '~/registry/list/components/collapsible_container.vue';
+import * as getters from '~/registry/list/stores/getters';
import { repoPropsData } from '../mock_data';
jest.mock('~/flash.js');
@@ -140,43 +140,33 @@ describe('collapsible registry container', () => {
});
describe('tracking', () => {
- const category = 'mock_page';
+ const testTrackingCall = action => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
+ label: 'registry_repository_delete',
+ });
+ };
+
beforeEach(() => {
jest.spyOn(Tracking, 'event');
wrapper.vm.deleteItem = jest.fn().mockResolvedValue();
wrapper.vm.fetchRepos = jest.fn();
- wrapper.setData({
- tracking: {
- ...wrapper.vm.tracking,
- category,
- },
- });
});
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteBtn();
deleteBtn.trigger('click');
- expect(Tracking.event).toHaveBeenCalledWith(category, 'click_button', {
- label: 'registry_repository_delete',
- category,
- });
+ testTrackingCall('click_button');
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
- expect(Tracking.event).toHaveBeenCalledWith(category, 'cancel_delete', {
- label: 'registry_repository_delete',
- category,
- });
+ testTrackingCall('cancel_delete');
});
it('send an event when confirm is clicked on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
- expect(Tracking.event).toHaveBeenCalledWith(category, 'confirm_delete', {
- label: 'registry_repository_delete',
- category,
- });
+ testTrackingCall('confirm_delete');
});
});
});
diff --git a/spec/frontend/registry/components/group_empty_state_spec.js b/spec/frontend/registry/list/components/group_empty_state_spec.js
index f71074b5154..7541c3d459c 100644
--- a/spec/frontend/registry/components/group_empty_state_spec.js
+++ b/spec/frontend/registry/list/components/group_empty_state_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import groupEmptyState from '~/registry/components/group_empty_state.vue';
+import groupEmptyState from '~/registry/list/components/group_empty_state.vue';
describe('Registry Group Empty state', () => {
let wrapper;
diff --git a/spec/frontend/registry/components/project_empty_state_spec.js b/spec/frontend/registry/list/components/project_empty_state_spec.js
index dd0fe32b68c..bd717a4eb10 100644
--- a/spec/frontend/registry/components/project_empty_state_spec.js
+++ b/spec/frontend/registry/list/components/project_empty_state_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import projectEmptyState from '~/registry/components/project_empty_state.vue';
+import projectEmptyState from '~/registry/list/components/project_empty_state.vue';
describe('Registry Project Empty state', () => {
let wrapper;
diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/list/components/table_registry_spec.js
index 7147988ab61..1d31381c85b 100644
--- a/spec/frontend/registry/components/table_registry_spec.js
+++ b/spec/frontend/registry/list/components/table_registry_spec.js
@@ -3,9 +3,9 @@ import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import createFlash from '~/flash';
import Tracking from '~/tracking';
-import tableRegistry from '~/registry/components/table_registry.vue';
+import tableRegistry from '~/registry/list/components/table_registry.vue';
import { repoPropsData } from '../mock_data';
-import * as getters from '~/registry/stores/getters';
+import * as getters from '~/registry/list/stores/getters';
jest.mock('~/flash');
@@ -304,17 +304,14 @@ describe('table registry', () => {
});
describe('event tracking', () => {
- const mockPageName = 'mock_page';
+ const testTrackingCall = (action, label = 'registry_tag_delete') => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, action, { label, property: 'foo' });
+ };
beforeEach(() => {
jest.spyOn(Tracking, 'event');
wrapper.vm.handleSingleDelete = jest.fn();
wrapper.vm.handleMultipleDelete = jest.fn();
- document.body.dataset.page = mockPageName;
- });
-
- afterEach(() => {
- document.body.dataset.page = null;
});
describe('single tag delete', () => {
@@ -325,29 +322,25 @@ describe('table registry', () => {
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteButtonsRow();
deleteBtn.at(0).trigger('click');
- expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
- label: 'registry_tag_delete',
- property: 'foo',
- });
+
+ testTrackingCall('click_button');
});
+
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
- expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
- label: 'registry_tag_delete',
- property: 'foo',
- });
+
+ testTrackingCall('cancel_delete');
});
+
it('send an event when confirm is clicked on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
- expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
- label: 'registry_tag_delete',
- property: 'foo',
- });
+ testTrackingCall('confirm_delete');
});
});
+
describe('bulk tag delete', () => {
beforeEach(() => {
const items = [0, 1, 2];
@@ -357,27 +350,22 @@ describe('table registry', () => {
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteButton();
deleteBtn.vm.$emit('click');
- expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
- label: 'bulk_registry_tag_delete',
- property: 'foo',
- });
+
+ testTrackingCall('click_button', 'bulk_registry_tag_delete');
});
+
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
- expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
- label: 'bulk_registry_tag_delete',
- property: 'foo',
- });
+
+ testTrackingCall('cancel_delete', 'bulk_registry_tag_delete');
});
+
it('send an event when confirm is clicked on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
- expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
- label: 'bulk_registry_tag_delete',
- property: 'foo',
- });
+ testTrackingCall('confirm_delete', 'bulk_registry_tag_delete');
});
});
});
diff --git a/spec/frontend/registry/mock_data.js b/spec/frontend/registry/list/mock_data.js
index 130ab298e89..130ab298e89 100644
--- a/spec/frontend/registry/mock_data.js
+++ b/spec/frontend/registry/list/mock_data.js
diff --git a/spec/frontend/registry/stores/actions_spec.js b/spec/frontend/registry/list/stores/actions_spec.js
index 7937fa82e80..1a9c23ed188 100644
--- a/spec/frontend/registry/stores/actions_spec.js
+++ b/spec/frontend/registry/list/stores/actions_spec.js
@@ -1,9 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as actions from '~/registry/stores/actions';
-import * as types from '~/registry/stores/mutation_types';
-import { TEST_HOST } from '../../helpers/test_constants';
-import testAction from '../../helpers/vuex_action_helper';
+import * as actions from '~/registry/list/stores/actions';
+import * as types from '~/registry/list/stores/mutation_types';
+import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import {
diff --git a/spec/frontend/registry/stores/getters_spec.js b/spec/frontend/registry/list/stores/getters_spec.js
index c16f520223b..c8d054b226b 100644
--- a/spec/frontend/registry/stores/getters_spec.js
+++ b/spec/frontend/registry/list/stores/getters_spec.js
@@ -1,4 +1,4 @@
-import * as getters from '~/registry/stores/getters';
+import * as getters from '~/registry/list/stores/getters';
describe('Getters Registry Store', () => {
let state;
diff --git a/spec/frontend/registry/stores/mutations_spec.js b/spec/frontend/registry/list/stores/mutations_spec.js
index 1d583028ca6..f894f688c1f 100644
--- a/spec/frontend/registry/stores/mutations_spec.js
+++ b/spec/frontend/registry/list/stores/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/registry/stores/mutations';
-import * as types from '~/registry/stores/mutation_types';
+import mutations from '~/registry/list/stores/mutations';
+import * as types from '~/registry/list/stores/mutation_types';
import {
defaultState,
reposServerResponse,
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
new file mode 100644
index 00000000000..4fb6924daba
--- /dev/null
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -0,0 +1,60 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+describe('clipboard button', () => {
+ let wrapper;
+
+ const createWrapper = propsData => {
+ wrapper = shallowMount(ClipboardButton, {
+ propsData,
+ sync: false,
+ attachToDocument: true,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('without gfm', () => {
+ beforeEach(() => {
+ createWrapper({
+ text: 'copy me',
+ title: 'Copy this value',
+ cssClass: 'btn-danger',
+ });
+ });
+
+ it('renders a button for clipboard', () => {
+ expect(wrapper.find(GlButton).exists()).toBe(true);
+ expect(wrapper.attributes('data-clipboard-text')).toBe('copy me');
+ expect(wrapper.find(Icon).props('name')).toBe('duplicate');
+ });
+
+ it('should have a tooltip with default values', () => {
+ expect(wrapper.attributes('data-original-title')).toBe('Copy this value');
+ });
+
+ it('should render provided classname', () => {
+ expect(wrapper.classes()).toContain('btn-danger');
+ });
+ });
+
+ describe('with gfm', () => {
+ it('sets data-clipboard-text with gfm', () => {
+ createWrapper({
+ text: 'copy me',
+ gfm: '`path/to/file`',
+ title: 'Copy this value',
+ cssClass: 'btn-danger',
+ });
+
+ expect(wrapper.attributes('data-clipboard-text')).toBe(
+ '{"text":"copy me","gfm":"`path/to/file`"}',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index ec143fec5d9..52c0298603d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -1,24 +1,31 @@
-import Vue from 'vue';
-import $ from 'jquery';
+import { mount } from '@vue/test-utils';
+import { hexToRgb } from '~/lib/utils/color_utils';
+import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
+import DropdownValueScopedLabel from '~/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue';
-import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
import {
mockConfig,
mockLabels,
} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+const labelStyles = {
+ textColor: '#FFFFFF',
+ color: '#BADA55',
+};
const createComponent = (
labels = mockLabels,
labelFilterBasePath = mockConfig.labelFilterBasePath,
) => {
- const Component = Vue.extend(dropdownValueComponent);
-
- return mountComponent(Component, {
- labels,
- labelFilterBasePath,
- enableScopedLabels: true,
+ labels.forEach(label => Object.assign(label, labelStyles));
+
+ return mount(DropdownValueComponent, {
+ propsData: {
+ labels,
+ labelFilterBasePath,
+ enableScopedLabels: true,
+ },
+ attachToDocument: true,
+ sync: false,
});
};
@@ -30,7 +37,7 @@ describe('DropdownValueComponent', () => {
});
afterEach(() => {
- vm.$destroy();
+ vm.destroy();
});
describe('computed', () => {
@@ -38,12 +45,12 @@ describe('DropdownValueComponent', () => {
it('returns true if `labels` prop is empty', () => {
const vmEmptyLabels = createComponent([]);
- expect(vmEmptyLabels.isEmpty).toBe(true);
- vmEmptyLabels.$destroy();
+ expect(vmEmptyLabels.classes()).not.toContain('has-labels');
+ vmEmptyLabels.destroy();
});
it('returns false if `labels` prop is empty', () => {
- expect(vm.isEmpty).toBe(false);
+ expect(vm.classes()).toContain('has-labels');
});
});
});
@@ -51,88 +58,71 @@ describe('DropdownValueComponent', () => {
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
- expect(
- vm.labelFilterUrl({
- title: 'Foo bar',
- }),
- ).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar');
+ expect(vm.find(DropdownValueScopedLabel).props('labelFilterUrl')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar',
+ );
});
});
describe('labelStyle', () => {
it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
- const label = {
- textColor: '#FFFFFF',
- color: '#BADA55',
- };
- const styleObj = vm.labelStyle(label);
-
- expect(styleObj.color).toBe(label.textColor);
- expect(styleObj.backgroundColor).toBe(label.color);
- });
- });
-
- describe('scopedLabelsDescription', () => {
- it('returns html for tooltip', () => {
- const html = vm.scopedLabelsDescription(mockLabels[1]);
- const $el = $.parseHTML(html);
-
- expect($el[0]).toHaveClass('scoped-label-tooltip-title');
- expect($el[2].textContent).toEqual(mockLabels[1].description);
+ expect(vm.find(DropdownValueScopedLabel).props('labelStyle')).toEqual({
+ color: labelStyles.textColor,
+ backgroundColor: labelStyles.color,
+ });
});
});
describe('showScopedLabels', () => {
it('returns true if the label is scoped label', () => {
- expect(vm.showScopedLabels(mockLabels[1])).toBe(true);
- });
-
- it('returns false when label is a regular label', () => {
- expect(vm.showScopedLabels(mockLabels[0])).toBe(false);
+ expect(vm.findAll(DropdownValueScopedLabel).length).toEqual(1);
});
});
});
describe('template', () => {
it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
- expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(
- true,
- );
+ expect(vm.classes()).toContain('hide-collapsed', 'value', 'issuable-show-labels');
});
it('render slot content inside component when `labels` prop is empty', () => {
const vmEmptyLabels = createComponent([]);
- expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(
- mockConfig.emptyValueText,
- );
- vmEmptyLabels.$destroy();
+ expect(
+ vmEmptyLabels
+ .find('.text-secondary')
+ .text()
+ .trim(),
+ ).toBe(mockConfig.emptyValueText);
+ vmEmptyLabels.destroy();
});
it('renders label element with filter URL', () => {
- expect(vm.$el.querySelector('a').getAttribute('href')).toBe(
+ expect(vm.find('a').attributes('href')).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
});
it('renders label element and styles based on label details', () => {
- const labelEl = vm.$el.querySelector('a span.badge.color-label');
+ const labelEl = vm.find('a span.badge.color-label');
- expect(labelEl).not.toBeNull();
- expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
- expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
+ expect(labelEl.exists()).toBe(true);
+ expect(labelEl.attributes('style')).toContain(
+ `background-color: rgb(${hexToRgb(labelStyles.color).join(', ')});`,
+ );
+ expect(labelEl.text().trim()).toBe(mockLabels[0].title);
});
describe('label is of scoped-label type', () => {
it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
- expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull();
+ expect(vm.find('span.scoped-label-wrapper').exists()).toBe(true);
});
it('renders anchor tag containing question icon', () => {
- const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label');
+ const anchor = vm.find('.scoped-label-wrapper a.scoped-label');
- expect(anchor).not.toBeNull();
- expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull();
+ expect(anchor.exists()).toBe(true);
+ expect(anchor.find('i.fa-question-circle').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
new file mode 100644
index 00000000000..bdd18110629
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -0,0 +1,113 @@
+import _ from 'underscore';
+import { trimText } from 'helpers/text_helper';
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import { TEST_HOST } from 'spec/test_constants';
+
+describe('User Avatar Link Component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ linkHref: `${TEST_HOST}/myavatarurl.com`,
+ imgSize: 99,
+ imgSrc: `${TEST_HOST}/myavatarurl.com`,
+ imgAlt: 'mydisplayname',
+ imgCssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ username: 'username',
+ };
+
+ const createWrapper = props => {
+ wrapper = shallowMount(UserAvatarLink, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ sync: false,
+ attachToDocument: true,
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should return a defined Vue component', () => {
+ expect(wrapper.isVueInstance()).toBe(true);
+ });
+
+ it('should have user-avatar-image registered as child component', () => {
+ expect(wrapper.vm.$options.components.userAvatarImage).toBeDefined();
+ });
+
+ it('user-avatar-link should have user-avatar-image as child component', () => {
+ expect(wrapper.find(UserAvatarImage).exists()).toBe(true);
+ });
+
+ it('should render GlLink as a child element', () => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(defaultProps.linkHref);
+ });
+
+ it('should return necessary props as defined', () => {
+ _.each(defaultProps, (val, key) => {
+ expect(wrapper.vm[key]).toBeDefined();
+ });
+ });
+
+ describe('no username', () => {
+ beforeEach(() => {
+ createWrapper({
+ username: '',
+ });
+ });
+
+ it('should only render image tag in link', () => {
+ const childElements = wrapper.vm.$el.childNodes;
+
+ expect(wrapper.find('img')).not.toBe('null');
+
+ // Vue will render the hidden component as <!---->
+ expect(childElements[1].tagName).toBeUndefined();
+ });
+
+ it('should render avatar image tooltip', () => {
+ expect(wrapper.vm.shouldShowUsername).toBe(false);
+ expect(wrapper.vm.avatarTooltipText).toEqual(defaultProps.tooltipText);
+ });
+ });
+
+ describe('username', () => {
+ it('should not render avatar image tooltip', () => {
+ expect(wrapper.find('.js-user-avatar-image-toolip').exists()).toBe(false);
+ });
+
+ it('should render username prop in <span>', () => {
+ expect(trimText(wrapper.find('.js-user-avatar-link-username').text())).toEqual(
+ defaultProps.username,
+ );
+ });
+
+ it('should render text tooltip for <span>', () => {
+ expect(
+ wrapper.find('.js-user-avatar-link-username').attributes('data-original-title'),
+ ).toEqual(defaultProps.tooltipText);
+ });
+
+ it('should render text tooltip placement for <span>', () => {
+ expect(wrapper.find('.js-user-avatar-link-username').attributes('tooltip-placement')).toBe(
+ defaultProps.tooltipPlacement,
+ );
+ });
+ });
+});
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index d3d25d3cb74..a0c85863150 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -235,4 +235,88 @@ describe ApplicationHelper do
end
end
end
+
+ describe '#body_data' do
+ context 'when @project is not set' do
+ it 'does not include project data in the body data elements' do
+ expect(helper.body_data).to eq(
+ {
+ page: 'application',
+ page_type_id: nil,
+ find_file: nil,
+ group: ''
+ }
+ )
+ end
+
+ context 'when @group is set' do
+ it 'sets group in the body data elements' do
+ group = create(:group)
+
+ assign(:group, group)
+
+ expect(helper.body_data).to eq(
+ {
+ page: 'application',
+ page_type_id: nil,
+ find_file: nil,
+ group: group.path
+ }
+ )
+ end
+ end
+ end
+
+ context 'when @project is set' do
+ it 'includes all possible body data elements and associates the project elements with project' do
+ project = create(:project)
+
+ assign(:project, project)
+
+ expect(helper.body_data).to eq(
+ {
+ page: 'application',
+ page_type_id: nil,
+ find_file: nil,
+ group: '',
+ project_id: project.id,
+ project: project.name,
+ namespace_id: project.namespace.id
+ }
+ )
+ end
+
+ context 'when controller is issues' do
+ before do
+ stub_controller_method(:controller_path, 'projects:issues')
+ end
+
+ context 'when params[:id] is present and the issue exsits and action_name is show' do
+ it 'sets all project and id elements correctly related to the issue' do
+ issue = create(:issue)
+ stub_controller_method(:action_name, 'show')
+ stub_controller_method(:params, { id: issue.id })
+
+ assign(:project, issue.project)
+
+ expect(helper.body_data).to eq(
+ {
+ page: 'projects:issues:show',
+ page_type_id: issue.id,
+ find_file: nil,
+ group: '',
+ project_id: issue.project.id,
+ project: issue.project.name,
+ namespace_id: issue.project.namespace.id
+ }
+ )
+ end
+ end
+ end
+ end
+
+ def stub_controller_method(method_name, value)
+ allow(helper.controller).to receive(method_name).and_return(value)
+ end
+ end
end
diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb
index 2ee27bc5427..2ad6b68a34c 100644
--- a/spec/helpers/award_emoji_helper_spec.rb
+++ b/spec/helpers/award_emoji_helper_spec.rb
@@ -4,59 +4,69 @@ require 'spec_helper'
describe AwardEmojiHelper do
describe '.toggle_award_url' do
+ subject { helper.toggle_award_url(awardable) }
+
context 'note on personal snippet' do
- let(:note) { create(:note_on_personal_snippet) }
+ let(:snippet) { create(:personal_snippet) }
+ let(:note) { create(:note_on_personal_snippet, noteable: snippet) }
+ let(:awardable) { note }
+
+ subject { helper.toggle_award_url(note) }
it 'returns correct url' do
expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji"
- expect(helper.toggle_award_url(note)).to eq(expected_url)
+ expect(subject).to eq(expected_url)
end
end
context 'note on project item' do
let(:note) { create(:note_on_project_snippet) }
+ let(:awardable) { note }
it 'returns correct url' do
@project = note.noteable.project
expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji"
- expect(helper.toggle_award_url(note)).to eq(expected_url)
+ expect(subject).to eq(expected_url)
end
end
context 'personal snippet' do
let(:snippet) { create(:personal_snippet) }
+ let(:awardable) { snippet }
it 'returns correct url' do
expected_url = "/snippets/#{snippet.id}/toggle_award_emoji"
- expect(helper.toggle_award_url(snippet)).to eq(expected_url)
+ expect(subject).to eq(expected_url)
end
end
context 'merge request' do
let(:merge_request) { create(:merge_request) }
+ let(:awardable) { merge_request }
it 'returns correct url' do
@project = merge_request.project
expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.iid}/toggle_award_emoji"
- expect(helper.toggle_award_url(merge_request)).to eq(expected_url)
+ expect(subject).to eq(expected_url)
end
end
context 'issue' do
let(:issue) { create(:issue) }
+ let(:awardable) { issue }
it 'returns correct url' do
@project = issue.project
expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.iid}/toggle_award_emoji"
- expect(helper.toggle_award_url(issue)).to eq(expected_url)
+ expect(subject).to eq(expected_url)
end
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 47c076e3322..def3078c652 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -258,6 +258,51 @@ describe DiffHelper do
end
end
+ context '#render_overflow_warning?' do
+ let(:diffs_collection) { instance_double(Gitlab::Diff::FileCollection::MergeRequestDiff, diff_files: diff_files) }
+ let(:diff_files) { Gitlab::Git::DiffCollection.new(files) }
+ let(:safe_file) { { too_large: false, diff: '' } }
+ let(:large_file) { { too_large: true, diff: '' } }
+ let(:files) { [safe_file, safe_file] }
+
+ before do
+ allow(diff_files).to receive(:overflow?).and_return(false)
+ end
+
+ context 'when neither collection nor individual file hit the limit' do
+ it 'returns false and does not log any overflow events' do
+ expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collection_limits)
+ expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_single_file_limits)
+
+ expect(render_overflow_warning?(diffs_collection)).to be false
+ end
+ end
+
+ context 'when the file collection has an overflow' do
+ before do
+ allow(diff_files).to receive(:overflow?).and_return(true)
+ end
+
+ it 'returns false and only logs collection overflow event' do
+ expect(Gitlab::Metrics).to receive(:add_event).with(:diffs_overflow_collection_limits).exactly(:once)
+ expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_single_file_limits)
+
+ expect(render_overflow_warning?(diffs_collection)).to be true
+ end
+ end
+
+ context 'when two individual files are too big' do
+ let(:files) { [safe_file, large_file, large_file] }
+
+ it 'returns false and only logs single file overflow events' do
+ expect(Gitlab::Metrics).to receive(:add_event).with(:diffs_overflow_single_file_limits).exactly(:once)
+ expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collection_limits)
+
+ expect(render_overflow_warning?(diffs_collection)).to be false
+ end
+ end
+ end
+
context '#diff_file_path_text' do
it 'returns full path by default' do
expect(diff_file_path_text(diff_file)).to eq(diff_file.new_path)
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 38699108b06..9cc1a9dfd5b 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -112,4 +112,98 @@ describe GitlabRoutingHelper do
expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/-/milestones/#{milestone.iid}/edit")
end
end
+
+ context 'snippets' do
+ let_it_be(:personal_snippet) { create(:personal_snippet) }
+ let_it_be(:project_snippet) { create(:project_snippet) }
+ let_it_be(:note) { create(:note_on_personal_snippet, noteable: personal_snippet) }
+
+ describe '#snippet_path' do
+ it 'returns the personal snippet path' do
+ expect(snippet_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}")
+ end
+
+ it 'returns the project snippet path' do
+ expect(snippet_path(project_snippet)).to eq("/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}")
+ end
+ end
+
+ describe '#snippet_url' do
+ it 'returns the personal snippet url' do
+ expect(snippet_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}")
+ end
+
+ it 'returns the project snippet url' do
+ expect(snippet_url(project_snippet)).to eq("#{Settings.gitlab['url']}/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}")
+ end
+ end
+
+ describe '#raw_snippet_path' do
+ it 'returns the raw personal snippet path' do
+ expect(raw_snippet_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/raw")
+ end
+
+ it 'returns the raw project snippet path' do
+ expect(raw_snippet_path(project_snippet)).to eq("/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}/raw")
+ end
+ end
+
+ describe '#raw_snippet_url' do
+ it 'returns the raw personal snippet url' do
+ expect(raw_snippet_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/raw")
+ end
+
+ it 'returns the raw project snippet url' do
+ expect(raw_snippet_url(project_snippet)).to eq("#{Settings.gitlab['url']}/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}/raw")
+ end
+ end
+
+ describe '#snippet_notes_path' do
+ it 'returns the notes path for the personal snippet' do
+ expect(snippet_notes_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/notes")
+ end
+ end
+
+ describe '#snippet_notes_url' do
+ it 'returns the notes url for the personal snippet' do
+ expect(snippet_notes_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/notes")
+ end
+ end
+
+ describe '#snippet_note_path' do
+ it 'returns the note path for the personal snippet' do
+ expect(snippet_note_path(personal_snippet, note)).to eq("/snippets/#{personal_snippet.id}/notes/#{note.id}")
+ end
+ end
+
+ describe '#snippet_note_url' do
+ it 'returns the note url for the personal snippet' do
+ expect(snippet_note_url(personal_snippet, note)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/notes/#{note.id}")
+ end
+ end
+
+ describe '#toggle_award_emoji_snippet_note_path' do
+ it 'returns the note award emoji path for the personal snippet' do
+ expect(toggle_award_emoji_snippet_note_path(personal_snippet, note)).to eq("/snippets/#{personal_snippet.id}/notes/#{note.id}/toggle_award_emoji")
+ end
+ end
+
+ describe '#toggle_award_emoji_snippet_note_url' do
+ it 'returns the note award emoji url for the personal snippet' do
+ expect(toggle_award_emoji_snippet_note_url(personal_snippet, note)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/notes/#{note.id}/toggle_award_emoji")
+ end
+ end
+
+ describe '#toggle_award_emoji_snippet_path' do
+ it 'returns the award emoji path for the personal snippet' do
+ expect(toggle_award_emoji_snippet_path(personal_snippet)).to eq("/snippets/#{personal_snippet.id}/toggle_award_emoji")
+ end
+ end
+
+ describe '#toggle_award_emoji_snippet_url' do
+ it 'returns the award url for the personal snippet' do
+ expect(toggle_award_emoji_snippet_url(personal_snippet)).to eq("#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}/toggle_award_emoji")
+ end
+ end
+ end
end
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index d88e151a11c..2a24950502b 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -9,107 +9,18 @@ describe SnippetsHelper do
let_it_be(:public_personal_snippet) { create(:personal_snippet, :public) }
let_it_be(:public_project_snippet) { create(:project_snippet, :public) }
- describe '#reliable_snippet_path' do
- subject { reliable_snippet_path(snippet) }
-
- context 'personal snippets' do
- let(:snippet) { public_personal_snippet }
-
- context 'public' do
- it 'returns a full path' do
- expect(subject).to eq("/snippets/#{snippet.id}")
- end
- end
- end
-
- context 'project snippets' do
- let(:snippet) { public_project_snippet }
-
- it 'returns a full path' do
- expect(subject).to eq("/#{snippet.project.full_path}/snippets/#{snippet.id}")
- end
- end
- end
-
- describe '#reliable_snippet_url' do
- subject { reliable_snippet_url(snippet) }
-
- context 'personal snippets' do
- let(:snippet) { public_personal_snippet }
-
- context 'public' do
- it 'returns a full url' do
- expect(subject).to eq("http://test.host/snippets/#{snippet.id}")
- end
- end
- end
-
- context 'project snippets' do
- let(:snippet) { public_project_snippet }
-
- it 'returns a full url' do
- expect(subject).to eq("http://test.host/#{snippet.project.full_path}/snippets/#{snippet.id}")
- end
- end
- end
-
- describe '#reliable_raw_snippet_path' do
- subject { reliable_raw_snippet_path(snippet) }
-
- context 'personal snippets' do
- let(:snippet) { public_personal_snippet }
-
- context 'public' do
- it 'returns a full path' do
- expect(subject).to eq("/snippets/#{snippet.id}/raw")
- end
- end
- end
-
- context 'project snippets' do
- let(:snippet) { public_project_snippet }
-
- it 'returns a full path' do
- expect(subject).to eq("/#{snippet.project.full_path}/snippets/#{snippet.id}/raw")
- end
- end
- end
-
- describe '#reliable_raw_snippet_url' do
- subject { reliable_raw_snippet_url(snippet) }
-
- context 'personal snippets' do
- let(:snippet) { public_personal_snippet }
-
- context 'public' do
- it 'returns a full url' do
- expect(subject).to eq("http://test.host/snippets/#{snippet.id}/raw")
- end
- end
- end
-
- context 'project snippets' do
- let(:snippet) { public_project_snippet }
-
- it 'returns a full url' do
- expect(subject).to eq("http://test.host/#{snippet.project.full_path}/snippets/#{snippet.id}/raw")
- end
- end
- end
-
describe '#embedded_raw_snippet_button' do
subject { embedded_raw_snippet_button.to_s }
it 'returns view raw button of embedded snippets for personal snippets' do
@snippet = create(:personal_snippet, :public)
-
- expect(subject).to eq(download_link("http://test.host/snippets/#{@snippet.id}/raw"))
+ expect(subject).to eq(download_link("#{Settings.gitlab['url']}/snippets/#{@snippet.id}/raw"))
end
it 'returns view raw button of embedded snippets for project snippets' do
@snippet = create(:project_snippet, :public)
- expect(subject).to eq(download_link("http://test.host/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw"))
+ expect(subject).to eq(download_link("#{Settings.gitlab['url']}/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw"))
end
def download_link(url)
@@ -123,13 +34,13 @@ describe SnippetsHelper do
it 'returns download button of embedded snippets for personal snippets' do
@snippet = create(:personal_snippet, :public)
- expect(subject).to eq(download_link("http://test.host/snippets/#{@snippet.id}/raw"))
+ expect(subject).to eq(download_link("#{Settings.gitlab['url']}/snippets/#{@snippet.id}/raw"))
end
it 'returns download button of embedded snippets for project snippets' do
@snippet = create(:project_snippet, :public)
- expect(subject).to eq(download_link("http://test.host/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw"))
+ expect(subject).to eq(download_link("#{Settings.gitlab['url']}/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw"))
end
def download_link(url)
@@ -145,7 +56,7 @@ describe SnippetsHelper do
context 'public' do
it 'returns a script tag with the snippet full url' do
- expect(subject).to eq(script_embed("http://test.host/snippets/#{snippet.id}"))
+ expect(subject).to eq(script_embed("#{Settings.gitlab['url']}/snippets/#{snippet.id}"))
end
end
end
@@ -154,7 +65,7 @@ describe SnippetsHelper do
let(:snippet) { public_project_snippet }
it 'returns a script tag with the snippet full url' do
- expect(subject).to eq(script_embed("http://test.host/#{snippet.project.path_with_namespace}/snippets/#{snippet.id}"))
+ expect(subject).to eq(script_embed("#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{snippet.id}"))
end
end
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 51433a58212..72367377929 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -13,7 +13,7 @@ import '~/boards/models/list';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
-import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
+import { listObj, boardsMockInterceptor, setMockEndpoints } from './mock_data';
describe('Board card', () => {
let vm;
@@ -22,8 +22,8 @@ describe('Board card', () => {
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onAny().reply(boardsMockInterceptor);
+ setMockEndpoints();
- gl.boardService = mockBoardService();
boardsStore.create();
boardsStore.detail.issue = {};
diff --git a/spec/javascripts/boards/board_list_common_spec.js b/spec/javascripts/boards/board_list_common_spec.js
index ada7589b795..a92d885790d 100644
--- a/spec/javascripts/boards/board_list_common_spec.js
+++ b/spec/javascripts/boards/board_list_common_spec.js
@@ -9,7 +9,7 @@ import BoardList from '~/boards/components/board_list.vue';
import '~/boards/models/issue';
import '~/boards/models/list';
-import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
+import { listObj, boardsMockInterceptor } from './mock_data';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
@@ -26,7 +26,6 @@ export default function createComponent({
document.body.appendChild(el);
const mock = new MockAdapter(axios);
mock.onAny().reply(boardsMockInterceptor);
- gl.boardService = mockBoardService();
boardsStore.create();
const BoardListComp = Vue.extend(BoardList);
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index 76675a78db2..8e4093cc25c 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -7,7 +7,7 @@ import boardNewIssue from '~/boards/components/board_new_issue.vue';
import boardsStore from '~/boards/stores/boards_store';
import '~/boards/models/list';
-import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
+import { listObj, boardsMockInterceptor } from './mock_data';
describe('Issue boards new issue form', () => {
let vm;
@@ -36,7 +36,6 @@ describe('Issue boards new issue form', () => {
mock = new MockAdapter(axios);
mock.onAny().reply(boardsMockInterceptor);
- gl.boardService = mockBoardService();
boardsStore.create();
list = new List(listObj);
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index ccb657e0df1..86a2a10b7a0 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import Board from '~/boards/components/board';
import List from '~/boards/models/list';
-import { mockBoardService } from '../mock_data';
describe('Board component', () => {
let vm;
@@ -35,13 +34,6 @@ describe('Board component', () => {
const setUpTests = (done, opts = {}) => {
loadFixtures('boards/show.html');
- gl.boardService = mockBoardService({
- boardsEndpoint: '/',
- listsEndpoint: '/',
- bulkUpdatePath: '/',
- boardId: 1,
- });
-
createComponent(opts);
Vue.nextTick(done);
@@ -61,15 +53,6 @@ describe('Board component', () => {
};
describe('List', () => {
- beforeEach(() => {
- gl.boardService = mockBoardService({
- boardsEndpoint: '/',
- listsEndpoint: '/',
- bulkUpdatePath: '/',
- boardId: 1,
- });
- });
-
it('board is expandable when list type is closed', () => {
expect(new List({ id: 1, list_type: 'closed' }).isExpandable).toBe(true);
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 05e6ea1394d..890a47c189a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -7,13 +7,13 @@ import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
-import { mockBoardService } from './mock_data';
+import { setMockEndpoints } from './mock_data';
describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = mockBoardService();
+ setMockEndpoints();
boardsStore.create();
issue = new ListIssue({
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 41b8f567e08..bd295ba61ac 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,6 +1,20 @@
import BoardService from '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
+export const setMockEndpoints = (opts = {}) => {
+ const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
+ const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
+ const bulkUpdatePath = opts.bulkUpdatePath || '';
+ const boardId = opts.boardId || '1';
+
+ boardsStore.setEndpoints({
+ boardsEndpoint,
+ listsEndpoint,
+ bulkUpdatePath,
+ boardId,
+ });
+};
+
export const boardObj = {
id: 1,
name: 'test',
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
deleted file mode 100644
index ef4bb470734..00000000000
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import Vue from 'vue';
-import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
-import { createStore } from '~/mr_notes/stores';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import diffsMockData from '../mock_data/merge_request_diffs';
-import getDiffWithCommit from '../mock_data/diff_with_commit';
-
-describe('CompareVersions', () => {
- let vm;
- const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
-
- beforeEach(() => {
- const store = createStore();
-
- store.state.diffs.addedLines = 10;
- store.state.diffs.removedLines = 20;
- store.state.diffs.diffFiles.push('test');
-
- vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, {
- mergeRequestDiffs: diffsMockData,
- mergeRequestDiff: diffsMockData[0],
- targetBranch,
- }).$mount();
- });
-
- describe('template', () => {
- it('should render Tree List toggle button with correct attribute values', () => {
- const treeListBtn = vm.$el.querySelector('.js-toggle-tree-list');
-
- expect(treeListBtn).not.toBeNull();
- expect(treeListBtn.dataset.originalTitle).toBe('Hide file browser');
- expect(treeListBtn.querySelectorAll('svg use').length).not.toBe(0);
- expect(treeListBtn.querySelector('svg use').getAttribute('xlink:href')).toContain(
- '#collapse-left',
- );
- });
-
- it('should render comparison dropdowns with correct values', () => {
- const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown');
- const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown');
-
- expect(sourceDropdown).not.toBeNull();
- expect(targetDropdown).not.toBeNull();
- expect(sourceDropdown.querySelector('a span').innerHTML).toContain('latest version');
- expect(targetDropdown.querySelector('a span').innerHTML).toContain(targetBranch.branchName);
- });
-
- it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
- vm.mergeRequestDiffs = [];
-
- vm.$nextTick(() => {
- const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown');
- const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown');
-
- expect(sourceDropdown).toBeNull();
- expect(targetDropdown).toBeNull();
- });
- });
-
- it('should render view types buttons with correct values', () => {
- const inlineBtn = vm.$el.querySelector('#inline-diff-btn');
- const parallelBtn = vm.$el.querySelector('#parallel-diff-btn');
-
- expect(inlineBtn).not.toBeNull();
- expect(parallelBtn).not.toBeNull();
- expect(inlineBtn.dataset.viewType).toEqual('inline');
- expect(parallelBtn.dataset.viewType).toEqual('parallel');
- expect(inlineBtn.innerHTML).toContain('Inline');
- expect(parallelBtn.innerHTML).toContain('Side-by-side');
- });
-
- it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
- vm.isLimitedContainer = true;
-
- vm.$nextTick(() => {
- const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width');
-
- expect(limitedContainer).not.toBeNull();
- });
- });
-
- it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
- vm.isLimitedContainer = false;
-
- vm.$nextTick(() => {
- const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width');
-
- expect(limitedContainer).toBeNull();
- });
- });
- });
-
- describe('setInlineDiffViewType', () => {
- it('should persist the view type in the url', () => {
- const viewTypeBtn = vm.$el.querySelector('#inline-diff-btn');
- viewTypeBtn.click();
-
- expect(window.location.toString()).toContain('?view=inline');
- });
- });
-
- describe('setParallelDiffViewType', () => {
- it('should persist the view type in the url', () => {
- const viewTypeBtn = vm.$el.querySelector('#parallel-diff-btn');
- viewTypeBtn.click();
-
- expect(window.location.toString()).toContain('?view=parallel');
- });
- });
-
- describe('comparableDiffs', () => {
- it('should not contain the first item in the mergeRequestDiffs property', () => {
- const { comparableDiffs } = vm;
- const comparableDiffsMock = diffsMockData.slice(1);
-
- expect(comparableDiffs).toEqual(comparableDiffsMock);
- });
- });
-
- describe('baseVersionPath', () => {
- it('should be set correctly from mergeRequestDiff', () => {
- expect(vm.baseVersionPath).toEqual(vm.mergeRequestDiff.base_version_path);
- });
- });
-
- describe('commit', () => {
- beforeEach(done => {
- vm.$store.state.diffs.commit = getDiffWithCommit().commit;
- vm.mergeRequestDiffs = [];
-
- vm.$nextTick(done);
- });
-
- it('renders latest version button', () => {
- expect(vm.$el.querySelector('.js-latest-version').textContent.trim()).toBe(
- 'Show latest version',
- );
- });
-
- it('renders short commit ID', () => {
- expect(vm.$el.textContent).toContain('Viewing commit');
- expect(vm.$el.textContent).toContain(vm.commit.short_id);
- });
- });
-});
diff --git a/spec/javascripts/diffs/mock_data/diff_with_commit.js b/spec/javascripts/diffs/mock_data/diff_with_commit.js
index d646294ee84..c36b0239060 100644
--- a/spec/javascripts/diffs/mock_data/diff_with_commit.js
+++ b/spec/javascripts/diffs/mock_data/diff_with_commit.js
@@ -1,7 +1,7 @@
-const FIXTURE = 'merge_request_diffs/with_commit.json';
+// No new code should be added to this file. Instead, modify the
+// file this one re-exports from. For more detail about why, see:
+// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-preloadFixtures(FIXTURE);
+import getDiffWithCommit from '../../../frontend/diffs/mock_data/diff_with_commit';
-export default function getDiffWithCommit() {
- return getJSONFixture(FIXTURE);
-}
+export default getDiffWithCommit;
diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
index 4bbef146336..de29eb7e560 100644
--- a/spec/javascripts/diffs/mock_data/merge_request_diffs.js
+++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
@@ -1,46 +1,7 @@
-export default [
- {
- base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
- version_index: 4,
- created_at: '2018-10-23T11:49:16.611Z',
- commits_count: 4,
- latest: true,
- short_commit_sha: 'de7a8f7f',
- version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
- compare_path:
- '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995',
- },
- {
- base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36',
- version_index: 3,
- created_at: '2018-10-23T11:46:40.617Z',
- commits_count: 3,
- latest: false,
- short_commit_sha: 'e78fc18f',
- version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36',
- compare_path:
- '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e',
- },
- {
- base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35',
- version_index: 2,
- created_at: '2018-10-04T09:57:39.648Z',
- commits_count: 2,
- latest: false,
- short_commit_sha: '48da7e7e',
- version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35',
- compare_path:
- '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e',
- },
- {
- base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20',
- version_index: 1,
- created_at: '2018-09-25T20:30:39.493Z',
- commits_count: 1,
- latest: false,
- short_commit_sha: '47bac2ed',
- version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20',
- compare_path:
- '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0',
- },
-];
+// No new code should be added to this file. Instead, modify the
+// file this one re-exports from. For more detail about why, see:
+// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
+
+import diffsMockData from '../../../frontend/diffs/mock_data/merge_request_diffs';
+
+export default diffsMockData;
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 4ab9a3998c0..09209ba2513 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -2,6 +2,32 @@ import { format } from 'timeago.js';
import Vue from 'vue';
import environmentItemComp from '~/environments/components/environment_item.vue';
+const tableData = {
+ name: {
+ title: 'Environment',
+ spacing: 'section-15',
+ },
+ deploy: {
+ title: 'Deployment',
+ spacing: 'section-10',
+ },
+ build: {
+ title: 'Job',
+ spacing: 'section-15',
+ },
+ commit: {
+ title: 'Commit',
+ spacing: 'section-20',
+ },
+ date: {
+ title: 'Updated',
+ spacing: 'section-10',
+ },
+ actions: {
+ spacing: 'section-25',
+ },
+};
+
describe('Environment item', () => {
let EnvironmentItem;
@@ -27,6 +53,7 @@ describe('Environment item', () => {
propsData: {
model: mockItem,
canReadEnvironment: true,
+ tableData,
},
}).$mount();
});
@@ -119,6 +146,7 @@ describe('Environment item', () => {
propsData: {
model: environment,
canReadEnvironment: true,
+ tableData,
},
}).$mount();
});
diff --git a/spec/javascripts/monitoring/charts/column_spec.js b/spec/javascripts/monitoring/charts/column_spec.js
index 27b3d435f08..9676617e8e1 100644
--- a/spec/javascripts/monitoring/charts/column_spec.js
+++ b/spec/javascripts/monitoring/charts/column_spec.js
@@ -11,7 +11,7 @@ describe('Column component', () => {
columnChart = shallowMount(localVue.extend(ColumnChart), {
propsData: {
graphData: {
- queries: [
+ metrics: [
{
x_label: 'Time',
y_label: 'Usage',
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index f9cc839bde6..f59c4ee4264 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -105,22 +105,11 @@ export const graphDataPrometheusQuery = {
metrics: [
{
id: 'metric_a1',
- metric_id: 2,
+ metricId: '2',
query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
unit: 'MB',
label: 'Total Consumption',
- prometheus_endpoint_path:
- '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
- },
- ],
- queries: [
- {
- metricId: null,
- id: 'metric_a1',
metric_id: 2,
- query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024',
- unit: 'MB',
- label: 'Total Consumption',
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
result: [
@@ -140,24 +129,12 @@ export const graphDataPrometheusQueryRange = {
metrics: [
{
id: 'metric_a1',
- metric_id: 2,
+ metricId: '2',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
unit: 'MB',
label: 'Total Consumption',
- prometheus_endpoint_path:
- '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
- },
- ],
- queries: [
- {
- metricId: '10',
- id: 'metric_a1',
metric_id: 2,
- query_range:
- 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
- unit: 'MB',
- label: 'Total Consumption',
prometheus_endpoint_path:
'/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024',
result: [
@@ -176,8 +153,7 @@ export const graphDataPrometheusQueryRangeMultiTrack = {
weight: 3,
x_label: 'Status Code',
y_label: 'Time',
- metrics: [],
- queries: [
+ metrics: [
{
metricId: '1',
id: 'response_metrics_nginx_ingress_throughput_status_code',
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
index 202b4ec8f2e..3459b44c7ec 100644
--- a/spec/javascripts/monitoring/utils_spec.js
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -314,32 +314,32 @@ describe('isDateTimePickerInputValid', () => {
});
describe('graphDataValidatorForAnomalyValues', () => {
- let oneQuery;
- let threeQueries;
- let fourQueries;
+ let oneMetric;
+ let threeMetrics;
+ let fourMetrics;
beforeEach(() => {
- oneQuery = graphDataPrometheusQuery;
- threeQueries = anomalyMockGraphData;
+ oneMetric = graphDataPrometheusQuery;
+ threeMetrics = anomalyMockGraphData;
- const queries = [...threeQueries.queries];
- queries.push(threeQueries.queries[0]);
- fourQueries = {
+ const metrics = [...threeMetrics.metrics];
+ metrics.push(threeMetrics.metrics[0]);
+ fourMetrics = {
...anomalyMockGraphData,
- queries,
+ metrics,
};
});
/*
- * Anomaly charts can accept results for exactly 3 queries,
+ * Anomaly charts can accept results for exactly 3 metrics,
*/
it('validates passes with the right query format', () => {
- expect(graphDataValidatorForAnomalyValues(threeQueries)).toBe(true);
+ expect(graphDataValidatorForAnomalyValues(threeMetrics)).toBe(true);
});
it('validation fails for wrong format, 1 metric', () => {
- expect(graphDataValidatorForAnomalyValues(oneQuery)).toBe(false);
+ expect(graphDataValidatorForAnomalyValues(oneMetric)).toBe(false);
});
it('validation fails for wrong format, more than 3 metrics', () => {
- expect(graphDataValidatorForAnomalyValues(fourQueries)).toBe(false);
+ expect(graphDataValidatorForAnomalyValues(fourMetrics)).toBe(false);
});
});
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
index c0d5ee9c446..e2fc359644d 100644
--- a/spec/javascripts/user_popovers_spec.js
+++ b/spec/javascripts/user_popovers_spec.js
@@ -38,6 +38,7 @@ describe('User Popovers', () => {
const shownPopover = document.querySelector('.popover');
expect(shownPopover).not.toBeNull();
+ expect(targetLink.getAttribute('aria-describedby')).not.toBeNull();
expect(shownPopover.innerHTML).toContain(dummyUser.name);
expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId.toString());
@@ -47,6 +48,7 @@ describe('User Popovers', () => {
setTimeout(() => {
// After Mouse leave it should be hidden now
expect(document.querySelector('.popover')).toBeNull();
+ expect(targetLink.getAttribute('aria-describedby')).toBeNull();
done();
});
}, 210); // We need to wait until the 200ms mouseover delay is over, only then the popover will be visible
diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
deleted file mode 100644
index 29a76574b89..00000000000
--- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import Vue from 'vue';
-import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('clipboard button', () => {
- const Component = Vue.extend(clipboardButton);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('without gfm', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- text: 'copy me',
- title: 'Copy this value',
- cssClass: 'btn-danger',
- });
- });
-
- it('renders a button for clipboard', () => {
- expect(vm.$el.tagName).toEqual('BUTTON');
- expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me');
- expect(vm.$el).toHaveSpriteIcon('duplicate');
- });
-
- it('should have a tooltip with default values', () => {
- expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value');
- });
-
- it('should render provided classname', () => {
- expect(vm.$el.classList).toContain('btn-danger');
- });
- });
-
- describe('with gfm', () => {
- it('sets data-clipboard-text with gfm', () => {
- vm = mountComponent(Component, {
- text: 'copy me',
- gfm: '`path/to/file`',
- title: 'Copy this value',
- cssClass: 'btn-danger',
- });
-
- expect(vm.$el.getAttribute('data-clipboard-text')).toEqual(
- '{"text":"copy me","gfm":"`path/to/file`"}',
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
deleted file mode 100644
index 80aa75847ae..00000000000
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import _ from 'underscore';
-import Vue from 'vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { TEST_HOST } from 'spec/test_constants';
-
-describe('User Avatar Link Component', function() {
- beforeEach(function() {
- this.propsData = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 99,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
-
- this.userAvatarLink = new UserAvatarLinkComponent({
- propsData: this.propsData,
- }).$mount();
-
- [this.userAvatarImage] = this.userAvatarLink.$children;
- });
-
- it('should return a defined Vue component', function() {
- expect(this.userAvatarLink).toBeDefined();
- });
-
- it('should have user-avatar-image registered as child component', function() {
- expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined();
- });
-
- it('user-avatar-link should have user-avatar-image as child component', function() {
- expect(this.userAvatarImage).toBeDefined();
- });
-
- it('should render <a> as a child element', function() {
- const link = this.userAvatarLink.$el;
-
- expect(link.tagName).toBe('A');
- expect(link.href).toBe(this.propsData.linkHref);
- });
-
- it('renders imgSrc with imgSize as image', function() {
- const { imgSrc, imgSize } = this.propsData;
- const image = this.userAvatarLink.$el.querySelector('img');
-
- expect(image).not.toBeNull();
- expect(image.src).toBe(`${imgSrc}?width=${imgSize}`);
- });
-
- it('should return necessary props as defined', function() {
- _.each(this.propsData, (val, key) => {
- expect(this.userAvatarLink[key]).toBeDefined();
- });
- });
-
- describe('no username', function() {
- beforeEach(function(done) {
- this.userAvatarLink.username = '';
-
- Vue.nextTick(done);
- });
-
- it('should only render image tag in link', function() {
- const childElements = this.userAvatarLink.$el.childNodes;
-
- expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null');
-
- // Vue will render the hidden component as <!---->
- expect(childElements[1].tagName).toBeUndefined();
- });
-
- it('should render avatar image tooltip', function() {
- expect(this.userAvatarLink.shouldShowUsername).toBe(false);
- expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText);
- });
- });
-
- describe('username', function() {
- it('should not render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip')).toBeNull();
- });
-
- it('should render username prop in <span>', function() {
- expect(
- this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(),
- ).toEqual(this.propsData.username);
- });
-
- it('should render text tooltip for <span>', function() {
- expect(
- this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset
- .originalTitle,
- ).toEqual(this.propsData.tooltipText);
- });
-
- it('should render text tooltip placement for <span>', function() {
- expect(
- this.userAvatarLink.$el
- .querySelector('.js-user-avatar-link-username')
- .getAttribute('tooltip-placement'),
- ).toEqual(this.propsData.tooltipPlacement);
- });
- });
-});
diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb
index 12dfad6698d..184d049d1fb 100644
--- a/spec/lib/gitaly/server_spec.rb
+++ b/spec/lib/gitaly/server_spec.rb
@@ -65,4 +65,26 @@ describe Gitaly::Server do
end
end
end
+
+ describe '#expected_version?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expected_version, :server_version, :result) do
+ '1.1.1' | '1.1.1' | true
+ '1.1.2' | '1.1.1' | false
+ '1.73.0' | '1.73.0-18-gf756ebe2' | false
+ '594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '1.55.6-45-g594c3ea3' | true
+ '594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '1.55.6-46-gabc123ff' | false
+ '594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '1.55.6' | false
+ end
+
+ with_them do
+ it do
+ allow(Gitlab::GitalyClient).to receive(:expected_server_version).and_return(expected_version)
+ allow(server).to receive(:server_version).and_return(server_version)
+
+ expect(server.expected_version?).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/ansi2json/result_spec.rb b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
new file mode 100644
index 00000000000..5b7b5481400
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Result do
+ let(:stream) { StringIO.new('hello') }
+ let(:state) { Gitlab::Ci::Ansi2json::State.new(nil, stream.size) }
+ let(:offset) { 0 }
+ let(:params) do
+ { lines: [], state: state, append: false, truncated: false, offset: offset, stream: stream }
+ end
+
+ subject { described_class.new(params) }
+
+ describe '#size' do
+ before do
+ stream.seek(5) # move stream cursor to the end
+ end
+
+ context 'when offset is at the start' do
+ let(:offset) { 0 }
+
+ it 'returns the full size' do
+ expect(subject.size).to eq(5)
+ end
+ end
+
+ context 'when offset is not zero' do
+ let(:offset) { 2 }
+
+ it 'returns the remaining size' do
+ expect(subject.size).to eq(3)
+ end
+ end
+ end
+
+ describe '#total' do
+ it 'returns size of stread' do
+ expect(subject.total).to eq(5)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..2a6314755ef
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Managed-Cluster-Applications.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Managed-Cluster-Applications') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:user) }
+
+ let(:project) { create(:project, :custom_repo, namespace: user.namespace, files: { 'README.md' => '' }) }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+ let(:pipeline_branch) { 'master' }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ end
+
+ context 'for a default branch' do
+ it 'creates a apply job' do
+ expect(build_names).to match_array('apply')
+ end
+ end
+
+ context 'outside of default branch' do
+ let(:pipeline_branch) { 'a_branch' }
+
+ before do
+ project.repository.create_branch(pipeline_branch)
+ end
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError, 'No stages / jobs for this pipeline.')
+ 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 449eee7a371..2ba253f9652 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -212,44 +212,118 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:transaction_open?).and_return(false)
end
- it 'creates a concurrent foreign key and validates it' do
- expect(model).to receive(:disable_statement_timeout).and_call_original
- expect(model).to receive(:execute).with(/statement_timeout/)
- expect(model).to receive(:execute).ordered.with(/NOT VALID/)
- expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ context 'ON DELETE statements' do
+ context 'on_delete: :nullify' do
+ it 'appends ON DELETE SET NULL statement' do
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).with(/RESET ALL/)
+
+ expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
+
+ model.add_concurrent_foreign_key(:projects, :users,
+ column: :user_id,
+ on_delete: :nullify)
+ end
+ end
- model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
- end
+ context 'on_delete: :cascade' do
+ it 'appends ON DELETE CASCADE statement' do
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).with(/RESET ALL/)
+
+ expect(model).to receive(:execute).with(/ON DELETE CASCADE/)
+
+ model.add_concurrent_foreign_key(:projects, :users,
+ column: :user_id,
+ on_delete: :cascade)
+ end
+ end
+
+ context 'on_delete: nil' do
+ it 'appends no ON DELETE statement' do
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).with(/RESET ALL/)
- it 'appends a valid ON DELETE statement' do
- expect(model).to receive(:disable_statement_timeout).and_call_original
- expect(model).to receive(:execute).with(/statement_timeout/)
- expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
- expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).not_to receive(:execute).with(/ON DELETE/)
- model.add_concurrent_foreign_key(:projects, :users,
- column: :user_id,
- on_delete: :nullify)
+ model.add_concurrent_foreign_key(:projects, :users,
+ column: :user_id,
+ on_delete: nil)
+ end
+ end
end
- it 'does not create a foreign key if it exists already' do
- expect(model).to receive(:foreign_key_exists?).with(:projects, :users, column: :user_id).and_return(true)
- expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
- expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
+ context 'when no custom key name is supplied' do
+ it 'creates a concurrent foreign key and validates it' do
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).ordered.with(/NOT VALID/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).with(/RESET ALL/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
+
+ it 'does not create a foreign key if it exists already' do
+ name = model.concurrent_foreign_key_name(:projects, :user_id)
+ expect(model).to receive(:foreign_key_exists?).with(:projects, :users,
+ column: :user_id,
+ on_delete: :cascade,
+ name: name).and_return(true)
+
+ expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
+ expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
- model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
end
- it 'allows the use of a custom key name' do
- expect(model).to receive(:disable_statement_timeout).and_call_original
- expect(model).to receive(:execute).with(/statement_timeout/)
- expect(model).to receive(:execute).ordered.with(/NOT VALID/)
- expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+foo/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ context 'when a custom key name is supplied' do
+ context 'for creating a new foreign key for a column that does not presently exist' do
+ it 'creates a new foreign key' do
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).ordered.with(/NOT VALID/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+foo/)
+ expect(model).to receive(:execute).with(/RESET ALL/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :foo)
+ end
+ end
+
+ context 'for creating a duplicate foreign key for a column that presently exists' do
+ context 'when the supplied key name is the same as the existing foreign key name' do
+ it 'does not create a new foreign key' do
+ expect(model).to receive(:foreign_key_exists?).with(:projects, :users,
+ name: :foo,
+ on_delete: :cascade,
+ column: :user_id).and_return(true)
+
+ expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
+ expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :foo)
+ end
+ end
+
+ context 'when the supplied key name is different from the existing foreign key name' do
+ it 'creates a new foreign key' do
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).ordered.with(/NOT VALID/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+bar/)
+ expect(model).to receive(:execute).with(/RESET ALL/)
- model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :foo)
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :bar)
+ end
+ end
+ end
end
end
end
@@ -266,23 +340,61 @@ describe Gitlab::Database::MigrationHelpers do
describe '#foreign_key_exists?' do
before do
- key = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:projects, :users, { column: :non_standard_id })
+ key = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:projects, :users, { column: :non_standard_id, name: :fk_projects_users_non_standard_id, on_delete: :cascade })
allow(model).to receive(:foreign_keys).with(:projects).and_return([key])
end
- it 'finds existing foreign keys by column' do
- expect(model.foreign_key_exists?(:projects, :users, column: :non_standard_id)).to be_truthy
+ shared_examples_for 'foreign key checks' do
+ it 'finds existing foreign keys by column' do
+ expect(model.foreign_key_exists?(:projects, target_table, column: :non_standard_id)).to be_truthy
+ end
+
+ it 'finds existing foreign keys by name' do
+ expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id)).to be_truthy
+ end
+
+ it 'finds existing foreign_keys by name and column' do
+ expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id, column: :non_standard_id)).to be_truthy
+ end
+
+ it 'finds existing foreign_keys by name, column and on_delete' do
+ expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id, column: :non_standard_id, on_delete: :cascade)).to be_truthy
+ end
+
+ it 'finds existing foreign keys by target table only' do
+ expect(model.foreign_key_exists?(:projects, target_table)).to be_truthy
+ end
+
+ it 'compares by column name if given' do
+ expect(model.foreign_key_exists?(:projects, target_table, column: :user_id)).to be_falsey
+ end
+
+ it 'compares by foreign key name if given' do
+ expect(model.foreign_key_exists?(:projects, target_table, name: :non_existent_foreign_key_name)).to be_falsey
+ end
+
+ it 'compares by foreign key name and column if given' do
+ expect(model.foreign_key_exists?(:projects, target_table, name: :non_existent_foreign_key_name, column: :non_standard_id)).to be_falsey
+ end
+
+ it 'compares by foreign key name, column and on_delete if given' do
+ expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id, column: :non_standard_id, on_delete: :nullify)).to be_falsey
+ end
end
- it 'finds existing foreign keys by target table only' do
- expect(model.foreign_key_exists?(:projects, :users)).to be_truthy
+ context 'without specifying a target table' do
+ let(:target_table) { nil }
+
+ it_behaves_like 'foreign key checks'
end
- it 'compares by column name if given' do
- expect(model.foreign_key_exists?(:projects, :users, column: :user_id)).to be_falsey
+ context 'specifying a target table' do
+ let(:target_table) { :users }
+
+ it_behaves_like 'foreign key checks'
end
- it 'compares by target if no column given' do
+ it 'compares by target table if no column given' do
expect(model.foreign_key_exists?(:projects, :other_table)).to be_falsey
end
end
diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
index 6d38f7f1b95..b3826666b18 100644
--- a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
+++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
@@ -8,21 +8,27 @@ describe Gitlab::Database::ObsoleteIgnoredColumns do
end
class SomeAbstract < MyBase
+ include IgnorableColumns
+
self.abstract_class = true
self.table_name = 'projects'
- self.ignored_columns += %i[unused]
+ ignore_column :unused, remove_after: '2019-01-01', remove_with: '12.0'
end
class B < MyBase
+ include IgnorableColumns
+
self.table_name = 'issues'
- self.ignored_columns += %i[id other]
+ ignore_column :id, :other, remove_after: '2019-01-01', remove_with: '12.0'
+ ignore_column :not_used_but_still_ignored, remove_after: Date.today.to_s, remove_with: '12.1'
end
class A < SomeAbstract
- self.ignored_columns += %i[id also_unused]
+ ignore_column :also_unused, remove_after: '2019-02-01', remove_with: '12.1'
+ ignore_column :not_used_but_still_ignored, remove_after: Date.today.to_s, remove_with: '12.1'
end
class C < MyBase
@@ -35,8 +41,13 @@ describe Gitlab::Database::ObsoleteIgnoredColumns do
describe '#execute' do
it 'returns a list of class names and columns pairs' do
expect(subject.execute).to eq([
- ['Testing::A', %w(unused also_unused)],
- ['Testing::B', %w(other)]
+ ['Testing::A', {
+ 'unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0'),
+ 'also_unused' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-02-01'), '12.1')
+ }],
+ ['Testing::B', {
+ 'other' => IgnorableColumns::ColumnIgnore.new(Date.parse('2019-01-01'), '12.0')
+ }]
])
end
end
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index 8fcd4eb3c21..e25ce4df4aa 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -12,6 +12,15 @@ describe Gitlab::EtagCaching::Router do
expect(result.name).to eq 'issue_notes'
end
+ it 'matches MR notes endpoint' do
+ result = described_class.match(
+ '/my-group/and-subgroup/here-comes-the-project/noteable/merge_request/1/notes'
+ )
+
+ expect(result).to be_present
+ expect(result.name).to eq 'merge_request_notes'
+ end
+
it 'matches issue title endpoint' do
result = described_class.match(
'/my-group/my-project/issues/123/realtime_changes'
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
index 33efc640257..d8c411fa27b 100644
--- a/spec/lib/gitlab/health_checks/probes/collection_spec.rb
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Gitlab::HealthChecks::Probes::Collection do
let(:readiness) { described_class.new(*checks) }
- describe '#call' do
+ describe '#execute' do
subject { readiness.execute }
context 'with all checks' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5612b0dc270..24f0eb9a30c 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -139,6 +139,8 @@ merge_requests:
- blocking_merge_requests
- blocked_merge_requests
- description_versions
+- deployment_merge_requests
+- deployments
external_pull_requests:
- project
merge_request_diff:
@@ -288,6 +290,7 @@ project:
- microsoft_teams_service
- mattermost_service
- hangouts_chat_service
+- unify_circuit_service
- buildkite_service
- bamboo_service
- teamcity_service
@@ -365,6 +368,7 @@ project:
- root_of_fork_network
- fork_network_member
- fork_network
+- fork_network_projects
- custom_attributes
- lfs_file_locks
- project_badges
@@ -434,6 +438,7 @@ project:
- upstream_project_subscriptions
- downstream_project_subscriptions
- service_desk_setting
+- import_failures
award_emoji:
- awardable
- user
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 2d8a603172d..95e2142acec 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -362,7 +362,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(restored_project_json).to eq(true)
end
- it_behaves_like 'restores project correctly',
+ it_behaves_like 'restores project successfully',
issues: 1,
labels: 2,
label_with_priorities: 'A project label',
@@ -375,7 +375,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
create(:ci_build, token: 'abcd')
end
- it_behaves_like 'restores project correctly',
+ it_behaves_like 'restores project successfully',
issues: 1,
labels: 2,
label_with_priorities: 'A project label',
@@ -452,7 +452,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(restored_project_json).to eq(true)
end
- it_behaves_like 'restores project correctly',
+ it_behaves_like 'restores project successfully',
issues: 2,
labels: 2,
label_with_priorities: 'A project label',
@@ -633,4 +633,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
end
+
+ context 'JSON with invalid records' do
+ subject(:restored_project_json) { project_tree_restorer.restore }
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+ let(:correlation_id) { 'my-correlation-id' }
+
+ before do
+ setup_import_export_config('with_invalid_records')
+
+ Labkit::Correlation::CorrelationId.use_id(correlation_id) { subject }
+ end
+
+ context 'when failures occur because a relation fails to be processed' do
+ it_behaves_like 'restores project successfully',
+ issues: 0,
+ labels: 0,
+ label_with_priorities: nil,
+ milestones: 1,
+ first_issue_labels: 0,
+ services: 0,
+ import_failures: 1
+
+ it 'records the failures in the database' do
+ import_failure = ImportFailure.last
+
+ expect(import_failure.project_id).to eq(project.id)
+ expect(import_failure.relation_key).to eq('milestones')
+ expect(import_failure.relation_index).to be_present
+ expect(import_failure.exception_class).to eq('ActiveRecord::RecordInvalid')
+ expect(import_failure.exception_message).to be_present
+ expect(import_failure.correlation_id_value).to eq('my-correlation-id')
+ expect(import_failure.created_at).to be_present
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 31944d51b3c..38b93913f6d 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -207,5 +207,15 @@ describe Gitlab::SQL::Pattern do
expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
end
end
+
+ context 'when passing an Arel column' do
+ let(:query) { 'foo' }
+
+ subject(:fuzzy_arel_match) { Project.fuzzy_arel_match(Route.arel_table[:path], query) }
+
+ it 'returns a condition with the table and column name' do
+ expect(fuzzy_arel_match.to_sql).to match(/"routes"."path".*ILIKE '\%foo\%'/)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 08d3c638f9e..0aab02b6c4c 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -59,6 +59,26 @@ describe Gitlab::UrlBuilder do
end
end
+ context 'when passing a ProjectSnippet' do
+ it 'returns a proper URL' do
+ project_snippet = create(:project_snippet)
+
+ url = described_class.build(project_snippet)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.full_path}/snippets/#{project_snippet.id}"
+ end
+ end
+
+ context 'when passing a PersonalSnippet' do
+ it 'returns a proper URL' do
+ personal_snippet = create(:personal_snippet)
+
+ url = described_class.build(personal_snippet)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/snippets/#{personal_snippet.id}"
+ end
+ end
+
context 'when passing a Note' do
context 'on a Commit' do
it 'returns a proper URL' do
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
index 75dc7d8e6d1..16a05af2216 100644
--- a/spec/lib/gitlab/visibility_level_spec.rb
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -95,4 +95,28 @@ describe Gitlab::VisibilityLevel do
expect(described_class.valid_level?(described_class::PUBLIC)).to be_truthy
end
end
+
+ describe '#visibility_level_decreased?' do
+ let(:project) { create(:project, :internal) }
+
+ context 'when visibility level decreases' do
+ before do
+ project.update!(visibility_level: described_class::PRIVATE)
+ end
+
+ it 'returns true' do
+ expect(project.visibility_level_decreased?).to be(true)
+ end
+ end
+
+ context 'when visibility level does not decrease' do
+ before do
+ project.update!(visibility_level: described_class::PUBLIC)
+ end
+
+ it 'returns false' do
+ expect(project.visibility_level_decreased?).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb
index 5bac102ac41..59d4a977d5e 100644
--- a/spec/lib/quality/kubernetes_client_spec.rb
+++ b/spec/lib/quality/kubernetes_client_spec.rb
@@ -5,15 +5,27 @@ require 'fast_spec_helper'
RSpec.describe Quality::KubernetesClient do
let(:namespace) { 'review-apps-ee' }
let(:release_name) { 'my-release' }
+ let(:pod_for_release) { "pod-my-release-abcd" }
+ let(:raw_resource_names_str) { "NAME\nfoo\n#{pod_for_release}\nbar" }
+ let(:raw_resource_names) { raw_resource_names_str.lines.map(&:strip) }
subject { described_class.new(namespace: namespace) }
+ describe 'RESOURCE_LIST' do
+ it 'returns the correct list of resources separated by commas' do
+ expect(described_class::RESOURCE_LIST).to eq('ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa,crd')
+ end
+ end
+
describe '#cleanup' do
+ before do
+ allow(subject).to receive(:raw_resource_names).and_return(raw_resource_names)
+ end
+
it 'raises an error if the Kubernetes command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl --namespace "#{namespace}" delete ) \
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized --wait=true -l release=\"#{release_name}\""])
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l release="#{release_name}")])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
@@ -21,9 +33,12 @@ RSpec.describe Quality::KubernetesClient do
it 'calls kubectl with the correct arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl --namespace "#{namespace}" delete ) \
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized --wait=true -l release=\"#{release_name}\""])
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l release="#{release_name}")])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl delete --namespace "#{namespace}" #{pod_for_release})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
@@ -35,20 +50,22 @@ RSpec.describe Quality::KubernetesClient do
it 'raises an error if the Kubernetes command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl --namespace "#{namespace}" delete ) \
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})'"])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})')])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
end
it 'calls kubectl with the correct arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl --namespace "#{namespace}" delete ) \
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})'"])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})')])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl delete --namespace "#{namespace}" #{pod_for_release})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
expect { subject.cleanup(release_name: release_name) }.to output.to_stdout
@@ -58,24 +75,37 @@ RSpec.describe Quality::KubernetesClient do
context 'with `wait: false`' do
it 'raises an error if the Kubernetes command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl --namespace "#{namespace}" delete ) \
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized --wait=false -l release=\"#{release_name}\""])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=false -l release="#{release_name}")])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup(release_name: release_name, wait: false) }.to raise_error(described_class::CommandFailedError)
end
it 'calls kubectl with the correct arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(kubectl --namespace "#{namespace}" delete ) \
- 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized --wait=false -l release=\"#{release_name}\""])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ .with(["kubectl delete #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" --now --ignore-not-found --include-uninitialized --wait=false -l release="#{release_name}")])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl delete --namespace "#{namespace}" #{pod_for_release})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
expect { subject.cleanup(release_name: release_name, wait: false) }.to output.to_stdout
end
end
end
+
+ describe '#raw_resource_names' do
+ it 'calls kubectl to retrieve the resource names' do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(["kubectl get #{described_class::RESOURCE_LIST} " +
+ %(--namespace "#{namespace}" -o custom-columns=NAME:.metadata.name)])
+ .and_return(Gitlab::Popen::Result.new([], raw_resource_names_str, '', double(success?: true)))
+
+ expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names)
+ end
+ end
end
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index 4db188bd8f2..c85994402dd 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -25,6 +25,13 @@ RSpec.describe Quality::TestLevel do
end
end
+ context 'when level is migration' do
+ it 'returns a pattern' do
+ expect(subject.pattern(:migration))
+ .to eq("spec/{migrations,lib/gitlab/background_migration}{,/**/}*_spec.rb")
+ end
+ end
+
context 'when level is integration' do
it 'returns a pattern' do
expect(subject.pattern(:integration))
@@ -79,6 +86,13 @@ RSpec.describe Quality::TestLevel do
end
end
+ context 'when level is migration' do
+ it 'returns a regexp' do
+ expect(subject.regexp(:migration))
+ .to eq(%r{spec/(migrations|lib/gitlab/background_migration)})
+ end
+ end
+
context 'when level is integration' do
it 'returns a regexp' do
expect(subject.regexp(:integration))
@@ -116,6 +130,18 @@ RSpec.describe Quality::TestLevel do
expect(subject.level_for('spec/models/abuse_report_spec.rb')).to eq(:unit)
end
+ it 'returns the correct level for a migration test' do
+ expect(subject.level_for('spec/migrations/add_default_and_free_plans_spec.rb')).to eq(:migration)
+ end
+
+ it 'returns the correct level for a background_migration test' do
+ expect(subject.level_for('spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb')).to eq(:migration)
+ end
+
+ it 'returns the correct level for a geo migration test' do
+ expect(described_class.new('ee/').level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration)
+ end
+
it 'returns the correct level for an integration test' do
expect(subject.level_for('spec/mailers/abuse_report_mailer_spec.rb')).to eq(:integration)
end
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index ed9da77038f..388a6418929 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -54,10 +54,20 @@ describe Sentry::Client do
end
end
+ shared_examples 'issues has correct return type' do |klass|
+ it "returns objects of type #{klass}" do
+ expect(subject[:issues]).to all( be_a(klass) )
+ end
+ end
+
shared_examples 'has correct length' do |length|
it { expect(subject.length).to eq(length) }
end
+ shared_examples 'issues has correct length' do |length|
+ it { expect(subject[:issues].length).to eq(length) }
+ end
+
# Requires sentry_api_request and subject to be defined
shared_examples 'calls sentry api' do
it 'calls sentry api' do
@@ -95,26 +105,44 @@ describe Sentry::Client do
let(:issue_status) { 'unresolved' }
let(:limit) { 20 }
let(:search_term) { '' }
+ let(:cursor) { nil }
+ let(:sort) { 'last_seen' }
let(:sentry_api_response) { issues_sample_response }
let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
- subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: 'last_seen') }
+ subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: sort, cursor: cursor) }
it_behaves_like 'calls sentry api'
- it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
- it_behaves_like 'has correct length', 1
+ it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues has correct length', 1
shared_examples 'has correct external_url' do
context 'external_url' do
it 'is constructed correctly' do
- expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
+ expect(subject[:issues][0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
end
end
end
+ context 'when response has a pagination info' do
+ let(:headers) do
+ {
+ link: '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
+ }
+ end
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response, headers: headers) }
+
+ it 'parses the pagination' do
+ expect(subject[:pagination]).to eq(
+ 'previous' => { 'cursor' => '1573556671000:0:1' },
+ 'next' => { 'cursor' => '1572959139000:0:0' }
+ )
+ end
+ end
+
context 'error object created from sentry response' do
using RSpec::Parameterized::TableSyntax
@@ -137,7 +165,7 @@ describe Sentry::Client do
end
with_them do
- it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
+ it { expect(subject[:issues][0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
end
it_behaves_like 'has correct external_url'
@@ -210,8 +238,8 @@ describe Sentry::Client do
it_behaves_like 'calls sentry api'
- it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
- it_behaves_like 'has correct length', 1
+ it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues has correct length', 1
it_behaves_like 'has correct external_url'
end
@@ -240,13 +268,23 @@ describe Sentry::Client do
it_behaves_like 'maps exceptions'
context 'when search term is present' do
- let(:search_term) { 'NoMethodError'}
+ let(:search_term) { 'NoMethodError' }
let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" }
it_behaves_like 'calls sentry api'
- it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
- it_behaves_like 'has correct length', 1
+ it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues has correct length', 1
+ end
+
+ context 'when cursor is present' do
+ let(:cursor) { '1572959139000:0:0' }
+ let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&cursor=#{cursor}&query=is:unresolved" }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'issues has correct length', 1
end
end
diff --git a/spec/lib/sentry/pagination_parser_spec.rb b/spec/lib/sentry/pagination_parser_spec.rb
new file mode 100644
index 00000000000..1be6f9f4163
--- /dev/null
+++ b/spec/lib/sentry/pagination_parser_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'support/helpers/fixture_helpers'
+
+describe Sentry::PaginationParser do
+ include FixtureHelpers
+
+ describe '.parse' do
+ subject { described_class.parse(headers) }
+
+ context 'when headers do not have "link" param' do
+ let(:headers) { {} }
+
+ it 'returns empty hash' do
+ is_expected.to eq({})
+ end
+ end
+
+ context 'when headers.link has previous and next pages' do
+ let(:headers) do
+ {
+ 'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ it 'returns info about both pages' do
+ is_expected.to eq(
+ 'previous' => { 'cursor' => '1573556671000:0:1' },
+ 'next' => { 'cursor' => '1572959139000:0:0' }
+ )
+ end
+ end
+
+ context 'when headers.link has only next page' do
+ let(:headers) do
+ {
+ 'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="false"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ it 'returns only info about the next page' do
+ is_expected.to eq(
+ 'next' => { 'cursor' => '1572959139000:0:0' }
+ )
+ end
+ end
+
+ context 'when headers.link has only previous page' do
+ let(:headers) do
+ {
+ 'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="false"; cursor="1572959139000:0:0"'
+ }
+ end
+
+ it 'returns only info about the previous page' do
+ is_expected.to eq(
+ 'previous' => { 'cursor' => '1573556671000:0:1' }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb b/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb
new file mode 100644
index 00000000000..110da221393
--- /dev/null
+++ b/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20191125114345_add_admin_mode_protected_path.rb')
+
+describe AddAdminModeProtectedPath, :migration do
+ ADMIN_MODE_ENDPOINT = '/admin/session'
+
+ subject(:migration) { described_class.new }
+
+ let(:application_settings) { table(:application_settings) }
+
+ context 'no settings available' do
+ it 'makes no changes' do
+ expect { migrate! }.not_to change { application_settings.count }
+ end
+ end
+
+ context 'protected_paths is null' do
+ before do
+ application_settings.create!(protected_paths: nil)
+ end
+
+ it 'makes no changes' do
+ expect { migrate! }.not_to change { application_settings.first.protected_paths }
+ end
+ end
+
+ it 'appends admin mode endpoint' do
+ application_settings.create!(protected_paths: '{a,b,c}')
+
+ protected_paths_before = %w[a b c]
+ protected_paths_after = protected_paths_before.dup << ADMIN_MODE_ENDPOINT
+
+ expect { migrate! }.to change { application_settings.first.protected_paths }.from(protected_paths_before).to(protected_paths_after)
+ end
+
+ it 'new default includes admin mode endpoint' do
+ settings_before = application_settings.create!
+
+ expect(settings_before.protected_paths).not_to include(ADMIN_MODE_ENDPOINT)
+
+ migrate!
+
+ application_settings.reset_column_information
+ settings_after = application_settings.create!
+
+ expect(settings_after.protected_paths).to include(ADMIN_MODE_ENDPOINT)
+ end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index b1f93dc7189..c26675e75bf 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -242,23 +242,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
end
end
-
- it 'does not remove the devise session if the active session could not be found' do
- Gitlab::Redis::SharedState.with do |redis|
- redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
- end
-
- other_user = create(:user)
-
- ActiveSession.destroy(other_user, request.session.id)
-
- Gitlab::Redis::SharedState.with do |redis|
- expect(redis.scan_each(match: "session:gitlab:*").to_a).not_to be_empty
- end
- end
end
describe '.cleanup' do
+ before do
+ stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
+ end
+
it 'removes obsolete lookup entries' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
@@ -276,5 +266,69 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
it 'does not bail if there are no lookup entries' do
ActiveSession.cleanup(user)
end
+
+ context 'cleaning up old sessions' do
+ let(:max_number_of_sessions_plus_one) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 1 }
+ let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 }
+
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ (1..max_number_of_sessions_plus_two).each do |number|
+ redis.set(
+ "session:user:gitlab:#{user.id}:#{number}",
+ Marshal.dump(ActiveSession.new(session_id: "#{number}", updated_at: number.days.ago))
+ )
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ "#{number}"
+ )
+ end
+ end
+ end
+
+ it 'removes obsolete active sessions entries' do
+ ActiveSession.cleanup(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
+
+ expect(sessions.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
+ expect(sessions).not_to include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}", "session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}")
+ end
+ end
+
+ it 'removes obsolete lookup entries' do
+ ActiveSession.cleanup(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
+
+ expect(lookup_entries.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
+ expect(lookup_entries).not_to include(max_number_of_sessions_plus_one.to_s, max_number_of_sessions_plus_two.to_s)
+ end
+ end
+
+ it 'removes obsolete lookup entries even without active session' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ "#{max_number_of_sessions_plus_two + 1}"
+ )
+ end
+
+ ActiveSession.cleanup(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
+
+ expect(lookup_entries.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
+ expect(lookup_entries).not_to include(
+ max_number_of_sessions_plus_one.to_s,
+ max_number_of_sessions_plus_two.to_s,
+ (max_number_of_sessions_plus_two + 1).to_s
+ )
+ end
+ end
+ end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index ba3b99f4421..9a76be5b6f1 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -66,6 +66,8 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
+ it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
+
context 'when snowplow is enabled' do
before do
setting.snowplow_enabled = true
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 020ada3c47a..b06fa845777 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -88,6 +88,42 @@ describe BroadcastMessage do
expect(Rails.cache).not_to receive(:delete).with(described_class::CACHE_KEY)
expect(described_class.current.length).to eq(0)
end
+
+ it 'returns message if it matches the target path' do
+ message = create(:broadcast_message, target_path: "*/onboarding_completed")
+
+ expect(described_class.current('/users/onboarding_completed')).to include(message)
+ end
+
+ it 'returns message if part of the target path matches' do
+ create(:broadcast_message, target_path: "/users/*/issues")
+
+ expect(described_class.current('/users/name/issues').length).to eq(1)
+ end
+
+ it 'returns the message for empty target path' do
+ create(:broadcast_message, target_path: "")
+
+ expect(described_class.current('/users/name/issues').length).to eq(1)
+ end
+
+ it 'returns the message if target path is nil' do
+ create(:broadcast_message, target_path: nil)
+
+ expect(described_class.current('/users/name/issues').length).to eq(1)
+ end
+
+ it 'does not return message if target path does not match' do
+ create(:broadcast_message, target_path: "/onboarding_completed")
+
+ expect(described_class.current('/welcome').length).to eq(0)
+ end
+
+ it 'does not return message if target path does not match when using wildcard' do
+ create(:broadcast_message, target_path: "/users/*/issues")
+
+ expect(described_class.current('/group/groupname/issues').length).to eq(0)
+ end
end
describe '#attributes' do
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index e51fd009f50..cdf56f24cd7 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Ci::BuildRunnerSession, model: true do
let!(:build) { create(:ci_build, :with_runner_session) }
+ let(:url) { 'https://new.example.com' }
subject { build.runner_session }
@@ -12,6 +13,25 @@ describe Ci::BuildRunnerSession, model: true do
it { is_expected.to validate_presence_of(:build) }
it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') }
+ context 'nested attribute assignment' do
+ it 'creates a new session' do
+ simple_build = create(:ci_build)
+ simple_build.runner_session_attributes = { url: url }
+ simple_build.save!
+
+ session = simple_build.reload.runner_session
+ expect(session).to be_a(Ci::BuildRunnerSession)
+ expect(session.url).to eq(url)
+ end
+
+ it 'updates session with new attributes' do
+ build.runner_session_attributes = { url: url }
+ build.save!
+
+ expect(build.reload.runner_session.url).to eq(url)
+ end
+ end
+
describe '#terminal_specification' do
let(:specification) { subject.terminal_specification }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 1413da231e0..76e31fddd98 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -19,8 +19,24 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 106365) }
+ context 'with update_project_statistics_after_commit enabled' do
+ before do
+ stub_feature_flags(update_project_statistics_after_commit: true)
+ end
+
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 106365) }
+ end
+ end
+
+ context 'with update_project_statistics_after_commit disabled' do
+ before do
+ stub_feature_flags(update_project_statistics_after_commit: false)
+ end
+
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 106365) }
+ end
end
describe '.with_reports' do
diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb
index 71e0b03dff9..ece478fdd36 100644
--- a/spec/models/ci/persistent_ref_spec.rb
+++ b/spec/models/ci/persistent_ref_spec.rb
@@ -73,8 +73,8 @@ describe Ci::PersistentRef do
pipeline.persistent_ref.create
end
- it 'does not create a persistent ref' do
- expect(project.repository).not_to receive(:create_ref)
+ it 'overwrites a persistent ref' do
+ expect(project.repository).to receive(:create_ref).and_call_original
subject
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d34919d17fe..3fa6dcc47a6 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1725,17 +1725,6 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '.latest_for_shas' do
- let(:sha) { 'abc' }
-
- it 'returns latest pipeline for sha' do
- create(:ci_pipeline, sha: sha)
- pipeline2 = create(:ci_pipeline, sha: sha)
-
- expect(described_class.latest_for_shas(sha)).to contain_exactly(pipeline2)
- end
- end
-
describe '.latest_successful_ids_per_project' do
let(:projects) { create_list(:project, 2) }
let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) }
diff --git a/spec/models/concerns/ignorable_columns_spec.rb b/spec/models/concerns/ignorable_columns_spec.rb
new file mode 100644
index 00000000000..55efa1b5fda
--- /dev/null
+++ b/spec/models/concerns/ignorable_columns_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IgnorableColumns do
+ let(:record_class) do
+ Class.new(ApplicationRecord) do
+ include IgnorableColumns
+ end
+ end
+
+ subject { record_class }
+
+ it 'adds columns to ignored_columns' do
+ expect do
+ subject.ignore_columns(:name, :created_at, remove_after: '2019-12-01', remove_with: '12.6')
+ end.to change { subject.ignored_columns }.from([]).to(%w(name created_at))
+ end
+
+ it 'adds columns to ignored_columns (array version)' do
+ expect do
+ subject.ignore_columns(%i[name created_at], remove_after: '2019-12-01', remove_with: '12.6')
+ end.to change { subject.ignored_columns }.from([]).to(%w(name created_at))
+ end
+
+ it 'requires remove_after attribute to be set' do
+ expect { subject.ignore_columns(:name, remove_after: nil, remove_with: 12.6) }.to raise_error(ArgumentError, /Please indicate/)
+ end
+
+ it 'requires remove_after attribute to be set' do
+ expect { subject.ignore_columns(:name, remove_after: "not a date", remove_with: 12.6) }.to raise_error(ArgumentError, /Please indicate/)
+ end
+
+ it 'requires remove_with attribute to be set' do
+ expect { subject.ignore_columns(:name, remove_after: '2019-12-01', remove_with: nil) }.to raise_error(ArgumentError, /Please indicate/)
+ end
+
+ describe '.ignored_columns_details' do
+ shared_examples_for 'storing removal information' do
+ it 'storing removal information' do
+ subject.ignore_column(columns, remove_after: '2019-12-01', remove_with: '12.6')
+
+ [columns].flatten.each do |column|
+ expect(subject.ignored_columns_details[column].remove_after).to eq(Date.parse('2019-12-01'))
+ expect(subject.ignored_columns_details[column].remove_with).to eq('12.6')
+ end
+ end
+ end
+
+ context 'with single column' do
+ let(:columns) { :name }
+ it_behaves_like 'storing removal information'
+ end
+
+ context 'with array column' do
+ let(:columns) { %i[name created_at] }
+ it_behaves_like 'storing removal information'
+ end
+
+ it 'defaults to empty Hash' do
+ expect(subject.ignored_columns_details).to eq({})
+ end
+ end
+
+ describe IgnorableColumns::ColumnIgnore do
+ subject { described_class.new(remove_after, remove_with) }
+
+ let(:remove_with) { double }
+
+ describe '#safe_to_remove?' do
+ context 'after remove_after date has passed' do
+ let(:remove_after) { Date.parse('2019-01-10') }
+
+ it 'returns true (safe to remove)' do
+ expect(subject.safe_to_remove?).to be_truthy
+ end
+ end
+
+ context 'before remove_after date has passed' do
+ let(:remove_after) { Date.today }
+
+ it 'returns false (not safe to remove)' do
+ expect(subject.safe_to_remove?).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 268542c39c4..522a27954e2 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -442,4 +442,36 @@ describe Deployment do
expect(deploy2.previous_environment_deployment).to be_nil
end
end
+
+ describe '#playable_build' do
+ subject { deployment.playable_build }
+
+ context 'when there is a deployable build' do
+ let(:deployment) { create(:deployment, deployable: build) }
+
+ context 'when the deployable build is playable' do
+ let(:build) { create(:ci_build, :playable) }
+
+ it 'returns that build' do
+ is_expected.to eq(build)
+ end
+ end
+
+ context 'when the deployable build is not playable' do
+ let(:build) { create(:ci_build) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'when there is no deployable build' do
+ let(:deployment) { create(:deployment) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/conversational_development_index/metric_spec.rb b/spec/models/dev_ops_score/metric_spec.rb
index 55ba466e614..89212d5ca26 100644
--- a/spec/models/conversational_development_index/metric_spec.rb
+++ b/spec/models/dev_ops_score/metric_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-describe ConversationalDevelopmentIndex::Metric do
- let(:conv_dev_index) { create(:conversational_development_index_metric) }
+describe DevOpsScore::Metric do
+ let(:conv_dev_index) { create(:dev_ops_score_metric) }
describe '#percentage_score' do
it 'returns stored percentage score' do
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index eea81d7c128..0f2c6928820 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -92,6 +92,84 @@ describe EnvironmentStatus do
end
end
+ describe '.for_deployed_merge_request' do
+ context 'when a merge request has no explicitly linked deployments' do
+ it 'returns the statuses based on the CI pipelines' do
+ mr = create(:merge_request, :merged)
+
+ expect(described_class)
+ .to receive(:after_merge_request)
+ .with(mr, mr.author)
+ .and_return([])
+
+ statuses = described_class.for_deployed_merge_request(mr, mr.author)
+
+ expect(statuses).to eq([])
+ end
+ end
+
+ context 'when a merge request has explicitly linked deployments' do
+ let(:merge_request) { create(:merge_request, :merged) }
+
+ let(:environment) do
+ create(:environment, project: merge_request.target_project)
+ end
+
+ it 'returns the statuses based on the linked deployments' do
+ deploy = create(
+ :deployment,
+ :success,
+ project: merge_request.target_project,
+ environment: environment,
+ deployable: nil
+ )
+
+ deploy.link_merge_requests(merge_request.target_project.merge_requests)
+
+ statuses = described_class
+ .for_deployed_merge_request(merge_request, merge_request.author)
+
+ expect(statuses.length).to eq(1)
+ expect(statuses[0].environment).to eq(environment)
+ expect(statuses[0].merge_request).to eq(merge_request)
+ end
+
+ it 'excludes environments the user can not see' do
+ deploy = create(
+ :deployment,
+ :success,
+ project: merge_request.target_project,
+ environment: environment,
+ deployable: nil
+ )
+
+ deploy.link_merge_requests(merge_request.target_project.merge_requests)
+
+ statuses = described_class
+ .for_deployed_merge_request(merge_request, create(:user))
+
+ expect(statuses).to be_empty
+ end
+
+ it 'excludes deployments that have the status "created"' do
+ deploy = create(
+ :deployment,
+ :created,
+ project: merge_request.target_project,
+ environment: environment,
+ deployable: nil
+ )
+
+ deploy.link_merge_requests(merge_request.target_project.merge_requests)
+
+ statuses = described_class
+ .for_deployed_merge_request(merge_request, merge_request.author)
+
+ expect(statuses).to be_empty
+ end
+ end
+ end
+
describe '.build_environments_status' do
subject { described_class.send(:build_environments_status, merge_request, user, pipeline) }
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index dbd3f8ffab3..27406533e5f 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -153,9 +153,9 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
it 'returns cached issues' do
expect(sentry_client).to receive(:list_issues).with(opts)
- .and_return(issues)
+ .and_return(issues: issues, pagination: {})
- expect(result).to eq(issues: issues)
+ expect(result).to eq(issues: issues, pagination: {})
end
end
diff --git a/spec/models/merge_request/pipelines_spec.rb b/spec/models/merge_request/pipelines_spec.rb
index 96f09eda647..0afbcc60ed6 100644
--- a/spec/models/merge_request/pipelines_spec.rb
+++ b/spec/models/merge_request/pipelines_spec.rb
@@ -75,7 +75,9 @@ describe MergeRequest::Pipelines do
let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) }
before do
- allow(merge_request).to receive(:all_commit_shas) { shas }
+ create(:merge_request_diff_commit,
+ merge_request_diff: merge_request.merge_request_diff,
+ sha: shas.second, relative_order: 1)
end
it 'returns merge request pipeline first' do
@@ -119,7 +121,11 @@ describe MergeRequest::Pipelines do
end
before do
- allow(merge_request_2).to receive(:all_commit_shas) { shas }
+ shas.each.with_index do |sha, index|
+ create(:merge_request_diff_commit,
+ merge_request_diff: merge_request_2.merge_request_diff,
+ sha: sha, relative_order: index)
+ end
end
it 'returns only related merge request pipelines' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 79ea29c84a4..bec817f2416 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -33,6 +33,21 @@ describe MergeRequest do
end
end
+ describe '.from_and_to_forks' do
+ it 'returns only MRs from and to forks (with no internal MRs)' do
+ project = create(:project)
+ fork = fork_project(project)
+ fork_2 = fork_project(project)
+ mr_from_fork = create(:merge_request, source_project: fork, target_project: project)
+ mr_to_fork = create(:merge_request, source_project: project, target_project: fork)
+
+ create(:merge_request, source_project: fork, target_project: fork_2)
+ create(:merge_request, source_project: project, target_project: project)
+
+ expect(described_class.from_and_to_forks(project)).to contain_exactly(mr_from_fork, mr_to_fork)
+ end
+ end
+
describe 'locking' do
using RSpec::Parameterized::TableSyntax
@@ -3376,4 +3391,56 @@ describe MergeRequest do
])
end
end
+
+ describe '#recent_visible_deployments' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:environment) do
+ create(:environment, project: merge_request.target_project)
+ end
+
+ it 'returns visible deployments' do
+ created = create(
+ :deployment,
+ :created,
+ project: merge_request.target_project,
+ environment: environment
+ )
+
+ success = create(
+ :deployment,
+ :success,
+ project: merge_request.target_project,
+ environment: environment
+ )
+
+ failed = create(
+ :deployment,
+ :failed,
+ project: merge_request.target_project,
+ environment: environment
+ )
+
+ merge_request.deployment_merge_requests.create!(deployment: created)
+ merge_request.deployment_merge_requests.create!(deployment: success)
+ merge_request.deployment_merge_requests.create!(deployment: failed)
+
+ expect(merge_request.recent_visible_deployments).to eq([failed, success])
+ end
+
+ it 'only returns a limited number of deployments' do
+ 20.times do
+ deploy = create(
+ :deployment,
+ :success,
+ project: merge_request.target_project,
+ environment: environment
+ )
+
+ merge_request.deployment_merge_requests.create!(deployment: deploy)
+ end
+
+ expect(merge_request.recent_visible_deployments.count).to eq(10)
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 74f2fc1bb61..a6d9ecaa7c5 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1048,20 +1048,20 @@ describe Note do
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
- def expect_expiration(note)
+ def expect_expiration(noteable)
expect_any_instance_of(Gitlab::EtagCaching::Store)
.to receive(:touch)
- .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+ .with("/#{noteable.project.namespace.to_param}/#{noteable.project.to_param}/noteable/#{noteable.class.name.underscore}/#{noteable.id}/notes")
end
it "expires cache for note's issue when note is saved" do
- expect_expiration(note)
+ expect_expiration(note.noteable)
note.save!
end
it "expires cache for note's issue when note is destroyed" do
- expect_expiration(note)
+ expect_expiration(note.noteable)
note.destroy!
end
@@ -1076,28 +1076,54 @@ describe Note do
end
end
- describe '#with_notes_filter' do
- let!(:comment) { create(:note) }
- let!(:system_note) { create(:note, system: true) }
+ context 'for merge requests' do
+ let_it_be(:merge_request) { create(:merge_request) }
- context 'when notes filter is nil' do
- subject { described_class.with_notes_filter(nil) }
+ context 'when adding a note to the MR' do
+ let(:note) { build(:note, noteable: merge_request, project: merge_request.project) }
- it { is_expected.to include(comment, system_note) }
+ it 'expires the MR note etag cache' do
+ expect_expiration(merge_request)
+
+ note.save!
+ end
end
- context 'when notes filter is set to all notes' do
- subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:all_notes]) }
+ context 'when adding a note to a commit on the MR' do
+ let(:note) { build(:note_on_commit, commit_id: merge_request.commits.first.id, project: merge_request.project) }
- it { is_expected.to include(comment, system_note) }
+ it 'expires the MR note etag cache' do
+ expect_expiration(merge_request)
+
+ note.save!
+ end
end
+ end
+ end
- context 'when notes filter is set to only comments' do
- subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:only_comments]) }
+ describe '#with_notes_filter' do
+ let!(:comment) { create(:note) }
+ let!(:system_note) { create(:note, system: true) }
- it { is_expected.to include(comment) }
- it { is_expected.not_to include(system_note) }
- end
+ subject { described_class.with_notes_filter(filter) }
+
+ context 'when notes filter is nil' do
+ let(:filter) { nil }
+
+ it { is_expected.to include(comment, system_note) }
+ end
+
+ context 'when notes filter is set to all notes' do
+ let(:filter) { UserPreference::NOTES_FILTERS[:all_notes] }
+
+ it { is_expected.to include(comment, system_note) }
+ end
+
+ context 'when notes filter is set to only comments' do
+ let(:filter) { UserPreference::NOTES_FILTERS[:only_comments] }
+
+ it { is_expected.to include(comment) }
+ it { is_expected.not_to include(system_note) }
end
end
diff --git a/spec/models/project_services/unify_circuit_service_spec.rb b/spec/models/project_services/unify_circuit_service_spec.rb
new file mode 100644
index 00000000000..51079ea5395
--- /dev/null
+++ b/spec/models/project_services/unify_circuit_service_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe UnifyCircuitService do
+ it_behaves_like "chat service", "Unify Circuit" do
+ let(:client_arguments) { webhook_url }
+ let(:content_key) { :subject }
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ac6ae5e1cc6..b1f88c4530e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -33,6 +33,7 @@ describe Project do
it { is_expected.to have_one(:microsoft_teams_service) }
it { is_expected.to have_one(:mattermost_service) }
it { is_expected.to have_one(:hangouts_chat_service) }
+ it { is_expected.to have_one(:unify_circuit_service) }
it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
@@ -1661,7 +1662,7 @@ describe Project do
end
describe '.search' do
- let(:project) { create(:project, description: 'kitten mittens') }
+ let_it_be(:project) { create(:project, description: 'kitten mittens') }
it 'returns projects with a matching name' do
expect(described_class.search(project.name)).to eq([project])
@@ -1699,6 +1700,39 @@ describe Project do
expect(described_class.search(project.path.upcase)).to eq([project])
end
+ context 'by full path' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ context 'when feature is enabled' do
+ before do
+ stub_feature_flags(project_search_by_full_path: true)
+ end
+
+ it 'returns projects that match the group path' do
+ expect(described_class.search(group.path)).to eq([project])
+ end
+
+ it 'returns projects that match the full path' do
+ expect(described_class.search(project.full_path)).to eq([project])
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(project_search_by_full_path: false)
+ end
+
+ it 'returns no results when searching by group path' do
+ expect(described_class.search(group.path)).to be_empty
+ end
+
+ it 'returns no results when searching by full path' do
+ expect(described_class.search(project.full_path)).to be_empty
+ end
+ end
+ end
+
describe 'with pending_delete project' do
let(:pending_delete_project) { create(:project, pending_delete: true) }
@@ -5088,12 +5122,24 @@ describe Project do
it { is_expected.not_to be_git_objects_poolable }
end
- context 'when the project is not public' do
+ context 'when the project is private' do
let(:project) { create(:project, :private) }
it { is_expected.not_to be_git_objects_poolable }
end
+ context 'when the project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it { is_expected.to be_git_objects_poolable }
+ end
+
+ context 'when the project is internal' do
+ let(:project) { create(:project, :repository, :internal) }
+
+ it { is_expected.to be_git_objects_poolable }
+ end
+
context 'when objects are poolable' do
let(:project) { create(:project, :repository, :public) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index bad05990965..c0245dfdf1a 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2399,7 +2399,40 @@ describe Repository do
end
describe '#ancestor? with Gitaly enabled' do
- it_behaves_like "#ancestor?"
+ let(:commit) { repository.commit }
+ let(:ancestor) { commit.parents.first }
+ let(:cache_key) { "ancestor:#{ancestor.id}:#{commit.id}" }
+
+ it_behaves_like '#ancestor?'
+
+ context 'caching', :request_store, :clean_gitlab_redis_cache do
+ it 'only calls out to Gitaly once' do
+ expect(repository.raw_repository).to receive(:ancestor?).once
+
+ 2.times { repository.ancestor?(commit.id, ancestor.id) }
+ end
+
+ it 'increments a counter with cache hits' do
+ counter = Gitlab::Metrics.counter(:repository_ancestor_calls_total, 'Repository ancestor calls')
+
+ expect do
+ 2.times { repository.ancestor?(commit.id, ancestor.id) }
+ end.to change { counter.get(cache_hit: 'true') }.by(1)
+ .and change { counter.get(cache_hit: 'false') }.by(1)
+ end
+
+ it 'returns the value from the request store' do
+ repository.__send__(:request_store_cache).write(cache_key, "it's apparent")
+
+ expect(repository.ancestor?(ancestor.id, commit.id)).to eq("it's apparent")
+ end
+
+ it 'returns the value from the redis cache' do
+ expect(repository.__send__(:cache)).to receive(:fetch).with(cache_key).and_return("it's apparent")
+
+ expect(repository.ancestor?(ancestor.id, commit.id)).to eq("it's apparent")
+ end
+ end
end
describe '#ancestor? with Rugged enabled', :enable_rugged do
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index cfc0703af23..82836dac1d7 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -31,6 +31,62 @@ describe Snippet do
it { is_expected.to validate_presence_of(:content) }
it { is_expected.to validate_inclusion_of(:visibility_level).in_array(Gitlab::VisibilityLevel.values) }
+
+ it do
+ allow(Gitlab::CurrentSettings).to receive(:snippet_size_limit).and_return(1)
+
+ is_expected
+ .to validate_length_of(:content)
+ .is_at_most(Gitlab::CurrentSettings.snippet_size_limit)
+ .with_message("is too long (2 Bytes). The maximum size is 1 Byte.")
+ end
+
+ context 'content validations' do
+ context 'with existing snippets' do
+ let(:snippet) { create(:personal_snippet, content: 'This is a valid content at the time of creation') }
+
+ before do
+ expect(snippet).to be_valid
+
+ stub_application_setting(snippet_size_limit: 2)
+ end
+
+ it 'does not raise a validation error if the content is not changed' do
+ snippet.title = 'new title'
+
+ expect(snippet).to be_valid
+ end
+
+ it 'raises and error if the content is changed and the size is bigger than limit' do
+ snippet.content = snippet.content + "test"
+
+ expect(snippet).not_to be_valid
+ end
+ end
+
+ context 'with new snippets' do
+ let(:limit) { 15 }
+
+ before do
+ stub_application_setting(snippet_size_limit: limit)
+ end
+
+ it 'is valid when content is smaller than the limit' do
+ snippet = build(:personal_snippet, content: 'Valid Content')
+
+ expect(snippet).to be_valid
+ end
+
+ it 'raises error when content is bigger than setting limit' do
+ snippet = build(:personal_snippet, content: 'This is an invalid content')
+
+ aggregate_failures do
+ expect(snippet).not_to be_valid
+ expect(snippet.errors[:content]).to include("is too long (#{snippet.content.size} Bytes). The maximum size is #{limit} Bytes.")
+ end
+ end
+ end
+ end
end
describe '#to_reference' do
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index c18cc245468..f715ecae347 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -306,4 +306,22 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:use_slash_commands) }
end
end
+
+ describe 'create_personal_snippet' do
+ context 'when anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.not_to be_allowed(:create_personal_snippet) }
+ end
+
+ context 'regular user' do
+ it { is_expected.to be_allowed(:create_personal_snippet) }
+ end
+
+ context 'when external' do
+ let(:current_user) { build(:user, :external) }
+
+ it { is_expected.not_to be_allowed(:create_personal_snippet) }
+ end
+ end
end
diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/dev_ops_score/metric_presenter_spec.rb
index ac18d5203e5..b6eab3f2e74 100644
--- a/spec/presenters/conversational_development_index/metric_presenter_spec.rb
+++ b/spec/presenters/dev_ops_score/metric_presenter_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-describe ConversationalDevelopmentIndex::MetricPresenter do
+describe DevOpsScore::MetricPresenter do
subject { described_class.new(metric) }
- let(:metric) { build(:conversational_development_index_metric) }
+ let(:metric) { build(:dev_ops_score_metric) }
describe '#cards' do
it 'includes instance score, leader score and percentage score' do
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 26849c0991d..91e047774bf 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -31,39 +31,36 @@ describe API::Deployments do
end
describe 'ordering' do
- using RSpec::Parameterized::TableSyntax
-
- let(:order_by) { nil }
- let(:sort) { nil }
+ let(:order_by) { 'iid' }
+ let(:sort) { 'desc' }
subject { get api("/projects/#{project.id}/deployments?order_by=#{order_by}&sort=#{sort}", user) }
+ before do
+ subject
+ end
+
def expect_deployments(ordered_deployments)
- json_response.each_with_index do |deployment_json, index|
- expect(deployment_json['id']).to eq(public_send(ordered_deployments[index]).id)
- end
+ expect(json_response.map { |d| d['id'] }).to eq(ordered_deployments.map(&:id))
end
- before do
- subject
+ it 'returns ordered deployments' do
+ expect(json_response.map { |i| i['id'] }).to eq([deployment_2.id, deployment_1.id, deployment_3.id])
end
- where(:order_by, :sort, :ordered_deployments) do
- 'created_at' | 'asc' | [:deployment_3, :deployment_2, :deployment_1]
- 'created_at' | 'desc' | [:deployment_1, :deployment_2, :deployment_3]
- 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3]
- 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1]
- 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2]
- 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
- 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
- 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
- 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
+ context 'with invalid order_by' do
+ let(:order_by) { 'wrong_sorting_value' }
+
+ it 'returns error' do
+ expect(response).to have_gitlab_http_status(400)
+ end
end
- with_them do
- it 'returns the deployments ordered' do
- expect_deployments(ordered_deployments)
+ context 'with invalid sorting' do
+ let(:sort) { 'wrong_sorting_direction' }
+
+ it 'returns error' do
+ expect(response).to have_gitlab_http_status(400)
end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index b7586307929..6599e4b8f23 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -36,6 +36,7 @@ describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_system_hooks']).to be(true)
expect(json_response).not_to have_key('performance_bar_allowed_group_path')
expect(json_response).not_to have_key('performance_bar_enabled')
+ expect(json_response['snippet_size_limit']).to eq(50.megabytes)
end
end
@@ -85,7 +86,8 @@ describe API::Settings, 'Settings' do
allow_local_requests_from_web_hooks_and_services: true,
allow_local_requests_from_system_hooks: false,
push_event_hooks_limit: 2,
- push_event_activities_limit: 2
+ push_event_activities_limit: 2,
+ snippet_size_limit: 5
}
expect(response).to have_gitlab_http_status(200)
@@ -121,6 +123,7 @@ describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
expect(json_response['push_event_hooks_limit']).to eq(2)
expect(json_response['push_event_activities_limit']).to eq(2)
+ expect(json_response['snippet_size_limit']).to eq(5)
end
end
diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb
index ea172698764..46d4f31dd31 100644
--- a/spec/routing/environments_spec.rb
+++ b/spec/routing/environments_spec.rb
@@ -11,7 +11,7 @@ describe 'environments routing' do
end
let(:environments_route) do
- "#{project.full_path}/environments/"
+ "#{project.full_path}/-/environments/"
end
describe 'routing environment folders' do
@@ -38,7 +38,7 @@ describe 'environments routing' do
end
def get_folder(folder)
- get("#{project.full_path}/environments/folders/#{folder}")
+ get("#{project.full_path}/-/environments/folders/#{folder}")
end
def folder_action(**opts)
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 561c2b572ec..fc562f93406 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -794,4 +794,28 @@ describe 'project routing' do
expect(post('/gitlab/gitlabhq/usage_ping/web_ide_clientside_preview')).to route_to('projects/usage_ping#web_ide_clientside_preview', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
+
+ describe Projects::EnvironmentsController, 'routing' do
+ describe 'legacy routing' do
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/environments", "/gitlab/gitlabhq/-/environments"
+ end
+ end
+
+ describe Projects::ClustersController, 'routing' do
+ describe 'legacy routing' do
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/clusters", "/gitlab/gitlabhq/-/clusters"
+ end
+ end
+
+ describe Projects::ErrorTrackingController, 'routing' do
+ describe 'legacy routing' do
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/error_tracking", "/gitlab/gitlabhq/-/error_tracking"
+ end
+ end
+
+ describe Projects::Serverless, 'routing' do
+ describe 'legacy routing' do
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/serverless", "/gitlab/gitlabhq/-/serverless"
+ end
+ end
end
diff --git a/spec/rubocop/cop/ignored_columns_spec.rb b/spec/rubocop/cop/ignored_columns_spec.rb
new file mode 100644
index 00000000000..64437765018
--- /dev/null
+++ b/spec/rubocop/cop/ignored_columns_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/ignored_columns'
+
+describe RuboCop::Cop::IgnoredColumns do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags the use of destroy_all with a local variable receiver' do
+ inspect_source(<<~RUBY)
+ class Foo < ApplicationRecord
+ self.ignored_columns += %i[id]
+ end
+ RUBY
+
+ expect(cop.offenses.size).to eq(1)
+ end
+end
diff --git a/spec/rubocop/cop/put_project_routes_under_scope_spec.rb b/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
new file mode 100644
index 00000000000..b0f1e52f397
--- /dev/null
+++ b/spec/rubocop/cop/put_project_routes_under_scope_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require_relative '../../../rubocop/cop/put_project_routes_under_scope'
+
+describe RuboCop::Cop::PutProjectRoutesUnderScope do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ before do
+ allow(cop).to receive(:in_project_routes?).and_return(true)
+ end
+
+ it 'registers an offense when route is outside scope' do
+ expect_offense(<<~PATTERN.strip_indent)
+ scope '-' do
+ resource :issues
+ end
+
+ resource :notes
+ ^^^^^^^^^^^^^^^ Put new project routes under /-/ scope
+ PATTERN
+ end
+
+ it 'does not register an offense when resource inside the scope' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ scope '-' do
+ resource :issues
+ resource :notes
+ end
+ PATTERN
+ end
+
+ it 'does not register an offense when resource is deep inside the scope' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ scope '-' do
+ resource :issues
+ resource :projects do
+ resource :issues do
+ resource :notes
+ end
+ end
+ end
+ PATTERN
+ end
+end
diff --git a/spec/serializers/cluster_basic_entity_spec.rb b/spec/serializers/cluster_basic_entity_spec.rb
index be03ee91784..8c3307a1837 100644
--- a/spec/serializers/cluster_basic_entity_spec.rb
+++ b/spec/serializers/cluster_basic_entity_spec.rb
@@ -24,7 +24,7 @@ describe ClusterBasicEntity do
it 'exposes the cluster details' do
expect(subject[:name]).to eq('the-cluster')
- expect(subject[:path]).to eq("/#{project.full_path}/clusters/#{cluster.id}")
+ expect(subject[:path]).to eq("/#{project.full_path}/-/clusters/#{cluster.id}")
end
context 'when the user does not have permission to view the cluster' do
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index d7816a3503d..2a57ea51b39 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -107,6 +107,36 @@ describe DeploymentEntity do
end
end
+ describe 'playable_build' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when the deployment has a playable deployable' do
+ context 'when this build is ready to be played' do
+ let(:build) { create(:ci_build, :playable, :scheduled, pipeline: pipeline) }
+
+ it 'exposes only the play_path' do
+ expect(subject[:playable_build].keys).to contain_exactly(:play_path)
+ end
+ end
+
+ context 'when this build has failed' do
+ let(:build) { create(:ci_build, :playable, :failed, pipeline: pipeline) }
+
+ it 'exposes the play_path and the retry_path' do
+ expect(subject[:playable_build].keys).to contain_exactly(:play_path, :retry_path)
+ end
+ end
+ end
+
+ context 'when the deployment does not have a playable deployable' do
+ let(:build) { create(:ci_build) }
+
+ it 'is not exposed' do
+ expect(subject[:playable_build]).to be_nil
+ end
+ end
+ end
+
context 'when deployment details serialization was disabled' do
include Gitlab::Routing
diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb
index 0687751fd67..6d98f91cfde 100644
--- a/spec/serializers/environment_status_entity_spec.rb
+++ b/spec/serializers/environment_status_entity_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe EnvironmentStatusEntity do
let(:user) { create(:user) }
- let(:request) { double('request') }
+ let(:request) { double('request', project: project) }
let(:deployment) { create(:deployment, :succeed, :review_app) }
let(:environment) { deployment.environment }
@@ -28,6 +28,7 @@ describe EnvironmentStatusEntity do
it { is_expected.to include(:external_url_formatted) }
it { is_expected.to include(:deployed_at) }
it { is_expected.to include(:deployed_at_formatted) }
+ it { is_expected.to include(:details) }
it { is_expected.to include(:changes) }
it { is_expected.to include(:status) }
@@ -72,7 +73,7 @@ describe EnvironmentStatusEntity do
it 'returns metrics url' do
expect(subject[:metrics_url])
- .to eq("/#{project.full_path}/environments/#{environment.id}/deployments/#{deployment.iid}/metrics")
+ .to eq("/#{project.full_path}/-/environments/#{environment.id}/deployments/#{deployment.iid}/metrics")
end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 02c5b817ea4..d95aaf3d104 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -218,5 +218,28 @@ describe PipelineEntity do
expect(subject[:merge_request_event_type]).to be_present
end
end
+
+ context 'when pipeline has failed builds' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:build) { create(:ci_build, :success, pipeline: pipeline) }
+ let_it_be(:failed_1) { create(:ci_build, :failed, pipeline: pipeline) }
+ let_it_be(:failed_2) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ context 'when the user can retry the pipeline' do
+ it 'exposes these failed builds' do
+ allow(entity).to receive(:can_retry?).and_return(true)
+
+ expect(subject[:failed_builds].map { |b| b[:id] }).to contain_exactly(failed_1.id, failed_2.id)
+ end
+ end
+
+ context 'when the user cannot retry the pipeline' do
+ it 'is nil' do
+ allow(entity).to receive(:can_retry?).and_return(false)
+
+ expect(subject[:failed_builds]).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index 67455190450..e0e280591cd 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -5,13 +5,14 @@ require 'spec_helper'
describe ErrorTracking::ListIssuesService do
set(:user) { create(:user) }
set(:project) { create(:project) }
- let(:params) { { search_term: 'something', sort: 'last_seen' } }
+ let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
let(:list_sentry_issues_args) do
{
issue_status: 'unresolved',
limit: 20,
- search_term: params[:search_term],
- sort: params[:sort]
+ search_term: 'something',
+ sort: 'last_seen',
+ cursor: 'some-cursor'
}
end
@@ -40,11 +41,11 @@ describe ErrorTracking::ListIssuesService do
expect(error_tracking_setting)
.to receive(:list_sentry_issues)
.with(list_sentry_issues_args)
- .and_return(issues: issues)
+ .and_return(issues: issues, pagination: {})
end
it 'returns the issues' do
- expect(result).to eq(status: :success, issues: issues)
+ expect(result).to eq(status: :success, pagination: {}, issues: issues)
end
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index e3a728f2566..2b0b486308a 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -11,7 +11,7 @@ describe Issuable::BulkUpdateService do
.reverse_merge(issuable_ids: Array(issuables).map(&:id).join(','))
type = Array(issuables).first.model_name.param_key
- Issuable::BulkUpdateService.new(user, bulk_update_params).execute(type)
+ Issuable::BulkUpdateService.new(parent, user, bulk_update_params).execute(type)
end
shared_examples 'updates milestones' do
@@ -184,6 +184,8 @@ describe Issuable::BulkUpdateService do
end
context 'with issuables at a project level' do
+ let(:parent) { project }
+
describe 'close issues' do
let(:issues) { create_list(:issue, 2, project: project) }
@@ -200,33 +202,6 @@ describe Issuable::BulkUpdateService do
expect(project.issues.opened).to be_empty
expect(project.issues.closed).not_to be_empty
end
-
- context 'when issue for a different project is created' do
- let(:private_project) { create(:project, :private) }
- let(:issue) { create(:issue, project: private_project, author: user) }
-
- context 'when user has access to the project' do
- it 'closes all issues passed' do
- private_project.add_maintainer(user)
-
- bulk_update(issues + [issue], state_event: 'close')
-
- expect(project.issues.opened).to be_empty
- expect(project.issues.closed).not_to be_empty
- expect(private_project.issues.closed).not_to be_empty
- end
- end
-
- context 'when user does not have access to project' do
- it 'only closes all issues that the user has access to' do
- bulk_update(issues + [issue], state_event: 'close')
-
- expect(project.issues.opened).to be_empty
- expect(project.issues.closed).not_to be_empty
- expect(private_project.issues.closed).to be_empty
- end
- end
- end
end
describe 'reopen issues' do
@@ -362,10 +337,29 @@ describe Issuable::BulkUpdateService do
end
end
end
+
+ describe 'updating issues from external project' do
+ it 'updates only issues that belong to the parent project' do
+ issue1 = create(:issue, project: project)
+ issue2 = create(:issue, project: create(:project))
+ result = bulk_update([issue1, issue2], assignee_ids: [user.id])
+
+ expect(result[:success]).to be_truthy
+ expect(result[:count]).to eq(1)
+
+ expect(issue1.reload.assignees).to eq([user])
+ expect(issue2.reload.assignees).to be_empty
+ end
+ end
end
context 'with issuables at a group level' do
let(:group) { create(:group) }
+ let(:parent) { group }
+
+ before do
+ group.add_reporter(user)
+ end
describe 'updating milestones' do
let(:milestone) { create(:milestone, group: group) }
@@ -398,11 +392,24 @@ describe Issuable::BulkUpdateService do
let(:regression) { create(:group_label, group: group) }
let(:merge_requests) { create(:group_label, group: group) }
- before do
- group.add_reporter(user)
- end
-
it_behaves_like 'updating labels'
end
+
+ describe 'with issues from external group' do
+ it 'updates issues that belong to the parent group or descendants' do
+ issue1 = create(:issue, project: create(:project, group: group))
+ issue2 = create(:issue, project: create(:project, group: create(:group)))
+ issue3 = create(:issue, project: create(:project, group: create(:group, parent: group)))
+ milestone = create(:milestone, group: group)
+ result = bulk_update([issue1, issue2, issue3], milestone_id: milestone.id)
+
+ expect(result[:success]).to be_truthy
+ expect(result[:count]).to eq(2)
+
+ expect(issue1.reload.milestone).to eq(milestone)
+ expect(issue2.reload.milestone).to be_nil
+ expect(issue3.reload.milestone).to eq(milestone)
+ end
+ end
end
end
diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
index f200c636aac..a772b911d8a 100644
--- a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
@@ -175,3 +175,64 @@ describe Metrics::Dashboard::GrafanaMetricEmbedService do
end
end
end
+
+describe Metrics::Dashboard::GrafanaUidParser do
+ let_it_be(:grafana_integration) { create(:grafana_integration) }
+ let_it_be(:project) { grafana_integration.project }
+
+ subject { described_class.new(grafana_url, project).parse }
+
+ context 'with a Grafana-defined uid' do
+ let(:grafana_url) { grafana_integration.grafana_url + '/d/XDaNK6amz/?panelId=1' }
+
+ it { is_expected.to eq 'XDaNK6amz' }
+ end
+
+ context 'with a user-defined uid' do
+ let(:grafana_url) { grafana_integration.grafana_url + '/d/pgbouncer-main/pgbouncer-overview?panelId=1' }
+
+ it { is_expected.to eq 'pgbouncer-main' }
+ end
+
+ context 'when a uid is not present' do
+ let(:grafana_url) { grafana_integration.grafana_url }
+
+ it { is_expected.to be nil }
+ end
+
+ context 'when the url starts with unrelated content' do
+ let(:grafana_url) { 'js:' + grafana_integration.grafana_url }
+
+ it { is_expected.to be nil }
+ end
+end
+
+describe Metrics::Dashboard::DatasourceNameParser do
+ include GrafanaApiHelpers
+
+ let(:grafana_url) { valid_grafana_dashboard_link('https://gitlab.grafana.net') }
+ let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/dashboard_response.json'), symbolize_names: true) }
+
+ subject { described_class.new(grafana_url, grafana_dashboard).parse }
+
+ it { is_expected.to eq 'GitLab Omnibus' }
+
+ context 'when the panelId is missing from the url' do
+ let(:grafana_url) { 'https:/gitlab.grafana.net/d/jbdbks/' }
+
+ it { is_expected.to be nil }
+ end
+
+ context 'when the panel is not present' do
+ # We're looking for panelId of 8, but only 6 is present
+ let(:grafana_dashboard) { { dashboard: { panels: [{ id: 6 }] } } }
+
+ it { is_expected.to be nil }
+ end
+
+ context 'when the dashboard panel does not have a datasource' do
+ let(:grafana_dashboard) { { dashboard: { panels: [{ id: 8 }] } } }
+
+ it { is_expected.to be nil }
+ end
+end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 642986bb176..d8ba042af35 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -296,9 +296,12 @@ describe Projects::DestroyService do
end
context 'as the root of a fork network' do
- let!(:fork_network) { create(:fork_network, root_project: project) }
+ let!(:fork_1) { fork_project(project, user) }
+ let!(:fork_2) { fork_project(project, user) }
it 'updates the fork network with the project name' do
+ fork_network = project.fork_network
+
destroy_project(project, user)
fork_network.reload
diff --git a/spec/services/projects/git_deduplication_service_spec.rb b/spec/services/projects/git_deduplication_service_spec.rb
index 3acbc46b473..9e6279da7de 100644
--- a/spec/services/projects/git_deduplication_service_spec.rb
+++ b/spec/services/projects/git_deduplication_service_spec.rb
@@ -58,6 +58,65 @@ describe Projects::GitDeduplicationService do
service.execute
end
+
+ context 'when visibility level of the project' do
+ before do
+ allow(pool.source_project).to receive(:repository_access_level).and_return(ProjectFeature::ENABLED)
+ end
+
+ context 'is private' do
+ it 'does not call fetch' do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::PRIVATE)
+ expect(pool.object_pool).not_to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ context 'is public' do
+ it 'calls fetch' do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::PUBLIC)
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ context 'is internal' do
+ it 'calls fetch' do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::INTERNAL)
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when the repository access level' do
+ before do
+ allow(pool.source_project).to receive(:visibility_level).and_return(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ context 'is private' do
+ it 'does not call fetch' do
+ allow(pool.source_project).to receive(:repository_access_level).and_return(ProjectFeature::PRIVATE)
+
+ expect(pool.object_pool).not_to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ context 'is greater than private' do
+ it 'calls fetch' do
+ allow(pool.source_project).to receive(:repository_access_level).and_return(ProjectFeature::PUBLIC)
+
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+ end
+ end
end
it 'links the repository to the object pool' do
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index a1175bf7123..a6bdc69cdca 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-describe Projects::UnlinkForkService do
+describe Projects::UnlinkForkService, :use_clean_rails_memory_store_caching do
include ProjectForksHelper
subject { described_class.new(forked_project, user) }
let(:project) { create(:project, :public) }
- let(:forked_project) { fork_project(project, user) }
+ let!(:forked_project) { fork_project(project, user) }
let(:user) { create(:user) }
context 'with opened merge request on the source project' do
@@ -86,4 +86,169 @@ describe Projects::UnlinkForkService do
expect { subject.execute }.not_to raise_error
end
end
+
+ context 'when given project is a source of forks' do
+ let!(:forked_project_2) { fork_project(project, user) }
+ let!(:fork_of_fork) { fork_project(forked_project, user) }
+
+ subject { described_class.new(project, user) }
+
+ context 'with opened merge requests from fork back to root project' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: forked_project) }
+ let!(:merge_request2) { create(:merge_request, source_project: project, target_project: fork_project(project)) }
+ let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }
+
+ let(:mr_close_service) { MergeRequests::CloseService.new(project, user) }
+
+ before do
+ allow(MergeRequests::CloseService).to receive(:new)
+ .with(project, user)
+ .and_return(mr_close_service)
+ end
+
+ it 'closes all pending merge requests' do
+ expect(mr_close_service).to receive(:execute).with(merge_request)
+ expect(mr_close_service).to receive(:execute).with(merge_request2)
+
+ subject.execute
+ end
+
+ it 'does not close merge requests that do not come from the project being unlinked' do
+ expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)
+
+ subject.execute
+ end
+ end
+
+ it 'removes its link to the fork network and updates direct network members' do
+ expect(project.fork_network_member).to be_present
+ expect(project.fork_network).to be_present
+ expect(project.forked_to_members.count).to eq(2)
+ expect(forked_project.forked_to_members.count).to eq(1)
+ expect(fork_of_fork.forked_to_members.count).to eq(0)
+
+ subject.execute
+
+ project.reload
+ forked_project.reload
+ fork_of_fork.reload
+
+ expect(project.fork_network_member).to be_nil
+ expect(project.fork_network).to be_nil
+ expect(forked_project.fork_network).to have_attributes(root_project_id: nil,
+ deleted_root_project_name: project.full_name)
+ expect(project.forked_to_members.count).to eq(0)
+ expect(forked_project.forked_to_members.count).to eq(1)
+ expect(fork_of_fork.forked_to_members.count).to eq(0)
+ end
+
+ it 'refreshes the forks count cache of the given project' do
+ expect(project.forks_count).to eq(2)
+
+ subject.execute
+
+ expect(project.forks_count).to be_zero
+ end
+
+ context 'when given project is a fork of an unlinked parent' do
+ let!(:fork_of_fork) { fork_project(forked_project, user) }
+ let(:lfs_object) { create(:lfs_object) }
+
+ before do
+ lfs_object.projects << project
+ end
+
+ it 'saves lfs objects to the root project' do
+ # Remove parent from network
+ described_class.new(forked_project, user).execute
+
+ described_class.new(fork_of_fork, user).execute
+
+ expect(lfs_object.projects).to include(fork_of_fork)
+ end
+ end
+
+ context 'and is node with a parent' do
+ subject { described_class.new(forked_project, user) }
+
+ context 'with opened merge requests from and to given project' do
+ let!(:mr_from_parent) { create(:merge_request, source_project: project, target_project: forked_project) }
+ let!(:mr_to_parent) { create(:merge_request, source_project: forked_project, target_project: project) }
+ let!(:mr_to_child) { create(:merge_request, source_project: forked_project, target_project: fork_of_fork) }
+ let!(:mr_from_child) { create(:merge_request, source_project: fork_of_fork, target_project: forked_project) }
+ let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }
+
+ let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) }
+
+ before do
+ allow(MergeRequests::CloseService).to receive(:new)
+ .with(forked_project, user)
+ .and_return(mr_close_service)
+ end
+
+ it 'close all pending merge requests' do
+ merge_requests = [mr_from_parent, mr_to_parent, mr_from_child, mr_to_child]
+
+ merge_requests.each do |mr|
+ expect(mr_close_service).to receive(:execute).with(mr).and_call_original
+ end
+
+ subject.execute
+
+ merge_requests = MergeRequest.where(id: merge_requests)
+
+ expect(merge_requests).to all(have_attributes(state: 'closed'))
+ end
+
+ it 'does not close merge requests which do not come from the project being unlinked' do
+ expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)
+
+ subject.execute
+ end
+ end
+
+ it 'refreshes the forks count cache of the parent and the given project' do
+ expect(project.forks_count).to eq(2)
+ expect(forked_project.forks_count).to eq(1)
+
+ subject.execute
+
+ expect(project.forks_count).to eq(1)
+ expect(forked_project.forks_count).to eq(0)
+ end
+
+ it 'removes its link to the fork network and updates direct network members' do
+ expect(project.fork_network).to be_present
+ expect(forked_project.fork_network).to be_present
+ expect(fork_of_fork.fork_network).to be_present
+
+ expect(project.forked_to_members.count).to eq(2)
+ expect(forked_project.forked_to_members.count).to eq(1)
+ expect(fork_of_fork.forked_to_members.count).to eq(0)
+
+ subject.execute
+ project.reload
+ forked_project.reload
+ fork_of_fork.reload
+
+ expect(project.fork_network).to be_present
+ expect(forked_project.fork_network).to be_nil
+ expect(fork_of_fork.fork_network).to be_present
+
+ expect(project.forked_to_members.count).to eq(1) # 1 child is gone
+ expect(forked_project.forked_to_members.count).to eq(0)
+ expect(fork_of_fork.forked_to_members.count).to eq(0)
+ end
+ end
+ end
+
+ context 'when given project is not part of a fork network' do
+ let!(:project_without_forks) { create(:project, :public) }
+
+ subject { described_class.new(project_without_forks, user) }
+
+ it 'does not raise errors' do
+ expect { subject.execute }.not_to raise_error
+ end
+ end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index c848a5397e1..3092fb7116a 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -16,7 +16,14 @@ describe Projects::UpdateService do
let(:admin) { create(:admin) }
context 'when changing visibility level' do
- context 'when visibility_level is INTERNAL' do
+ def expect_to_call_unlink_fork_service
+ service = Projects::UnlinkForkService.new(project, user)
+
+ expect(Projects::UnlinkForkService).to receive(:new).with(project, user).and_return(service)
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ context 'when visibility_level changes to INTERNAL' do
it 'updates the project to internal' do
expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in)
@@ -25,9 +32,21 @@ describe Projects::UpdateService do
expect(result).to eq({ status: :success })
expect(project).to be_internal
end
+
+ context 'and project is PUBLIC' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'unlinks project from fork network' do
+ expect_to_call_unlink_fork_service
+
+ update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
end
- context 'when visibility_level is PUBLIC' do
+ context 'when visibility_level changes to PUBLIC' do
it 'updates the project to public' do
expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in)
@@ -36,9 +55,17 @@ describe Projects::UpdateService do
expect(result).to eq({ status: :success })
expect(project).to be_public
end
+
+ context 'and project is PRIVATE' do
+ it 'does not unlink project from fork network' do
+ expect(Projects::UnlinkForkService).not_to receive(:new)
+
+ update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
end
- context 'when visibility_level is PRIVATE' do
+ context 'when visibility_level changes to PRIVATE' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
@@ -52,6 +79,30 @@ describe Projects::UpdateService do
expect(result).to eq({ status: :success })
expect(project).to be_private
end
+
+ context 'and project is PUBLIC' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'unlinks project from fork network' do
+ expect_to_call_unlink_fork_service
+
+ update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'and project is INTERNAL' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'unlinks project from fork network' do
+ expect_to_call_unlink_fork_service
+
+ update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
end
context 'when visibility levels are restricted to PUBLIC only' do
@@ -107,28 +158,48 @@ describe Projects::UpdateService do
let(:project) { create(:project, :internal) }
let(:forked_project) { fork_project(project) }
- it 'updates forks visibility level when parent set to more restrictive' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+ context 'and unlink forks feature flag is off' do
+ before do
+ stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
+ end
+
+ it 'updates forks visibility level when parent set to more restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
+
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
+
+ expect(project).to be_private
+ expect(forked_project.reload).to be_private
+ end
+
+ it 'does not update forks visibility level when parent set to less restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
- expect(project).to be_internal
- expect(forked_project).to be_internal
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- expect(update_project(project, admin, opts)).to eq({ status: :success })
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
- expect(project).to be_private
- expect(forked_project.reload).to be_private
+ expect(project).to be_public
+ expect(forked_project.reload).to be_internal
+ end
end
- it 'does not update forks visibility level when parent set to less restrictive' do
- opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+ context 'and unlink forks feature flag is on' do
+ it 'does not change visibility of forks' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
- expect(project).to be_internal
- expect(forked_project).to be_internal
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- expect(update_project(project, admin, opts)).to eq({ status: :success })
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
- expect(project).to be_public
- expect(forked_project.reload).to be_internal
+ expect(project).to be_private
+ expect(forked_project.reload).to be_internal
+ end
end
end
diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb
index 653f17a4324..8b68da6674a 100644
--- a/spec/services/submit_usage_ping_service_spec.rb
+++ b/spec/services/submit_usage_ping_service_spec.rb
@@ -46,12 +46,12 @@ describe SubmitUsagePingService do
stub_response(with_conv_index_params)
expect { subject.execute }
- .to change { ConversationalDevelopmentIndex::Metric.count }
+ .to change { DevOpsScore::Metric.count }
.by(1)
- expect(ConversationalDevelopmentIndex::Metric.last.leader_issues).to eq 10.2
- expect(ConversationalDevelopmentIndex::Metric.last.instance_issues).to eq 3.2
- expect(ConversationalDevelopmentIndex::Metric.last.percentage_issues).to eq 31.37
+ expect(DevOpsScore::Metric.last.leader_issues).to eq 10.2
+ expect(DevOpsScore::Metric.last.instance_issues).to eq 3.2
+ expect(DevOpsScore::Metric.last.percentage_issues).to eq 31.37
end
end
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/users/repair_ldap_blocked_service_spec.rb
index 9918bb8e054..0205b40bc97 100644
--- a/spec/services/repair_ldap_blocked_user_service_spec.rb
+++ b/spec/services/users/repair_ldap_blocked_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe RepairLdapBlockedUserService do
+describe Users::RepairLdapBlockedService do
let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
let(:identity) { user.ldap_identity }
subject(:service) { described_class.new(user) }
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
index 6f385d6e019..aaf408f6143 100644
--- a/spec/support/database_cleaner.rb
+++ b/spec/support/database_cleaner.rb
@@ -15,6 +15,39 @@ RSpec.configure do |config|
delete_from_all_tables!
end
+ config.append_after(:context, :migration) do
+ delete_from_all_tables!
+
+ # Postgres maximum number of columns in a table is 1600 (https://github.com/postgres/postgres/blob/de41869b64d57160f58852eab20a27f248188135/src/include/access/htup_details.h#L23-L47).
+ # And since:
+ # "The DROP COLUMN form does not physically remove the column, but simply makes
+ # it invisible to SQL operations. Subsequent insert and update operations in the
+ # table will store a null value for the column. Thus, dropping a column is quick
+ # but it will not immediately reduce the on-disk size of your table, as the space
+ # occupied by the dropped column is not reclaimed.
+ # The space will be reclaimed over time as existing rows are updated."
+ # according to https://www.postgresql.org/docs/current/sql-altertable.html.
+ # We drop and recreate the database if any table has more than 1200 columns, just to be safe.
+ max_allowed_columns = 1200
+ tables_with_more_than_allowed_columns =
+ ApplicationRecord.connection.execute("SELECT attrelid::regclass::text AS table, COUNT(*) AS column_count FROM pg_attribute GROUP BY attrelid HAVING COUNT(*) > #{max_allowed_columns}")
+
+ if tables_with_more_than_allowed_columns.any?
+ tables_with_more_than_allowed_columns.each do |result|
+ puts "The #{result['table']} table has #{result['column_count']} columns."
+ end
+ puts "Recreating the database"
+ start = Gitlab::Metrics::System.monotonic_time
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
+ ActiveRecord::Tasks::DatabaseTasks.create_current
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+
+ puts "Database re-creation done in #{Gitlab::Metrics::System.monotonic_time - start}"
+ end
+ end
+
config.around(:each, :delete) do |example|
self.class.use_transactional_tests = false
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb
index f26a8554055..2fee58a9f30 100644
--- a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb
@@ -2,7 +2,7 @@
# Shared examples for ProjectTreeRestorer (shared to allow the testing
# of EE-specific features)
-RSpec.shared_examples 'restores project correctly' do |**results|
+RSpec.shared_examples 'restores project successfully' do |**results|
it 'restores the project' do
expect(shared.errors).to be_empty
expect(restored_project_json).to be_truthy
@@ -34,4 +34,8 @@ RSpec.shared_examples 'restores project correctly' do |**results|
expect(project.import_type).to be_nil
expect(project.creator_id).not_to eq 123
end
+
+ it 'records exact number of import failures' do
+ expect(project.import_failures.size).to eq(results.fetch(:import_failures, 0))
+ end
end
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index 98bf647a9bc..7936a8eb974 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -80,7 +80,7 @@ shared_examples_for "chat service" do |service_name|
it_behaves_like "triggered #{service_name} service"
- it "specifies the webhook when it is configured" do
+ it "specifies the webhook when it is configured", if: defined?(client) do
expect(client).to receive(:new).with(client_arguments).and_return(double(:chat_service).as_null_object)
subject.execute(sample_data)
diff --git a/spec/tasks/gitlab/import_export/import_rake_spec.rb b/spec/tasks/gitlab/import_export/import_rake_spec.rb
new file mode 100644
index 00000000000..18b89912b9f
--- /dev/null
+++ b/spec/tasks/gitlab/import_export/import_rake_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+describe 'gitlab:import_export:import rake task', :sidekiq do
+ let(:username) { 'root' }
+ let(:namespace_path) { username }
+ let!(:user) { create(:user, username: username) }
+ let(:task_params) { [username, namespace_path, project_name, archive_path] }
+ let(:project) { Project.find_by_full_path("#{namespace_path}/#{project_name}") }
+
+ before do
+ Rake.application.rake_require('tasks/gitlab/import_export/import')
+ allow(Settings.uploads.object_store).to receive(:[]=).and_call_original
+ allow_any_instance_of(GitlabProjectImport).to receive(:exit)
+ .and_raise(RuntimeError, 'exit not handled')
+ end
+
+ around do |example|
+ old_direct_upload_setting = Settings.uploads.object_store['direct_upload']
+ old_background_upload_setting = Settings.uploads.object_store['background_upload']
+
+ Settings.uploads.object_store['direct_upload'] = true
+ Settings.uploads.object_store['background_upload'] = true
+
+ example.run
+
+ Settings.uploads.object_store['direct_upload'] = old_direct_upload_setting
+ Settings.uploads.object_store['background_upload'] = old_background_upload_setting
+ end
+
+ subject { run_rake_task('gitlab:import_export:import', task_params) }
+
+ context 'when project import is valid' do
+ let(:project_name) { 'import_rake_test_project' }
+ let(:archive_path) { 'spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz' }
+
+ it 'performs project import successfully' do
+ expect { subject }.to output(/Done!/).to_stdout
+ expect { subject }.not_to raise_error
+
+ expect(project.merge_requests.count).to be > 0
+ expect(project.issues.count).to be > 0
+ expect(project.milestones.count).to be > 0
+ expect(project.import_state.status).to eq('finished')
+ end
+
+ it 'disables direct & background upload only during project creation' do
+ expect_next_instance_of(Projects::GitlabProjectsImportService) do |service|
+ expect(service).to receive(:execute).and_wrap_original do |m|
+ expect(Settings.uploads.object_store['background_upload']).to eq(false)
+ expect(Settings.uploads.object_store['direct_upload']).to eq(false)
+
+ m.call
+ end
+ end
+
+ expect_next_instance_of(GitlabProjectImport) do |importer|
+ expect(importer).to receive(:execute_sidekiq_job).and_wrap_original do |m|
+ expect(Settings.uploads.object_store['background_upload']).to eq(true)
+ expect(Settings.uploads.object_store['direct_upload']).to eq(true)
+ expect(Settings.uploads.object_store).not_to receive(:[]=).with('backgroud_upload', false)
+ expect(Settings.uploads.object_store).not_to receive(:[]=).with('direct_upload', false)
+
+ m.call
+ end
+ end
+
+ subject
+ end
+ end
+
+ context 'when project import is invalid' do
+ let(:project_name) { 'import_rake_invalid_test_project' }
+ let(:archive_path) { 'spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz' }
+ let(:not_imported_message) { /Total number of not imported relations: 1/ }
+ let(:error) { /Validation failed: Notes is invalid/ }
+
+ context 'when import_graceful_failures feature flag is enabled' do
+ before do
+ stub_feature_flags(import_graceful_failures: true)
+ end
+
+ it 'performs project import successfully' do
+ expect { subject }.to output(not_imported_message).to_stdout
+ expect { subject }.not_to raise_error
+
+ expect(project.merge_requests).to be_empty
+ expect(project.import_state.last_error).to be_nil
+ expect(project.import_state.status).to eq('finished')
+ end
+ end
+
+ context 'when import_graceful_failures feature flag is disabled' do
+ before do
+ stub_feature_flags(import_graceful_failures: false)
+ end
+
+ it 'fails project import with an error' do
+ # Catch exit call, and raise exception instead
+ expect_any_instance_of(GitlabProjectImport).to receive(:exit)
+ .with(1).and_raise(SystemExit)
+
+ expect { subject }.to raise_error(SystemExit).and output(error).to_stdout
+
+ expect(project.merge_requests).to be_empty
+ expect(project.import_state.last_error).to match(error)
+ expect(project.import_state.status).to eq('failed')
+ end
+ end
+ end
+end
diff --git a/spec/views/layouts/application.html.haml_spec.rb b/spec/views/layouts/application.html.haml_spec.rb
new file mode 100644
index 00000000000..bdd4a97a1f5
--- /dev/null
+++ b/spec/views/layouts/application.html.haml_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'layouts/application' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ allow(view).to receive(:session).and_return({})
+ allow(view).to receive(:user_signed_in?).and_return(true)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ context 'body data elements for pageview context' do
+ let(:body_data) do
+ {
+ body_data_page: 'projects:issues:show',
+ body_data_page_type_id: '1',
+ body_data_project_id: '2',
+ body_data_namespace_id: '3'
+ }
+ end
+
+ before do
+ allow(view).to receive(:body_data).and_return(body_data)
+ render
+ end
+
+ it 'includes the body element page' do
+ expect(rendered).to include('data-page="projects:issues:show"')
+ end
+
+ it 'includes the body element page_type_id' do
+ expect(rendered).to include('data-page-type-id="1"')
+ end
+
+ it 'includes the body element project_id' do
+ expect(rendered).to include('data-project-id="2"')
+ end
+
+ it 'includes the body element namespace_id' do
+ expect(rendered).to include('data-namespace-id="3"')
+ end
+ end
+end
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index 26e429ac5d0..1a04ffed103 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -126,6 +126,16 @@ describe 'layouts/header/_new_dropdown' do
expect(rendered).to have_link('New snippet', href: new_snippet_path)
end
+
+ context 'when the user is not allowed to create snippets' do
+ let(:user) { create(:user, :external)}
+
+ it 'has no "New snippet" link' do
+ render
+
+ expect(rendered).not_to have_link('New snippet', href: new_snippet_path)
+ end
+ end
end
def stub_current_user(current_user)
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 40927a22dc4..8005b549838 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -53,6 +53,7 @@ describe 'projects/edit' do
render
expect(rendered).to have_content('Remove fork relationship')
+ expect(rendered).to have_link(source_project.full_name, href: project_path(source_project))
end
it 'hides the fork relationship settings from an unauthorized user' do
@@ -78,7 +79,7 @@ describe 'projects/edit' do
render
expect(rendered).to have_content('Remove fork relationship')
- expect(rendered).to have_content(source_project.full_name)
+ expect(rendered).to have_link(source_project.full_name, href: project_path(source_project))
end
end
end
diff --git a/vendor/project_templates/hexo.tar.gz b/vendor/project_templates/hexo.tar.gz
index 033f363b8df..489da1a34ec 100644
--- a/vendor/project_templates/hexo.tar.gz
+++ b/vendor/project_templates/hexo.tar.gz
Binary files differ
diff --git a/vendor/project_templates/hugo.tar.gz b/vendor/project_templates/hugo.tar.gz
index f479ea12900..1f756a696e3 100644
--- a/vendor/project_templates/hugo.tar.gz
+++ b/vendor/project_templates/hugo.tar.gz
Binary files differ
diff --git a/vendor/project_templates/jekyll.tar.gz b/vendor/project_templates/jekyll.tar.gz
index c323ce6fac6..0a97723712a 100644
--- a/vendor/project_templates/jekyll.tar.gz
+++ b/vendor/project_templates/jekyll.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index df27000b93f..c2f8345c0a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -722,10 +722,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.82.0.tgz#c059c460afc13ebfe9df370521ca8963fa5afb80"
integrity sha512-9L4Brys2LCk44lHvFsCFDKN768lYjoMVYDb4PD7FSjqUEruQQ1SRj0rvb1RWKLhiTCDKrtDOXkH6I1TTEms24w==
-"@gitlab/ui@7.16.1":
- version "7.16.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-7.16.1.tgz#a539bd2e39866549f71d8678efe7cca8478ebde3"
- integrity sha512-7SdwSC2P2/PKZNaIzNihAudSpP95cex98i6IMcukK0ocJYvHr8S9s8GoznaD8YugTR1EGhu+f1M6ubneU5vUwQ==
+"@gitlab/ui@8.0.1":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-8.0.1.tgz#4e3b4791045540785cc389af931e24c6411910ca"
+ integrity sha512-PfZPlx3f12wcGxe0eMAXRk1gdhEAkX4czQWAt8EQ1WosKiADCNzCpEPR4jyWa60RF/+zHqJKIjq0VqLMClk8Jg==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
@@ -1637,6 +1637,14 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=
+array-includes@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+ integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
array-union@^1.0.1, array-union@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
@@ -3659,7 +3667,7 @@ default-require-extensions@^2.0.0:
dependencies:
strip-bom "^3.0.0"
-define-properties@^1.1.2:
+define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@@ -4111,22 +4119,26 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.5.1, es-abstract@^1.6.1:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
- integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+es-abstract@^1.12.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.2.tgz#4e874331645e9925edef141e74fc4bd144669d34"
+ integrity sha512-jYo/J8XU2emLXl3OLwfwtuFfuF2w6DYPs+xy9ZfVyPkDcrauu6LYrw/q2TyCtrbc/KUdCiC5e9UajRhgNkVopA==
dependencies:
- es-to-primitive "^1.2.0"
+ es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
+ has-symbols "^1.0.1"
is-callable "^1.1.4"
is-regex "^1.0.4"
- object-keys "^1.0.12"
+ object-inspect "^1.7.0"
+ object-keys "^1.1.1"
+ string.prototype.trimleft "^2.1.0"
+ string.prototype.trimright "^2.1.0"
-es-to-primitive@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
- integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+es-to-primitive@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
+ integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
dependencies:
is-callable "^1.1.4"
is-date-object "^1.0.1"
@@ -4213,10 +4225,10 @@ eslint-import-resolver-webpack@^0.10.1:
resolve "^1.4.0"
semver "^5.3.0"
-eslint-module-utils@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49"
- integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==
+eslint-module-utils@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c"
+ integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==
dependencies:
debug "^2.6.8"
pkg-dir "^2.0.0"
@@ -4231,21 +4243,22 @@ eslint-plugin-filenames@^1.3.2:
lodash.snakecase "4.1.1"
lodash.upperfirst "4.3.1"
-eslint-plugin-import@^2.14.0, eslint-plugin-import@^2.16.0:
- version "2.16.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f"
- integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==
+eslint-plugin-import@^2.16.0, eslint-plugin-import@^2.18.2:
+ version "2.18.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6"
+ integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==
dependencies:
+ array-includes "^3.0.3"
contains-path "^0.1.0"
debug "^2.6.9"
doctrine "1.5.0"
eslint-import-resolver-node "^0.3.2"
- eslint-module-utils "^2.3.0"
+ eslint-module-utils "^2.4.0"
has "^1.0.3"
- lodash "^4.17.11"
minimatch "^3.0.4"
+ object.values "^1.1.0"
read-pkg-up "^2.0.0"
- resolve "^1.9.0"
+ resolve "^1.11.0"
eslint-plugin-jasmine@^2.10.1:
version "2.10.1"
@@ -5369,10 +5382,10 @@ has-flag@^3.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
-has-symbols@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
- integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+has-symbols@^1.0.0, has-symbols@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
+ integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
has-unicode@^2.0.0:
version "2.0.1"
@@ -8168,7 +8181,12 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-keys@^1.0.11, object-keys@^1.0.12:
+object-inspect@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
+ integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+
+object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
@@ -8215,6 +8233,16 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
+object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
@@ -9723,10 +9751,10 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
-resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.9.0:
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
- integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
+resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
+ integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
dependencies:
path-parse "^1.0.6"
@@ -10518,6 +10546,22 @@ string-width@^4.1.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^5.2.0"
+string.prototype.trimleft@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
+ integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
+ dependencies:
+ define-properties "^1.1.3"
+ function-bind "^1.1.1"
+
+string.prototype.trimright@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
+ integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
+ dependencies:
+ define-properties "^1.1.3"
+ function-bind "^1.1.1"
+
string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"