summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml3
-rw-r--r--.gitlab/CODEOWNERS.disabled4
-rw-r--r--.gitlab/issue_templates/Feature proposal.md4
-rw-r--r--CHANGELOG.md26
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--Dangerfile33
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile9
-rw-r--r--Gemfile.lock78
-rw-r--r--app/assets/javascripts/avatar_picker.js (renamed from app/assets/javascripts/group_avatar.js)9
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js6
-rw-r--r--app/assets/javascripts/boards/models/issue.js12
-rw-r--r--app/assets/javascripts/breakpoints.js3
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js34
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js7
-rw-r--r--app/assets/javascripts/contextual_sidebar.js3
-rw-r--r--app/assets/javascripts/create_label.js9
-rw-r--r--app/assets/javascripts/diffs/components/app.vue15
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue137
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue6
-rw-r--r--app/assets/javascripts/diffs/constants.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js12
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_storage_keys.js4
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue57
-rw-r--r--app/assets/javascripts/gl_dropdown.js5
-rw-r--r--app/assets/javascripts/gl_field_errors.js2
-rw-r--r--app/assets/javascripts/helpers/monitor_helper.js17
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue35
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue5
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue23
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue14
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js5
-rw-r--r--app/assets/javascripts/ide/stores/getters.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js34
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/constants.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js14
-rw-r--r--app/assets/javascripts/ide/stores/utils.js28
-rw-r--r--app/assets/javascripts/issue.js14
-rw-r--r--app/assets/javascripts/issue_show/index.js7
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js15
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue19
-rw-r--r--app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue30
-rw-r--r--app/assets/javascripts/jobs/index.js1
-rw-r--r--app/assets/javascripts/jobs/store/getters.js3
-rw-r--r--app/assets/javascripts/labels_select.js97
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js44
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js32
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue65
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue100
-rw-r--r--app/assets/javascripts/monitoring/constants.js13
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js1
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js6
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js39
-rw-r--r--app/assets/javascripts/monitoring/utils.js34
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue44
-rw-r--r--app/assets/javascripts/notifications_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/admin/groups/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/groups/new/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue5
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue121
-rw-r--r--app/assets/javascripts/related_merge_requests/index.js24
-rw-r--r--app/assets/javascripts/related_merge_requests/store/actions.js37
-rw-r--r--app/assets/javascripts/related_merge_requests/store/index.js14
-rw-r--r--app/assets/javascripts/related_merge_requests/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/related_merge_requests/store/mutations.js19
-rw-r--r--app/assets/javascripts/related_merge_requests/store/state.js7
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue7
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue7
-rw-r--r--app/assets/javascripts/serverless/components/area.vue146
-rw-r--r--app/assets/javascripts/serverless/components/function_details.vue54
-rw-r--r--app/assets/javascripts/serverless/components/function_row.vue5
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue44
-rw-r--r--app/assets/javascripts/serverless/components/missing_prometheus.vue63
-rw-r--r--app/assets/javascripts/serverless/constants.js3
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js125
-rw-r--r--app/assets/javascripts/serverless/services/get_functions_service.js11
-rw-r--r--app/assets/javascripts/serverless/store/actions.js113
-rw-r--r--app/assets/javascripts/serverless/store/getters.js10
-rw-r--r--app/assets/javascripts/serverless/store/index.js18
-rw-r--r--app/assets/javascripts/serverless/store/mutation_types.js9
-rw-r--r--app/assets/javascripts/serverless/store/mutations.js38
-rw-r--r--app/assets/javascripts/serverless/store/state.js13
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_details_store.js11
-rw-r--r--app/assets/javascripts/serverless/stores/serverless_store.js29
-rw-r--r--app/assets/javascripts/serverless/utils.js23
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue48
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue38
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue74
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue103
-rw-r--r--app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue47
-rw-r--r--app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js62
-rw-r--r--app/assets/stylesheets/application.scss35
-rw-r--r--app/assets/stylesheets/components/project_list_item.scss24
-rw-r--r--app/assets/stylesheets/framework/callout.scss4
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss6
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/framework/forms.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss97
-rw-r--r--app/assets/stylesheets/pages/diff.scss33
-rw-r--r--app/assets/stylesheets/pages/environments.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss36
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss54
-rw-r--r--app/assets/stylesheets/pages/monitor.scss5
-rw-r--r--app/assets/stylesheets/pages/notes.scss1
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss30
-rw-r--r--app/assets/stylesheets/pages/projects.scss3
-rw-r--r--app/assets/stylesheets/utilities.scss17
-rw-r--r--app/controllers/admin/groups_controller.rb3
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb1
-rw-r--r--app/controllers/concerns/preview_markdown.rb2
-rw-r--r--app/controllers/dashboard_controller.rb5
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb7
-rw-r--r--app/controllers/projects/application_controller.rb6
-rw-r--r--app/controllers/projects/commits_controller.rb2
-rw-r--r--app/controllers/projects/environments/prometheus_api_controller.rb37
-rw-r--r--app/controllers/projects/environments_controller.rb14
-rw-r--r--app/controllers/projects/git_http_client_controller.rb6
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects/pipelines_controller.rb12
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb23
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb47
-rw-r--r--app/finders/merge_requests_finder.rb13
-rw-r--r--app/finders/projects/serverless/functions_finder.rb28
-rw-r--r--app/graphql/gitlab_schema.rb31
-rw-r--r--app/graphql/types/base_field.rb9
-rw-r--r--app/graphql/types/ci/pipeline_type.rb6
-rw-r--r--app/graphql/types/issue_type.rb12
-rw-r--r--app/graphql/types/merge_request_type.rb8
-rw-r--r--app/graphql/types/milestone_type.rb2
-rw-r--r--app/graphql/types/project_type.rb14
-rw-r--r--app/graphql/types/query_type.rb3
-rw-r--r--app/graphql/types/user_type.rb2
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/blob_helper.rb14
-rw-r--r--app/helpers/builds_helper.rb6
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci_variables_helper.rb8
-rw-r--r--app/helpers/form_helper.rb5
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb59
-rw-r--r--app/helpers/namespaces_helper.rb7
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/search_helper.rb50
-rw-r--r--app/helpers/tree_helper.rb10
-rw-r--r--app/helpers/visibility_level_helper.rb16
-rw-r--r--app/helpers/wiki_helper.rb20
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/build.rb13
-rw-r--r--app/models/ci/build_runner_session.rb18
-rw-r--r--app/models/concerns/artifact_migratable.rb14
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/prometheus_adapter.rb4
-rw-r--r--app/models/concerns/reactive_caching.rb39
-rw-r--r--app/models/concerns/sortable.rb28
-rw-r--r--app/models/environment.rb6
-rw-r--r--app/models/global_label.rb2
-rw-r--r--app/models/group.rb11
-rw-r--r--app/models/individual_note_discussion.rb2
-rw-r--r--app/models/project_wiki.rb16
-rw-r--r--app/models/repository.rb5
-rw-r--r--app/models/serverless/function.rb26
-rw-r--r--app/models/user.rb42
-rw-r--r--app/models/wiki_page.rb19
-rw-r--r--app/policies/group_policy.rb11
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/presenters/ci/bridge_presenter.rb9
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/group_variable_entity.rb1
-rw-r--r--app/serializers/issue_entity.rb2
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/serializers/projects/serverless/service_entity.rb7
-rw-r--r--app/serializers/suggestion_entity.rb2
-rw-r--r--app/serializers/suggestion_serializer.rb9
-rw-r--r--app/serializers/variable_entity.rb1
-rw-r--r--app/services/clusters/applications/base_helm_service.rb12
-rw-r--r--app/services/concerns/suggestible.rb7
-rw-r--r--app/services/groups/base_service.rb6
-rw-r--r--app/services/groups/create_service.rb2
-rw-r--r--app/services/groups/update_service.rb1
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb4
-rw-r--r--app/services/merge_requests/base_service.rb33
-rw-r--r--app/services/preview_markdown_service.rb28
-rw-r--r--app/services/prometheus/proxy_service.rb116
-rw-r--r--app/services/users/build_service.rb6
-rw-r--r--app/uploaders/legacy_artifact_uploader.rb3
-rw-r--r--app/uploaders/records_uploads.rb4
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml4
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml4
-rw-r--r--app/views/ci/variables/_content.html.haml2
-rw-r--r--app/views/ci/variables/_header.html.haml2
-rw-r--r--app/views/ci/variables/_variable_row.html.haml20
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml47
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml61
-rw-r--r--app/views/clusters/platforms/kubernetes/_form.html.haml89
-rw-r--r--app/views/groups/_group_admin_settings.html.haml4
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/settings/_general.html.haml4
-rw-r--r--app/views/groups/settings/_permissions.html.haml1
-rw-r--r--app/views/groups/settings/_project_creation_level.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/_new_project_fields.html.haml6
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml36
-rw-r--r--app/views/projects/buttons/_download_links.html.haml9
-rw-r--r--app/views/projects/diffs/_replaced_image_diff.html.haml6
-rw-r--r--app/views/projects/diffs/_single_image_diff.html.haml2
-rw-r--r--app/views/projects/edit.html.haml27
-rw-r--r--app/views/projects/issues/_closed_by_box.html.haml4
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml36
-rw-r--r--app/views/projects/issues/show.html.haml5
-rw-r--r--app/views/projects/jobs/show.html.haml1
-rw-r--r--app/views/projects/pipelines/charts.html.haml4
-rw-r--r--app/views/projects/pipelines/new.html.haml4
-rw-r--r--app/views/projects/serverless/functions/index.html.haml5
-rw-r--r--app/views/projects/serverless/functions/show.html.haml11
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml9
-rw-r--r--app/views/projects/wikis/pages.html.haml13
-rw-r--r--app/views/shared/_choose_avatar_button.html.haml4
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml4
-rw-r--r--app/views/shared/_delete_label_modal.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml10
-rw-r--r--app/views/shared/form_elements/_description.html.haml2
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml8
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml8
-rw-r--r--app/views/shared/labels/_form.html.haml4
-rw-r--r--app/views/shared/milestones/_issuable.html.haml3
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml3
-rw-r--r--app/views/shared/notes/_form.html.haml2
-rw-r--r--app/workers/reactive_caching_worker.rb7
-rw-r--r--changelogs/unreleased/13784-validate-variables-for-masking.yml5
-rw-r--r--changelogs/unreleased/24704-download-repository-path.yml5
-rw-r--r--changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml5
-rw-r--r--changelogs/unreleased/38564-cant-leave-subgroup.yml5
-rw-r--r--changelogs/unreleased/48090-filter-sensitive-metric-labels.yml5
-rw-r--r--changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/54417-graphql-type-authorization.yml5
-rw-r--r--changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml5
-rw-r--r--changelogs/unreleased/55964-fix-email-encoding.yml5
-rw-r--r--changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml5
-rw-r--r--changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml5
-rw-r--r--changelogs/unreleased/57364-improve-diff-nav-header.yml5
-rw-r--r--changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml5
-rw-r--r--changelogs/unreleased/57493-add-limit-to-user-name.yml5
-rw-r--r--changelogs/unreleased/57602-create-cluster-validations.yml5
-rw-r--r--changelogs/unreleased/57668-create-file-from-url.yml5
-rw-r--r--changelogs/unreleased/58375-api-controller.yml5
-rw-r--r--changelogs/unreleased/58375-reactive-caching-changes.yml5
-rw-r--r--changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml5
-rw-r--r--changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml5
-rw-r--r--changelogs/unreleased/58835-button-run-pipeline.yml5
-rw-r--r--changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml5
-rw-r--r--changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml5
-rw-r--r--changelogs/unreleased/60068-avoid-null-domain-help-text.yml5
-rw-r--r--changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml5
-rw-r--r--changelogs/unreleased/add_backtrace_to_kubernetes_log.yml5
-rw-r--r--changelogs/unreleased/allow-ref-name-caching-projects-controller.yml5
-rw-r--r--changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml5
-rw-r--r--changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml5
-rw-r--r--changelogs/unreleased/create-label-and-list-checkbox.yml5
-rw-r--r--changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml5
-rw-r--r--changelogs/unreleased/duplicate-related-mrs.yml5
-rw-r--r--changelogs/unreleased/extend-cte-optimisations-to-projects.yml5
-rw-r--r--changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml5
-rw-r--r--changelogs/unreleased/fix-UI-links-to-route-map-info.yml5
-rw-r--r--changelogs/unreleased/fix-include-ci-yaml.yml5
-rw-r--r--changelogs/unreleased/fix-issues-time-counter.yml5
-rw-r--r--changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml5
-rw-r--r--changelogs/unreleased/fix-missing-border.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.32.0.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.33.0.yml5
-rw-r--r--changelogs/unreleased/graphql-prometheus.yml5
-rw-r--r--changelogs/unreleased/id-51433-sort-wiki-by-date.yml5
-rw-r--r--changelogs/unreleased/ide-fix-detect-mr-from-fork.yml5
-rw-r--r--changelogs/unreleased/knative-prometheus.yml5
-rw-r--r--changelogs/unreleased/localize-notification-dropdown.yml5
-rw-r--r--changelogs/unreleased/minimized-multiple-queries-ce.yml5
-rw-r--r--changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml5
-rw-r--r--changelogs/unreleased/osw-support-multi-line-suggestions.yml5
-rw-r--r--changelogs/unreleased/recreate-all-diffs-on-import.yml5
-rw-r--r--changelogs/unreleased/remove-ci-charts-undescriptive-header.yml5
-rw-r--r--changelogs/unreleased/sh-fix-project-branches-merge-status.yml5
-rw-r--r--changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml5
-rw-r--r--changelogs/unreleased/sh-fix-ref-name-caching.yml5
-rw-r--r--changelogs/unreleased/sh-fix-rugged-tree-entries.yml5
-rw-r--r--changelogs/unreleased/sh-force-gc-after-import.yml5
-rw-r--r--changelogs/unreleased/sh-update-rails-5-0-7-2.yml5
-rw-r--r--changelogs/unreleased/stop-signing-avatar-paths.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-shell.yml5
-rw-r--r--changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml5
-rw-r--r--changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml5
-rw-r--r--config.ru4
-rw-r--r--config/helpers/is_ee_env.js4
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/graphql.rb3
-rw-r--r--config/initializers/premailer.rb3
-rw-r--r--config/prometheus/common_metrics.yml10
-rw-r--r--config/routes/project.rb8
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/fixtures/development/02_application_settings.rb10
-rw-r--r--db/fixtures/development/02_settings.rb8
-rw-r--r--db/fixtures/development/08_settings.rb7
-rw-r--r--db/fixtures/production/001_application_settings.rb2
-rw-r--r--db/migrate/20190311132500_add_default_project_creation_application_setting.rb22
-rw-r--r--db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb22
-rw-r--r--db/migrate/20190325080727_truncate_user_fullname.rb21
-rw-r--r--db/migrate/20190326164045_import_common_metrics_knative.rb17
-rw-r--r--db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb33
-rw-r--r--db/post_migrate/20190325111602_rename_v2_root_namespaces.rb27
-rw-r--r--db/schema.rb4
-rw-r--r--doc/administration/auth/google_secure_ldap.md207
-rw-r--r--doc/administration/auth/img/google_secure_ldap_add_step_1.pngbin0 -> 28849 bytes
-rw-r--r--doc/administration/auth/img/google_secure_ldap_add_step_2.pngbin0 -> 82115 bytes
-rw-r--r--doc/administration/auth/img/google_secure_ldap_client_settings.pngbin0 -> 63959 bytes
-rw-r--r--doc/administration/auth/ldap.md8
-rw-r--r--doc/administration/container_registry.md12
-rw-r--r--doc/administration/git_protocol.md2
-rw-r--r--doc/api/runners.md35
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request.pngbin18834 -> 14044 bytes
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request_pipeline.pngbin0 -> 10152 bytes
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.pngbin0 -> 10889 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md75
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/ci/variables/predefined_variables.md3
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md28
-rw-r--r--doc/ci/yaml/README.md148
-rw-r--r--doc/development/api_graphql_styleguide.md140
-rw-r--r--doc/development/ee_features.md6
-rw-r--r--doc/development/fe_guide/style_guide_scss.md10
-rw-r--r--doc/development/testing_guide/frontend_testing.md4
-rw-r--r--doc/development/testing_guide/review_apps.md4
-rw-r--r--doc/install/README.md4
-rw-r--r--doc/install/kubernetes/gitlab_chart.md159
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md249
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md272
-rw-r--r--doc/install/kubernetes/index.md17
-rw-r--r--doc/install/kubernetes/preparation/connect.md30
-rw-r--r--doc/install/kubernetes/preparation/eks.md48
-rw-r--r--doc/install/kubernetes/preparation/networking.md41
-rw-r--r--doc/install/kubernetes/preparation/rbac.md23
-rw-r--r--doc/install/kubernetes/preparation/tiller.md110
-rw-r--r--doc/install/kubernetes/preparation/tools_installation.md22
-rw-r--r--doc/update/mysql_to_postgresql.md286
-rw-r--r--doc/user/discussions/img/multi-line-suggestion-preview.pngbin0 -> 61692 bytes
-rw-r--r--doc/user/discussions/img/multi-line-suggestion-syntax.pngbin0 -> 29753 bytes
-rw-r--r--doc/user/discussions/index.md18
-rw-r--r--doc/user/group/index.md11
-rw-r--r--doc/user/profile/account/two_factor_authentication.md13
-rw-r--r--doc/user/project/clusters/index.md2
-rw-r--r--doc/user/project/clusters/serverless/img/function-details-loaded.pngbin0 -> 93515 bytes
-rw-r--r--doc/user/project/clusters/serverless/index.md95
-rw-r--r--doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.pngbin6491 -> 19986 bytes
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md8
-rw-r--r--doc/user/project/repository/img/download_source_code.pngbin0 -> 61467 bytes
-rw-r--r--doc/user/project/repository/index.md20
-rw-r--r--doc/user/reserved_names.md1
-rw-r--r--jest.config.js11
-rw-r--r--lib/api/entities.rb9
-rw-r--r--lib/api/groups.rb1
-rw-r--r--lib/api/internal.rb3
-rw-r--r--lib/api/issues.rb4
-rw-r--r--lib/api/merge_requests.rb1
-rw-r--r--lib/api/settings.rb3
-rw-r--r--lib/api/users.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb12
-rw-r--r--lib/banzai/filter/blockquote_fence_filter.rb4
-rw-r--r--lib/banzai/filter/label_reference_filter.rb6
-rw-r--r--lib/banzai/suggestions_parser.rb16
-rw-r--r--lib/gitlab/access.rb21
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/config/entry/includes.rb2
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb4
-rw-r--r--lib/gitlab/ci/status/build/factory.rb3
-rw-r--r--lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb24
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml54
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml51
-rw-r--r--lib/gitlab/ci/templates/Serverless.gitlab-ci.yml13
-rw-r--r--lib/gitlab/config/entry/validators.rb24
-rw-r--r--lib/gitlab/danger/helper.rb5
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb19
-rw-r--r--lib/gitlab/etag_caching/router.rb23
-rw-r--r--lib/gitlab/git/repository.rb7
-rw-r--r--lib/gitlab/git/wiki.rb10
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/git_access_result/success.rb5
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb9
-rw-r--r--lib/gitlab/gl_repository.rb4
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb105
-rw-r--r--lib/gitlab/graphql/authorize/instrumentation.rb44
-rw-r--r--lib/gitlab/graphql/errors.rb1
-rw-r--r--lib/gitlab/graphql/query_analyzers/log_query_complexity.rb18
-rw-r--r--lib/gitlab/graphql/tracing.rb43
-rw-r--r--lib/gitlab/metrics/transaction.rb15
-rw-r--r--lib/gitlab/object_hierarchy.rb62
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb8
-rw-r--r--lib/gitlab/prometheus/queries/knative_invocation_query.rb39
-rw-r--r--lib/gitlab/prometheus_client.rb83
-rw-r--r--lib/gitlab/repo_path.rb5
-rw-r--r--lib/gitlab/untrusted_regexp.rb13
-rw-r--r--lib/gitlab/untrusted_regexp/ruby_syntax.rb39
-rw-r--r--lib/gitlab/url_helpers.rb16
-rw-r--r--lib/gitlab/workhorse.rb52
-rw-r--r--lib/tasks/gitlab/info.rake27
-rw-r--r--locale/ar_SA/gitlab.po3
-rw-r--r--locale/bg/gitlab.po3
-rw-r--r--locale/ca_ES/gitlab.po3
-rw-r--r--locale/cs_CZ/gitlab.po3
-rw-r--r--locale/cy_GB/gitlab.po3
-rw-r--r--locale/da_DK/gitlab.po3
-rw-r--r--locale/de/gitlab.po3
-rw-r--r--locale/el_GR/gitlab.po3
-rw-r--r--locale/en/gitlab.po3
-rw-r--r--locale/eo/gitlab.po3
-rw-r--r--locale/es/gitlab.po3
-rw-r--r--locale/et_EE/gitlab.po3
-rw-r--r--locale/fil_PH/gitlab.po3
-rw-r--r--locale/fr/gitlab.po3
-rw-r--r--locale/gitlab.pot304
-rw-r--r--locale/gl_ES/gitlab.po3
-rw-r--r--locale/he_IL/gitlab.po3
-rw-r--r--locale/hi_IN/gitlab.po3
-rw-r--r--locale/hr_HR/gitlab.po3
-rw-r--r--locale/hu_HU/gitlab.po3
-rw-r--r--locale/id_ID/gitlab.po3
-rw-r--r--locale/it/gitlab.po3
-rw-r--r--locale/ja/gitlab.po3
-rw-r--r--locale/ko/gitlab.po3
-rw-r--r--locale/mn_MN/gitlab.po3
-rw-r--r--locale/nb_NO/gitlab.po3
-rw-r--r--locale/nl_NL/gitlab.po3
-rw-r--r--locale/pa_IN/gitlab.po3
-rw-r--r--locale/pl_PL/gitlab.po3
-rw-r--r--locale/pt_BR/gitlab.po3
-rw-r--r--locale/pt_PT/gitlab.po3
-rw-r--r--locale/ro_RO/gitlab.po3
-rw-r--r--locale/ru/gitlab.po3
-rw-r--r--locale/sk_SK/gitlab.po3
-rw-r--r--locale/sq_AL/gitlab.po3
-rw-r--r--locale/sr_CS/gitlab.po3
-rw-r--r--locale/sr_SP/gitlab.po3
-rw-r--r--locale/sv_SE/gitlab.po3
-rw-r--r--locale/sw_KE/gitlab.po3
-rw-r--r--locale/tr_TR/gitlab.po3
-rw-r--r--locale/uk/gitlab.po10
-rw-r--r--locale/zh_CN/gitlab.po3
-rw-r--r--locale/zh_HK/gitlab.po3
-rw-r--r--locale/zh_TW/gitlab.po3
-rw-r--r--package.json13
-rw-r--r--qa/qa/page/file/show.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb2
-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/snippet/create_snippet_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb4
-rw-r--r--qa/qa/tools/generate_perf_testdata.rb67
-rw-r--r--spec/config/object_store_settings_spec.rb2
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb7
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb6
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb4
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/dashboard_controller_spec.rb33
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb2
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb23
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb2
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb16
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb4
-rw-r--r--spec/controllers/projects/environments/prometheus_api_controller_spec.rb152
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb36
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb4
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb9
-rw-r--r--spec/controllers/projects/services_controller_spec.rb6
-rw-r--r--spec/controllers/projects_controller_spec.rb14
-rw-r--r--spec/controllers/user_callouts_controller_spec.rb8
-rw-r--r--spec/factories/ci/builds.rb5
-rw-r--r--spec/factories/groups.rb1
-rw-r--r--spec/features/boards/boards_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb18
-rw-r--r--spec/features/commits/user_uses_quick_actions_spec.rb23
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb25
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb4
-rw-r--r--spec/features/explore/groups_list_spec.rb6
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/clusters/user_spec.rb2
-rw-r--r--spec/features/groups/group_settings_spec.rb8
-rw-r--r--spec/features/groups/settings/ci_cd_spec.rb6
-rw-r--r--spec/features/groups_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb6
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb6
-rw-r--r--spec/features/issues/form_spec.rb2
-rw-r--r--spec/features/issues/issue_detail_spec.rb2
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb58
-rw-r--r--spec/features/labels_hierarchy_spec.rb2
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb13
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb2
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb111
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb149
-rw-r--r--spec/features/project_variables_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb2
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/clusters/applications_spec.rb4
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb2
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb2
-rw-r--r--spec/features/projects/environments/environments_spec.rb12
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/members/invite_group_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb19
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb6
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb10
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb26
-rw-r--r--spec/features/projects/serverless/functions_spec.rb2
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb3
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb4
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/user_creates_project_spec.rb27
-rw-r--r--spec/features/projects/wiki/user_views_wiki_pages_spec.rb89
-rw-r--r--spec/features/raven_js_spec.rb4
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb2
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb6
-rw-r--r--spec/features/users/overview_spec.rb6
-rw-r--r--spec/finders/issues_finder_spec.rb89
-rw-r--r--spec/finders/merge_requests_finder_spec.rb8
-rw-r--r--spec/finders/milestones_finder_spec.rb2
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb32
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json8
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json32
-rw-r--r--spec/fixtures/api/schemas/variable.json2
-rw-r--r--spec/fixtures/blockquote_fence_after.md16
-rw-r--r--spec/fixtures/valid.po3
-rw-r--r--spec/frontend/helpers/monitor_helper_spec.js45
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js18
-rw-r--r--spec/frontend/labels_select_spec.js116
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap21
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart_container_spec.js64
-rw-r--r--spec/graphql/features/authorization_spec.rb274
-rw-r--r--spec/graphql/gitlab_schema_spec.rb42
-rw-r--r--spec/graphql/types/base_field_spec.rb19
-rw-r--r--spec/graphql/types/issue_type_spec.rb2
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb11
-rw-r--r--spec/graphql/types/milestone_type_spec.rb9
-rw-r--r--spec/graphql/types/project_type_spec.rb12
-rw-r--r--spec/graphql/types/query_type_spec.rb4
-rw-r--r--spec/graphql/types/user_type_spec.rb9
-rw-r--r--spec/helpers/icons_helper_spec.rb6
-rw-r--r--spec/helpers/labels_helper_spec.rb20
-rw-r--r--spec/helpers/namespaces_helper_spec.rb72
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/helpers/version_check_helper_spec.rb8
-rw-r--r--spec/helpers/wiki_helper_spec.rb52
-rw-r--r--spec/javascripts/boards/issue_spec.js2
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js19
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js20
-rw-r--r--spec/javascripts/diffs/components/app_spec.js18
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js20
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js4
-rw-r--r--spec/javascripts/fixtures/issues.rb58
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js62
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js25
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js84
-rw-r--r--spec/javascripts/ide/components/file_row_extra_spec.js2
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js4
-rw-r--r--spec/javascripts/ide/components/pipelines/list_spec.js31
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js75
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js17
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js33
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js20
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js62
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js106
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js61
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js125
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js36
-rw-r--r--spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js37
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js6
-rw-r--r--spec/javascripts/lib/utils/higlight_spec.js43
-rw-r--r--spec/javascripts/monitoring/charts/area_spec.js23
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js93
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js82
-rw-r--r--spec/javascripts/notes/mock_data.js6
-rw-r--r--spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js89
-rw-r--r--spec/javascripts/related_merge_requests/store/actions_spec.js110
-rw-r--r--spec/javascripts/related_merge_requests/store/mutations_spec.js49
-rw-r--r--spec/javascripts/serverless/components/area_spec.js121
-rw-r--r--spec/javascripts/serverless/components/environment_row_spec.js41
-rw-r--r--spec/javascripts/serverless/components/function_details_spec.js113
-rw-r--r--spec/javascripts/serverless/components/function_row_spec.js13
-rw-r--r--spec/javascripts/serverless/components/functions_spec.js113
-rw-r--r--spec/javascripts/serverless/components/missing_prometheus_spec.js37
-rw-r--r--spec/javascripts/serverless/components/pod_box_spec.js22
-rw-r--r--spec/javascripts/serverless/components/url_spec.js20
-rw-r--r--spec/javascripts/serverless/mock_data.js57
-rw-r--r--spec/javascripts/serverless/store/actions_spec.js88
-rw-r--r--spec/javascripts/serverless/store/getters_spec.js43
-rw-r--r--spec/javascripts/serverless/store/mutations_spec.js86
-rw-r--r--spec/javascripts/serverless/stores/serverless_store_spec.js36
-rw-r--r--spec/javascripts/serverless/utils.js20
-rw-r--r--spec/javascripts/sidebar/todo_spec.js6
-rw-r--r--spec/javascripts/test_bundle.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js60
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js66
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestions_spec.js109
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js110
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js132
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js15
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js40
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js7
-rw-r--r--spec/lib/api/helpers/custom_validators_spec.rb6
-rw-r--r--spec/lib/banzai/filter/blockquote_fence_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb6
-rw-r--r--spec/lib/banzai/suggestions_parser_spec.rb32
-rw-r--r--spec/lib/forever_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/file/local_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/external/file/template_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/templates/templates_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb8
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb4
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/suggestion_spec.rb87
-rw-r--r--spec/lib/gitlab/diff/suggestions_parser_spec.rb61
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb18
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb14
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb8
-rw-r--r--spec/lib/gitlab/git/gitmodules_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb33
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb106
-rw-r--r--spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb67
-rw-r--r--spec/lib/gitlab/graphql/tracing_spec.rb33
-rw-r--r--spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/config_map_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/certificate_spec.rb6
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb20
-rw-r--r--spec/lib/gitlab/kubernetes/role_binding_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/service_account_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/service_account_token_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb229
-rw-r--r--spec/lib/gitlab/object_hierarchy_spec.rb40
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb2
-rw-r--r--spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb30
-rw-r--r--spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb26
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb107
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb6
-rw-r--r--spec/lib/gitlab/search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb6
-rw-r--r--spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb6
-rw-r--r--spec/lib/gitlab/tracing_spec.rb6
-rw-r--r--spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb68
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb41
-rw-r--r--spec/mailers/notify_spec.rb13
-rw-r--r--spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb34
-rw-r--r--spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb6
-rw-r--r--spec/migrations/schedule_runners_token_encryption_spec.rb2
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_spec.rb2
-rw-r--r--spec/migrations/truncate_user_fullname_spec.rb21
-rw-r--r--spec/models/application_setting_spec.rb9
-rw-r--r--spec/models/badge_spec.rb2
-rw-r--r--spec/models/badges/project_badge_spec.rb2
-rw-r--r--spec/models/ci/build_runner_session_spec.rb16
-rw-r--r--spec/models/ci/build_spec.rb47
-rw-r--r--spec/models/ci/runner_spec.rb2
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb8
-rw-r--r--spec/models/clusters/applications/helm_spec.rb4
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb6
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb6
-rw-r--r--spec/models/clusters/applications/knative_spec.rb22
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb16
-rw-r--r--spec/models/clusters/applications/runner_spec.rb8
-rw-r--r--spec/models/clusters/cluster_spec.rb8
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb6
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb4
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/concerns/prometheus_adapter_spec.rb42
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb44
-rw-r--r--spec/models/concerns/spammable_spec.rb4
-rw-r--r--spec/models/deploy_token_spec.rb32
-rw-r--r--spec/models/environment_spec.rb23
-rw-r--r--spec/models/group_spec.rb38
-rw-r--r--spec/models/namespace_spec.rb8
-rw-r--r--spec/models/network/graph_spec.rb2
-rw-r--r--spec/models/project_auto_devops_spec.rb16
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb24
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb18
-rw-r--r--spec/models/remote_mirror_spec.rb4
-rw-r--r--spec/models/repository_spec.rb24
-rw-r--r--spec/models/serverless/function_spec.rb21
-rw-r--r--spec/models/service_spec.rb4
-rw-r--r--spec/models/suggestion_spec.rb16
-rw-r--r--spec/models/user_spec.rb5
-rw-r--r--spec/models/wiki_page_spec.rb45
-rw-r--r--spec/policies/group_member_policy_spec.rb105
-rw-r--r--spec/policies/group_policy_spec.rb114
-rw-r--r--spec/presenters/ci/bridge_presenter_spec.rb15
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb12
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb16
-rw-r--r--spec/requests/api/internal_spec.rb50
-rw-r--r--spec/requests/api/issues_spec.rb12
-rw-r--r--spec/requests/api/merge_requests_spec.rb15
-rw-r--r--spec/requests/api/pipelines_spec.rb15
-rw-r--r--spec/requests/api/project_clusters_spec.rb62
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/serializers/analytics_stage_serializer_spec.rb2
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb2
-rw-r--r--spec/serializers/build_details_entity_spec.rb10
-rw-r--r--spec/serializers/environment_entity_spec.rb4
-rw-r--r--spec/serializers/job_entity_spec.rb18
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb2
-rw-r--r--spec/serializers/suggestion_entity_spec.rb3
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb22
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb72
-rw-r--r--spec/services/clusters/applications/patch_service_spec.rb70
-rw-r--r--spec/services/clusters/applications/upgrade_service_spec.rb70
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb2
-rw-r--r--spec/services/deploy_tokens/create_service_spec.rb10
-rw-r--r--spec/services/groups/transfer_service_spec.rb82
-rw-r--r--spec/services/issues/build_service_spec.rb4
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb32
-rw-r--r--spec/services/merge_requests/create_service_spec.rb13
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb17
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb21
-rw-r--r--spec/services/notes/build_service_spec.rb36
-rw-r--r--spec/services/notes/create_service_spec.rb41
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb1
-rw-r--r--spec/services/preview_markdown_service_spec.rb73
-rw-r--r--spec/services/projects/auto_devops/disable_service_spec.rb10
-rw-r--r--spec/services/projects/participants_service_spec.rb4
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb195
-rw-r--r--spec/services/suggestions/apply_service_spec.rb64
-rw-r--r--spec/services/task_list_toggle_service_spec.rb21
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb4
-rw-r--r--spec/support/features/variable_list_shared_examples.rb123
-rw-r--r--spec/support/helpers/graphql_helpers.rb8
-rw-r--r--spec/support/helpers/prometheus_helpers.rb14
-rw-r--r--spec/support/redis/redis_shared_examples.rb2
-rw-r--r--spec/support/shared_context/policies/project_policy_shared_context.rb1
-rw-r--r--spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb2
-rw-r--r--spec/support/shared_examples/issuables_list_metadata_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb4
-rw-r--r--spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb77
-rw-r--r--spec/support/shared_examples/services/base_helm_service_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/snippet_visibility_shared_examples.rb12
-rw-r--r--spec/uploaders/records_uploads_spec.rb13
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb2
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb4
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb10
-rw-r--r--spec/views/shared/milestones/_issuables.html.haml.rb6
-rw-r--r--spec/views/shared/projects/_project.html.haml_spec.rb4
-rw-r--r--yarn.lock113
845 files changed, 13013 insertions, 4998 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f89a52e7a3e..98fdda3593e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -852,8 +852,6 @@ qa:selectors:
.qa-frontend-node: &qa-frontend-node
<<: *dedicated-no-docs-no-db-pull-cache-job
stage: test
- variables:
- NODE_OPTIONS: --max_old_space_size=3584
cache:
key: "$CI_JOB_NAME"
paths:
@@ -1143,3 +1141,4 @@ schedule:review-performance:
<<: *review-schedules-only
script:
- wait_for_job_to_be_done "schedule:review-deploy"
+
diff --git a/.gitlab/CODEOWNERS.disabled b/.gitlab/CODEOWNERS.disabled
index b9f886c1d47..89a9696d3e8 100644
--- a/.gitlab/CODEOWNERS.disabled
+++ b/.gitlab/CODEOWNERS.disabled
@@ -1,6 +1,6 @@
# Backend Maintainers are the default for all ruby files
-*.rb @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @nick.thomas @rspeicher @rymai @smcgivern
-*.rake @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @nick.thomas @rspeicher @rymai @smcgivern
+*.rb @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern
+*.rake @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index b4007c1ba7b..8a49715e0e8 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -24,6 +24,10 @@ Personas can be found at https://about.gitlab.com/handbook/marketing/product-mar
<!-- See the Feature Change Documentation Workflow https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html
Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#documentation-requirements -->
+### Testing
+
+<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/ -->
+
### What does success look like, and how can we measure that?
<!-- Define both the success metrics and acceptance criteria. Note that success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this. -->
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc8123a5888..c99e487d04b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,32 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.9.6 (2019-04-04)
+
+### Fixed (3 changes)
+
+- Force to recreate all MR diffs on import. !26480
+- Fix API /project/:id/branches not returning correct merge status. !26785
+- Avoid excessive recursive calls with Rugged TreeEntries. !26813
+
+### Performance (1 change)
+
+- Force a full GC after importing a project. !26803
+
+
+## 11.9.5 (2019-04-03)
+
+### Fixed (3 changes)
+
+- Force to recreate all MR diffs on import. !26480
+- Fix API /project/:id/branches not returning correct merge status. !26785
+- Avoid excessive recursive calls with Rugged TreeEntries. !26813
+
+### Performance (1 change)
+
+- Force a full GC after importing a project. !26803
+
+
## 11.9.3 (2019-03-27)
### Security (8 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 97dbe2f512b..82e16b4fbf4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,7 +18,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
## Contributing Documentation has been moved
As of July 2018, all the documentation for contributing to the GitLab project has been moved to a new location.
-[view the new documentation](doc/development/contributing/index.md) to find the latest information.
+[View the new documentation](doc/development/contributing/index.md) to find the latest information.
## Contribute to GitLab
diff --git a/Dangerfile b/Dangerfile
index 95dd48aae9e..3e8cb456003 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,16 +1,19 @@
danger.import_plugin('danger/plugins/helper.rb')
-danger.import_dangerfile(path: 'danger/metadata')
-danger.import_dangerfile(path: 'danger/changes_size')
-danger.import_dangerfile(path: 'danger/changelog')
-danger.import_dangerfile(path: 'danger/specs')
-danger.import_dangerfile(path: 'danger/gemfile')
-danger.import_dangerfile(path: 'danger/database')
-danger.import_dangerfile(path: 'danger/documentation')
-danger.import_dangerfile(path: 'danger/frozen_string')
-danger.import_dangerfile(path: 'danger/commit_messages')
-danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
-danger.import_dangerfile(path: 'danger/prettier')
-danger.import_dangerfile(path: 'danger/eslint')
-danger.import_dangerfile(path: 'danger/roulette')
-danger.import_dangerfile(path: 'danger/single_codebase')
-danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
+
+unless helper.release_automation?
+ danger.import_dangerfile(path: 'danger/metadata')
+ danger.import_dangerfile(path: 'danger/changes_size')
+ danger.import_dangerfile(path: 'danger/changelog')
+ danger.import_dangerfile(path: 'danger/specs')
+ danger.import_dangerfile(path: 'danger/gemfile')
+ danger.import_dangerfile(path: 'danger/database')
+ danger.import_dangerfile(path: 'danger/documentation')
+ danger.import_dangerfile(path: 'danger/frozen_string')
+ danger.import_dangerfile(path: 'danger/commit_messages')
+ danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
+ danger.import_dangerfile(path: 'danger/prettier')
+ danger.import_dangerfile(path: 'danger/eslint')
+ danger.import_dangerfile(path: 'danger/roulette')
+ danger.import_dangerfile(path: 'danger/single_codebase')
+ danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
+end
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 034552a83ee..7aa332e4163 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.30.0
+1.33.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index d139a75408e..f7ee06693c1 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-8.7.1
+9.0.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index d127a0ff9f1..6d2890793d4 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.3.3
+8.5.0
diff --git a/Gemfile b/Gemfile
index c0815c4a2a6..6052018754a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-gem 'rails', '5.0.7.1'
+gem 'rails', '5.0.7.2'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Improves copy-on-write performance for MRI
@@ -139,10 +139,7 @@ gem 'icalendar'
gem 'diffy', '~> 3.1.0'
# Application server
-# The 2.0.6 version of rack requires monkeypatch to be present in
-# `config.ru`. This can be removed once a new update for Rack
-# is available that contains https://github.com/rack/rack/pull/1201.
-gem 'rack', '2.0.6'
+gem 'rack', '~> 2.0.7'
group :unicorn do
gem 'unicorn', '~> 5.4.1'
@@ -419,7 +416,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.13.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.19.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4ebcc6c81b2..b522aa85b39 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,41 +4,41 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
- actioncable (5.0.7.1)
- actionpack (= 5.0.7.1)
+ actioncable (5.0.7.2)
+ actionpack (= 5.0.7.2)
nio4r (>= 1.2, < 3.0)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.7.1)
- actionpack (= 5.0.7.1)
- actionview (= 5.0.7.1)
- activejob (= 5.0.7.1)
+ actionmailer (5.0.7.2)
+ actionpack (= 5.0.7.2)
+ actionview (= 5.0.7.2)
+ activejob (= 5.0.7.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.0.7.1)
- actionview (= 5.0.7.1)
- activesupport (= 5.0.7.1)
+ actionpack (5.0.7.2)
+ actionview (= 5.0.7.2)
+ activesupport (= 5.0.7.2)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.7.1)
- activesupport (= 5.0.7.1)
+ actionview (5.0.7.2)
+ activesupport (= 5.0.7.2)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.0.7.1)
- activesupport (= 5.0.7.1)
+ activejob (5.0.7.2)
+ activesupport (= 5.0.7.2)
globalid (>= 0.3.6)
- activemodel (5.0.7.1)
- activesupport (= 5.0.7.1)
- activerecord (5.0.7.1)
- activemodel (= 5.0.7.1)
- activesupport (= 5.0.7.1)
+ activemodel (5.0.7.2)
+ activesupport (= 5.0.7.2)
+ activerecord (5.0.7.2)
+ activemodel (= 5.0.7.2)
+ activesupport (= 5.0.7.2)
arel (~> 7.0)
activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6)
- activesupport (5.0.7.1)
+ activesupport (5.0.7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -281,7 +281,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.13.0)
+ gitaly-proto (1.19.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -298,7 +298,7 @@ GEM
omniauth (~> 1.3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
rubyntlm (~> 0.5)
- globalid (0.4.1)
+ globalid (0.4.2)
activesupport (>= 4.2.0)
gon (6.2.0)
actionpack (>= 3.0)
@@ -385,7 +385,7 @@ GEM
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (1.2.0)
+ i18n (1.6.0)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2)
@@ -617,7 +617,7 @@ GEM
puma (>= 2.7, < 4)
pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6)
- rack (2.0.6)
+ rack (2.0.7)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -635,17 +635,17 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (5.0.7.1)
- actioncable (= 5.0.7.1)
- actionmailer (= 5.0.7.1)
- actionpack (= 5.0.7.1)
- actionview (= 5.0.7.1)
- activejob (= 5.0.7.1)
- activemodel (= 5.0.7.1)
- activerecord (= 5.0.7.1)
- activesupport (= 5.0.7.1)
+ rails (5.0.7.2)
+ actioncable (= 5.0.7.2)
+ actionmailer (= 5.0.7.2)
+ actionpack (= 5.0.7.2)
+ actionview (= 5.0.7.2)
+ activejob (= 5.0.7.2)
+ activemodel (= 5.0.7.2)
+ activerecord (= 5.0.7.2)
+ activesupport (= 5.0.7.2)
bundler (>= 1.3.0)
- railties (= 5.0.7.1)
+ railties (= 5.0.7.2)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -661,9 +661,9 @@ GEM
rails-i18n (5.1.1)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
- railties (5.0.7.1)
- actionpack (= 5.0.7.1)
- activesupport (= 5.0.7.1)
+ railties (5.0.7.2)
+ actionpack (= 5.0.7.2)
+ activesupport (= 5.0.7.2)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -1017,7 +1017,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.13.0)
+ gitaly-proto (~> 1.19.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.7.0)
@@ -1100,12 +1100,12 @@ DEPENDENCIES
pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
- rack (= 2.0.6)
+ rack (~> 2.0.7)
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
- rails (= 5.0.7.1)
+ rails (= 5.0.7.2)
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1)
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/avatar_picker.js
index dcda625f587..d38e0b4abaa 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/avatar_picker.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
-export default function groupAvatar() {
- $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() {
+export default function initAvatarPicker() {
+ $('.js-choose-avatar-button').on('click', function onClickAvatar() {
const form = $(this).closest('form');
- return form.find('.js-group-avatar-input').click();
+ return form.find('.js-avatar-input').click();
});
- $('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
+
+ $('.js-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form');
const filename = $(this)
.val()
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 10577da9305..a5ed695af35 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -8,7 +8,11 @@ import boardsStore from '../stores/boards_store';
$(document)
.off('created.label')
- .on('created.label', (e, label) => {
+ .on('created.label', (e, label, addNewList) => {
+ if (!addNewList) {
+ return;
+ }
+
boardsStore.new({
title: label.title,
position: boardsStore.state.lists.length - 2,
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index dd92d3c8552..2edb6723ada 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -119,7 +119,17 @@ class ListIssue {
}
const projectPath = this.project ? this.project.path : '';
- return Vue.http.patch(`${this.path}.json`, data);
+ return Vue.http.patch(`${this.path}.json`, data).then(({ body = {} } = {}) => {
+ /**
+ * Since post implementation of Scoped labels, server can reject
+ * same key-ed labels. To keep the UI and server Model consistent,
+ * we're just assigning labels that server echo's back to us when we
+ * PATCH the said object.
+ */
+ if (body) {
+ this.labels = body.labels;
+ }
+ });
}
}
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 7951348d8b2..4d5d6bb864b 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -14,6 +14,9 @@ const BreakpointInstance = {
return breakpoint;
},
+ isDesktop() {
+ return ['lg', 'md'].includes(this.getBreakpointSize);
+ },
};
export default BreakpointInstance;
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 5b20fa141cd..da3100b9386 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -40,6 +40,12 @@ export default class VariableList {
// converted. we need the value as a string.
default: $('.js-ci-variable-input-protected').attr('data-default'),
},
+ masked: {
+ selector: '.js-ci-variable-input-masked',
+ // use `attr` instead of `data` as we don't want the value to be
+ // converted. we need the value as a string.
+ default: $('.js-ci-variable-input-masked').attr('data-default'),
+ },
environment_scope: {
// We can't use a `.js-` class here because
// gl_dropdown replaces the <input> and doesn't copy over the class
@@ -88,13 +94,16 @@ export default class VariableList {
}
});
- // Always make sure there is an empty last row
- this.$container.on('input trigger-change', inputSelector, () => {
+ this.$container.on('input trigger-change', inputSelector, e => {
+ // Always make sure there is an empty last row
const $lastRow = this.$container.find('.js-row').last();
if (this.checkIfRowTouched($lastRow)) {
this.insertRow($lastRow);
}
+
+ // If masked, validate value against regex
+ this.validateMaskability($(e.currentTarget).closest('.js-row'));
});
}
@@ -171,12 +180,33 @@ export default class VariableList {
checkIfRowTouched($row) {
return Object.keys(this.inputMap).some(name => {
+ // Row should not qualify as touched if only switches have been touched
+ if (['protected', 'masked'].includes(name)) return false;
+
const entry = this.inputMap[name];
const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default;
});
}
+ validateMaskability($row) {
+ const invalidInputClass = 'gl-field-error-outline';
+
+ const maskableRegex = /^\w{8,}$/; // Eight or more alphanumeric characters plus underscores
+ const variableValue = $row.find(this.inputMap.secret_value.selector).val();
+ const isValueMaskable = maskableRegex.test(variableValue) || variableValue === '';
+ const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
+
+ // Show a validation error if the user wants to mask an unmaskable variable value
+ $row
+ .find(this.inputMap.secret_value.selector)
+ .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
+ $row
+ .find('.js-secret-value-placeholder')
+ .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
+ $row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
+ }
+
toggleEnableRow(isEnabled = true) {
this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index c95d7608e37..a88e4b7b314 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -288,10 +288,11 @@ export default class Clusters {
}
toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
- const helpTextHidden = ingressNewState.status !== APPLICATION_STATUS.INSTALLED;
- const domainSnippetText = `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`;
+ const { externalIp, status } = ingressNewState;
+ const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp;
+ const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`;
- if (ingressPreviousState.status !== ingressNewState.status) {
+ if (ingressPreviousState.status !== status) {
this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden);
this.ingressDomainSnippet.textContent = domainSnippetText;
}
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 67fcdd082a2..03dea1ec0a5 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -41,6 +41,9 @@ export default class ContextualSidebar {
this.toggleCollapsedSidebar(value, true);
}
});
+ this.$page.on('transitionstart transitionend', () => {
+ $(document).trigger('content.resize');
+ });
$(window).on('resize', () => _.debounce(this.render(), 100));
}
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 28ca7d97314..eac0e37bcaa 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -14,6 +14,7 @@ export default class CreateLabelDropdown {
this.$newLabelField = $('#new_label_name', this.$el);
this.$newColorField = $('#new_label_color', this.$el);
this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+ this.$addList = $('.js-add-list', this.$el);
this.$newLabelError = $('.js-label-error', this.$el);
this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
@@ -21,6 +22,8 @@ export default class CreateLabelDropdown {
this.$newLabelError.hide();
this.$newLabelCreateButton.disable();
+ this.addListDefault = this.$addList.is(':checked');
+
this.cleanBinding();
this.addBinding();
}
@@ -83,6 +86,8 @@ export default class CreateLabelDropdown {
this.$newColorField.val('').trigger('change');
+ this.$addList.prop('checked', this.addListDefault);
+
this.$colorPreview
.css('background-color', '')
.parent()
@@ -116,9 +121,9 @@ export default class CreateLabelDropdown {
this.$newLabelError.html(errors).show();
} else {
+ const addNewList = this.$addList.is(':checked');
this.$dropdownBack.trigger('click');
-
- $(document).trigger('created.label', label);
+ $(document).trigger('created.label', [label, addNewList]);
}
},
);
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index e8f8c09152a..5e74998579b 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -20,6 +20,7 @@ import {
MAX_TREE_WIDTH,
TREE_HIDE_STATS_WIDTH,
MR_TREE_SHOW_KEY,
+ CENTERED_LIMITED_CONTAINER_CLASSES,
} from '../constants';
export default {
@@ -114,6 +115,9 @@ export default {
hideFileStats() {
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
},
+ isLimitedContainer() {
+ return !this.showTreeList && !this.isParallelView;
+ },
},
watch: {
diffViewType() {
@@ -148,6 +152,7 @@ export default {
this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
eventHub.$once('fetchDiffData', this.fetchData);
+ this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
},
beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData);
@@ -202,8 +207,6 @@ export default {
adjustView() {
if (this.shouldShow) {
this.$nextTick(() => {
- window.mrTabs.resetViewContainer();
- window.mrTabs.expandViewContainer(this.showTreeList);
this.setEventListeners();
});
} else {
@@ -256,6 +259,7 @@ export default {
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:target-branch="targetBranch"
+ :is-limited-container="isLimitedContainer"
/>
<hidden-files-warning
@@ -285,7 +289,12 @@ export default {
/>
<tree-list :hide-file-stats="hideFileStats" />
</div>
- <div class="diff-files-holder">
+ <div
+ class="diff-files-holder"
+ :class="{
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
+ }"
+ >
<commit-widget v-if="commit" :commit="commit" />
<template v-if="renderDiffFiles">
<diff-file
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index fe49dfff10b..363ebad1594 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -7,6 +7,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue';
+import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
@@ -35,6 +36,11 @@ export default {
required: false,
default: null,
},
+ isLimitedContainer: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
@@ -62,6 +68,9 @@ export default {
return this.mergeRequestDiff.base_version_path;
},
},
+ created() {
+ this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
+ },
mounted() {
polyfillSticky(this.$el);
},
@@ -77,8 +86,13 @@ export default {
</script>
<template>
- <div class="mr-version-controls" :class="{ 'is-fileTreeOpen': showTreeList }">
- <div class="mr-version-menus-container content-block">
+ <div class="mr-version-controls border-top border-bottom">
+ <div
+ class="mr-version-menus-container content-block"
+ :class="{
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
+ }"
+ >
<button
v-gl-tooltip.hover
type="button"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index fda7b7c5fd9..32e5fa5bf8b 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
-import { polyfillSticky } from '~/lib/utils/sticky';
+import { polyfillSticky, stickyMonitor } from '~/lib/utils/sticky';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -11,7 +11,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue';
-import { scrollToElement } from '~/lib/utils/common_utils';
+import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
export default {
components: {
@@ -137,9 +137,20 @@ export default {
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
},
+ showExpandDiffToFullFileEnabled() {
+ return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded;
+ },
+ expandDiffToFullFileTitle() {
+ if (this.diffFile.isShowingFullFile) {
+ return s__('MRDiff|Show changes only');
+ }
+ return s__('MRDiff|Show full file');
+ },
},
mounted() {
polyfillSticky(this.$refs.header);
+ const fileHeaderHeight = this.$refs.header.clientHeight;
+ stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
},
methods: {
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
@@ -243,70 +254,70 @@ export default {
class="file-actions d-none d-sm-block"
>
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
- <template v-if="diffFile.blob && diffFile.blob.readable_text">
- <button
- :disabled="!diffHasDiscussions(diffFile)"
- :class="{ active: hasExpandedDiscussions }"
- :title="s__('MergeRequests|Toggle comments for this file')"
- class="js-btn-vue-toggle-comments btn"
- type="button"
- @click="handleToggleDiscussions"
- >
- <icon name="comment" />
- </button>
-
- <edit-button
- v-if="!diffFile.deleted_file"
- :can-current-user-fork="canCurrentUserFork"
- :edit-path="diffFile.edit_path"
- :can-modify-blob="diffFile.can_modify_blob"
- @showForkMessage="showForkMessage"
- />
- </template>
+ <div class="btn-group" role="group">
+ <template v-if="diffFile.blob && diffFile.blob.readable_text">
+ <button
+ :disabled="!diffHasDiscussions(diffFile)"
+ :class="{ active: hasExpandedDiscussions }"
+ :title="s__('MergeRequests|Toggle comments for this file')"
+ class="js-btn-vue-toggle-comments btn"
+ type="button"
+ @click="handleToggleDiscussions"
+ >
+ <icon name="comment" />
+ </button>
- <a
- v-if="diffFile.replaced_view_path"
- :href="diffFile.replaced_view_path"
- class="btn view-file js-view-replaced-file"
- v-html="viewReplacedFileButtonText"
- >
- </a>
- <gl-tooltip :target="() => $refs.viewButton" placement="bottom">
- <span v-html="viewFileButtonText"></span>
- </gl-tooltip>
- <gl-button
- ref="viewButton"
- :href="diffFile.view_path"
- target="blank"
- class="view-file js-view-file-button"
- >
- <icon name="external-link" />
- </gl-button>
- <gl-button
- v-if="!diffFile.is_fully_expanded"
- class="expand-file js-expand-file"
- @click="toggleFullDiff(diffFile.file_path)"
- >
- <template v-if="diffFile.isShowingFullFile">
- {{ s__('MRDiff|Show changes only') }}
- </template>
- <template v-else>
- {{ s__('MRDiff|Show full file') }}
+ <edit-button
+ v-if="!diffFile.deleted_file"
+ :can-current-user-fork="canCurrentUserFork"
+ :edit-path="diffFile.edit_path"
+ :can-modify-blob="diffFile.can_modify_blob"
+ @showForkMessage="showForkMessage"
+ />
</template>
- <gl-loading-icon v-if="diffFile.isLoadingFullFile" inline />
- </gl-button>
- <a
- v-if="diffFile.external_url"
- v-gl-tooltip.hover
- :href="diffFile.external_url"
- :title="`View on ${diffFile.formatted_external_url}`"
- target="_blank"
- rel="noopener noreferrer"
- class="btn btn-file-option js-external-url"
- >
- <icon name="external-link" />
- </a>
+ <a
+ v-if="diffFile.replaced_view_path"
+ :href="diffFile.replaced_view_path"
+ class="btn view-file js-view-replaced-file"
+ v-html="viewReplacedFileButtonText"
+ >
+ </a>
+ <gl-button
+ v-if="!diffFile.is_fully_expanded"
+ ref="expandDiffToFullFileButton"
+ v-gl-tooltip.hover
+ :title="expandDiffToFullFileTitle"
+ class="expand-file js-expand-file"
+ @click="toggleFullDiff(diffFile.file_path)"
+ >
+ <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline />
+ <icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" />
+ <icon v-else name="doc-expand" />
+ </gl-button>
+ <gl-button
+ ref="viewButton"
+ v-gl-tooltip.hover
+ :href="diffFile.view_path"
+ target="blank"
+ class="view-file js-view-file-button"
+ :title="viewFileButtonText"
+ >
+ <icon name="external-link" />
+ </gl-button>
+
+ <a
+ v-if="diffFile.external_url"
+ v-gl-tooltip.hover
+ :href="diffFile.external_url"
+ :title="`View on ${diffFile.formatted_external_url}`"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="btn btn-file-option js-external-url"
+ >
+ <icon name="external-link" />
+ </a>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index bb66ab36283..41670b45798 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -48,10 +48,13 @@ export default {
noteableType: this.noteableType,
noteTargetLine: this.noteTargetLine,
diffViewType: this.diffViewType,
- diffFile: this.getDiffFileByHash(this.diffFileHash),
+ diffFile: this.diffFile,
linePosition: this.linePosition,
};
},
+ diffFile() {
+ return this.getDiffFileByHash(this.diffFileHash);
+ },
},
mounted() {
if (this.isLoggedIn) {
@@ -102,6 +105,7 @@ export default {
:line-code="line.line_code"
:line="line"
:help-page-path="helpPagePath"
+ :diff-file="diffFile"
save-button-title="Comment"
class="diff-comment-form"
@handleFormUpdateAddToReview="addToReview"
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 6f380fe6ece..5dabe224baa 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -47,3 +47,6 @@ export const OLD_LINE_KEY = 'old_line';
export const NEW_LINE_KEY = 'new_line';
export const TYPE_KEY = 'type';
export const LEFT_LINE_KEY = 'left';
+
+export const CENTERED_LIMITED_CONTAINER_CLASSES =
+ 'container-limited limit-container-width mx-auto px-3';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 0c2e87521d9..efd03ec952f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,6 +1,7 @@
import _ from 'underscore';
import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
@@ -36,10 +37,11 @@ export default class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = filteredSearchTokenKeys;
this.stateFiltersSelector = stateFiltersSelector;
- this.recentsStorageKeyNames = {
- issues: 'issue-recent-searches',
- merge_requests: 'merge-request-recent-searches',
- };
+
+ const { multipleAssignees } = this.filteredSearchInput.dataset;
+ if (multipleAssignees && this.filteredSearchTokenKeys.enableMultipleAssignees) {
+ this.filteredSearchTokenKeys.enableMultipleAssignees();
+ }
this.recentSearchesStore = new RecentSearchesStore({
isLocalStorageAvailable: RecentSearchesService.isAvailable(),
@@ -51,7 +53,7 @@ export default class FilteredSearchManager {
const fullPath = this.searchHistoryDropdownElement
? this.searchHistoryDropdownElement.dataset.fullPath
: 'project';
- const recentSearchesKey = `${fullPath}-${this.recentsStorageKeyNames[this.page]}`;
+ const recentSearchesKey = `${fullPath}-${recentSearchesStorageKeys[this.page]}`;
this.recentSearchesService = new RecentSearchesService(recentSearchesKey);
}
diff --git a/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
new file mode 100644
index 00000000000..7e9b809e9b2
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/recent_searches_storage_keys.js
@@ -0,0 +1,4 @@
+export default {
+ issues: 'issue-recent-searches',
+ merge_requests: 'merge-request-recent-searches',
+};
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 42d14b65b3a..92c3bcb5012 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,6 +1,9 @@
<script>
/* eslint-disable vue/require-default-prop */
-import Identicon from '../../vue_shared/components/identicon.vue';
+import _ from 'underscore';
+import Identicon from '~/vue_shared/components/identicon.vue';
+import highlight from '~/lib/utils/highlight';
+import { truncateNamespace } from '~/lib/utils/text_utility';
export default {
components: {
@@ -36,43 +39,13 @@ export default {
},
computed: {
hasAvatar() {
- return this.avatarUrl !== null;
+ return _.isString(this.avatarUrl) && !_.isEmpty(this.avatarUrl);
},
- highlightedItemName() {
- if (this.matcher) {
- const matcherRegEx = new RegExp(this.matcher, 'gi');
- const matches = this.itemName.match(matcherRegEx);
-
- if (matches && matches.length > 0) {
- return this.itemName.replace(matches[0], `<b>${matches[0]}</b>`);
- }
- }
- return this.itemName;
- },
- /**
- * Smartly truncates item namespace by doing two things;
- * 1. Only include Group names in path by removing item name
- * 2. Only include first and last group names in the path
- * when namespace has more than 2 groups present
- *
- * First part (removal of item name from namespace) can be
- * done from backend but doing so involves migration of
- * existing item namespaces which is not wise thing to do.
- */
truncatedNamespace() {
- if (!this.namespace) {
- return null;
- }
- const namespaceArr = this.namespace.split(' / ');
-
- namespaceArr.splice(-1, 1);
- let namespace = namespaceArr.join(' / ');
-
- if (namespaceArr.length > 2) {
- namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
- }
-
- return namespace;
+ return truncateNamespace(this.namespace);
+ },
+ highlightedItemName() {
+ return highlight(this.itemName, this.matcher);
},
},
};
@@ -92,8 +65,16 @@ export default {
/>
</div>
<div class="frequent-items-item-metadata-container">
- <div :title="itemName" class="frequent-items-item-title" v-html="highlightedItemName"></div>
- <div v-if="truncatedNamespace" :title="namespace" class="frequent-items-item-namespace">
+ <div
+ :title="itemName"
+ class="frequent-items-item-title js-frequent-items-item-title"
+ v-html="highlightedItemName"
+ ></div>
+ <div
+ v-if="namespace"
+ :title="namespace"
+ class="frequent-items-item-namespace js-frequent-items-item-namespace"
+ >
{{ truncatedNamespace }}
</div>
</div>
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 27d8669b256..1c6b18c0e03 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -561,10 +561,9 @@ GitLabDropdown = (function() {
!$target.data('isLink')
) {
e.stopPropagation();
- return false;
- } else {
- return true;
}
+
+ return true;
}
};
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index d5d5954ce6a..c4fd719c8d0 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -15,7 +15,7 @@ export default class GlFieldErrors {
initValidators() {
// register selectors here as needed
- const validateSelectors = [':text', ':password', '[type=email]']
+ const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]']
.map(selector => `input${selector}`)
.join(',');
diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js
new file mode 100644
index 00000000000..2c2a04d5b5e
--- /dev/null
+++ b/app/assets/javascripts/helpers/monitor_helper.js
@@ -0,0 +1,17 @@
+/* eslint-disable import/prefer-default-export */
+
+export const makeDataSeries = (queryResults, defaultConfig) =>
+ queryResults.reduce((acc, result) => {
+ const data = result.values.filter(([, value]) => !Number.isNaN(value));
+ if (!data.length) {
+ return acc;
+ }
+ const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_');
+ const name = result.metric[relevantMetric];
+ const series = { data };
+ if (name) {
+ series.name = `${defaultConfig.name}: ${name}`;
+ }
+
+ return acc.concat({ ...defaultConfig, ...series });
+ }, []);
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index d360dc42cd3..1824a0f6147 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,17 +1,23 @@
<script>
import _ from 'underscore';
-import { mapActions, mapState, mapGetters } from 'vuex';
+import { mapActions, mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, __ } from '~/locale';
-import * as consts from '../../stores/modules/commit/constants';
+import consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
+const { mapState: mapCommitState, mapGetters: mapCommitGetters } = createNamespacedHelpers(
+ 'commit',
+);
+
export default {
components: {
RadioGroup,
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
- ...mapGetters(['currentProject', 'currentBranch']),
+ ...mapCommitState(['commitAction', 'shouldCreateMR', 'shouldDisableNewMrOption']),
+ ...mapGetters(['currentProject', 'currentBranch', 'currentMergeRequest']),
+ ...mapCommitGetters(['shouldDisableNewMrOption']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -32,7 +38,7 @@ export default {
this.updateSelectedCommitAction();
},
methods: {
- ...mapActions('commit', ['updateCommitAction']),
+ ...mapActions('commit', ['updateCommitAction', 'toggleShouldCreateMR']),
updateSelectedCommitAction() {
if (this.currentBranch && !this.currentBranch.can_push) {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
@@ -43,7 +49,6 @@ export default {
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
- commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
currentBranchPermissionsTooltip: __(
"This option is disabled as you don't have write permissions for the current branch",
),
@@ -64,13 +69,17 @@ export default {
:label="__('Create a new branch')"
:show-input="true"
/>
- <radio-group
- v-if="currentProject.merge_requests_enabled"
- :value="$options.commitToNewBranchMR"
- :label="__('Create a new branch and merge request')"
- :title="__('This option is disabled while you still have unstaged changes')"
- :show-input="true"
- :disabled="disableMergeRequestRadio"
- />
+ <hr class="my-2" />
+ <label class="mb-0">
+ <input
+ :checked="shouldCreateMR"
+ :disabled="shouldDisableNewMrOption"
+ type="checkbox"
+ @change="toggleShouldCreateMR"
+ />
+ <span class="prepend-left-10" :class="{ 'text-secondary': shouldDisableNewMrOption }">
+ {{ __('Start a new merge request') }}
+ </span>
+ </label>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index d6673cf0421..80a6ab9598a 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -23,7 +23,7 @@ export default {
type: Object,
required: true,
},
- mouseOver: {
+ dropdownOpen: {
type: Boolean,
required: true,
},
@@ -92,8 +92,9 @@ export default {
<new-dropdown
:type="file.type"
:path="file.path"
- :mouse-over="mouseOver"
+ :is-open="dropdownOpen"
class="prepend-left-8"
+ v-on="$listeners"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 593a9162a06..27d24fa5e1d 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -21,38 +21,29 @@ export default {
required: false,
default: '',
},
- mouseOver: {
+ isOpen: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
},
- data() {
- return {
- dropdownOpen: false,
- };
- },
watch: {
- dropdownOpen() {
+ isOpen() {
this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView({
block: 'nearest',
});
});
},
- mouseOver() {
- if (!this.mouseOver) {
- this.dropdownOpen = false;
- }
- },
},
methods: {
...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
createNewItem(type) {
this.openNewEntryModal({ type, path: this.path });
- this.dropdownOpen = false;
+ this.$emit('toggle', false);
},
openDropdown() {
- this.dropdownOpen = !this.dropdownOpen;
+ this.$emit('toggle', !this.isOpen);
},
},
modalTypes,
@@ -63,7 +54,7 @@ export default {
<div class="ide-new-btn">
<div
:class="{
- show: dropdownOpen,
+ show: isOpen,
}"
class="dropdown d-flex"
>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 451c8030e16..5ae73b2fc9c 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -24,7 +24,13 @@ export default {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
- ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
+ ...mapState('pipelines', [
+ 'isLoadingPipeline',
+ 'hasLoadedPipeline',
+ 'latestPipeline',
+ 'stages',
+ 'isLoadingJobs',
+ ]),
ciLintText() {
return sprintf(
__('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
@@ -36,7 +42,7 @@ export default {
);
},
showLoadingIcon() {
- return this.isLoadingPipeline && this.latestPipeline === null;
+ return this.isLoadingPipeline && !this.hasLoadedPipeline;
},
},
created() {
@@ -51,7 +57,7 @@ export default {
<template>
<div class="ide-pipeline">
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
- <template v-else-if="latestPipeline !== null">
+ <template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" />
<span class="prepend-left-8">
@@ -62,7 +68,7 @@ export default {
</span>
</header>
<empty-state
- v-if="latestPipeline === false"
+ v-if="!latestPipeline"
:help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 8dd88f187d4..99f1d4a573d 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -5,7 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
-import * as consts from '../stores/modules/commit/constants';
+import consts from '../stores/modules/commit/constants';
import { activityBarViews, stageKeys } from '../constants';
export default {
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 362ced248a1..1273e375859 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -4,10 +4,11 @@ import service from '../../services';
import * as types from '../mutation_types';
import { activityBarViews } from '../../constants';
-export const getMergeRequestsForBranch = ({ commit }, { projectId, branchId } = {}) =>
+export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) =>
service
.getProjectMergeRequests(`${projectId}`, {
source_branch: branchId,
+ source_project_id: state.projects[projectId].id,
order_by: 'created_at',
per_page: 1,
})
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 06ed5c0b572..4b10d148ebf 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -147,6 +147,11 @@ export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
+ } else {
+ dispatch('createTempEntry', {
+ name: path,
+ type: 'blob',
+ });
}
}
})
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 8ad85074d6b..490658a4543 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -25,7 +25,10 @@ export const projectsWithTrees = state =>
});
export const currentMergeRequest = state => {
- if (state.projects[state.currentProjectId]) {
+ if (
+ state.projects[state.currentProjectId] &&
+ state.projects[state.currentProjectId].mergeRequests
+ ) {
return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
}
return null;
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 24c2f71ae2b..c2760eb1554 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -6,7 +6,7 @@ import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
-import * as consts from './constants';
+import consts from './constants';
import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
@@ -18,16 +18,23 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
-export const updateCommitAction = ({ commit }, commitAction) => {
- commit(types.UPDATE_COMMIT_ACTION, commitAction);
+export const updateCommitAction = ({ commit, rootGetters }, commitAction) => {
+ commit(types.UPDATE_COMMIT_ACTION, {
+ commitAction,
+ currentMergeRequest: rootGetters.currentMergeRequest,
+ });
+};
+
+export const toggleShouldCreateMR = ({ commit }) => {
+ commit(types.TOGGLE_SHOULD_CREATE_MR);
};
export const updateBranchName = ({ commit }, branchName) => {
commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
};
-export const setLastCommitMessage = ({ rootState, commit }, data) => {
- const currentProject = rootState.projects[rootState.currentProjectId];
+export const setLastCommitMessage = ({ commit, rootGetters }, data) => {
+ const { currentProject } = rootGetters;
const commitStats = data.stats
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
additions: data.stats.additions,
@@ -48,8 +55,8 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
};
-export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
- const selectedProject = rootState.projects[rootState.currentProjectId];
+export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetters }, { data }) => {
+ const selectedProject = rootGetters.currentProject;
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
commit: {
@@ -135,14 +142,15 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
branch: getters.branchName,
})
.then(() => {
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
+ if (state.shouldCreateMR) {
+ const { currentProject } = rootGetters;
+ const targetBranch = getters.isCreatingNewBranch
+ ? rootState.currentBranchId
+ : currentProject.default_branch;
+
dispatch(
'redirectToUrl',
- createNewMergeRequestUrl(
- rootState.projects[rootState.currentProjectId].web_url,
- getters.branchName,
- rootState.currentBranchId,
- ),
+ createNewMergeRequestUrl(currentProject.web_url, getters.branchName, targetBranch),
{ root: true },
);
}
diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js
index 230b0a3d9b5..c6c3701effe 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/constants.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js
@@ -1,3 +1,7 @@
-export const COMMIT_TO_CURRENT_BRANCH = '1';
-export const COMMIT_TO_NEW_BRANCH = '2';
-export const COMMIT_TO_NEW_BRANCH_MR = '3';
+const COMMIT_TO_CURRENT_BRANCH = '1';
+const COMMIT_TO_NEW_BRANCH = '2';
+
+export default {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index bbe40b2ec2f..6aa5d22a4ea 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,5 +1,5 @@
import { sprintf, n__, __ } from '../../../../locale';
-import * as consts from './constants';
+import consts from './constants';
const BRANCH_SUFFIX_COUNT = 5;
const createTranslatedTextForFiles = (files, text) => {
@@ -20,10 +20,7 @@ export const placeholderBranchName = (state, _, rootState) =>
)}`;
export const branchName = (state, getters, rootState) => {
- if (
- state.commitAction === consts.COMMIT_TO_NEW_BRANCH ||
- state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR
- ) {
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
if (state.newBranchName === '') {
return getters.placeholderBranchName;
}
@@ -49,5 +46,10 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
.join('\n');
};
+export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
+
+export const shouldDisableNewMrOption = (state, _getters, _rootState, rootGetters) =>
+ rootGetters.currentMergeRequest && state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
index 9221f054e9f..7ad8f3570b7 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -2,3 +2,4 @@ export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE';
export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
+export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 797357e3df9..be0f894c059 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
+import consts from './constants';
export default {
[types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
@@ -6,9 +7,13 @@ export default {
commitMessage,
});
},
- [types.UPDATE_COMMIT_ACTION](state, commitAction) {
+ [types.UPDATE_COMMIT_ACTION](state, { commitAction, currentMergeRequest }) {
Object.assign(state, {
commitAction,
+ shouldCreateMR:
+ commitAction === consts.COMMIT_TO_CURRENT_BRANCH && currentMergeRequest
+ ? false
+ : state.shouldCreateMR,
});
},
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
@@ -21,4 +26,9 @@ export default {
submitCommitLoading,
});
},
+ [types.TOGGLE_SHOULD_CREATE_MR](state) {
+ Object.assign(state, {
+ shouldCreateMR: !state.shouldCreateMR,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
index 8dae50961b0..5c0e6a41ca1 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/state.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -3,4 +3,5 @@ export default () => ({
commitAction: '1',
newBranchName: '',
submitCommitLoading: false,
+ shouldCreateMR: false,
});
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
index b4be100cb07..eaaa82cb339 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -10,6 +10,7 @@ export default {
},
[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) {
state.isLoadingPipeline = false;
+ state.hasLoadedPipeline = true;
if (pipeline) {
state.latestPipeline = {
@@ -34,7 +35,7 @@ export default {
};
});
} else {
- state.latestPipeline = false;
+ state.latestPipeline = null;
}
},
[types.REQUEST_JOBS](state, id) {
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
index 8651e267b53..8dfa0ec491f 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -1,5 +1,6 @@
export default () => ({
isLoadingPipeline: true,
+ hasLoadedPipeline: false,
isLoadingJobs: false,
latestPipeline: null,
stages: [],
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index eac7441ee54..359943b4ab7 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -1,5 +1,5 @@
import * as types from '../mutation_types';
-import { sortTree } from '../utils';
+import { sortTree, mergeTrees } from '../utils';
export default {
[types.TOGGLE_TREE_OPEN](state, path) {
@@ -23,9 +23,15 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
- Object.assign(state.trees[treePath], {
- tree: data,
- });
+ const selectedTree = state.trees[treePath];
+
+ // If we opened files while loading the tree, we need to merge them
+ // Otherwise, simply overwrite the tree
+ const tree = !selectedTree.tree.length
+ ? data
+ : selectedTree.loading && mergeTrees(selectedTree.tree, data);
+
+ Object.assign(selectedTree, { tree });
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
Object.assign(tree, {
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 0b2a18e9c8a..3ab8f3f11be 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -170,3 +170,31 @@ export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`)
export const getChangesCountForFiles = (files, path) =>
files.filter(f => filePathMatches(f.path, path)).length;
+
+export const mergeTrees = (fromTree, toTree) => {
+ if (!fromTree || !fromTree.length) {
+ return toTree;
+ }
+
+ const recurseTree = (n, t) => {
+ if (!n) {
+ return t;
+ }
+ const existingTreeNode = t.find(el => el.path === n.path);
+
+ if (existingTreeNode && n.tree.length > 0) {
+ existingTreeNode.opened = true;
+ recurseTree(n.tree[0], existingTreeNode.tree);
+ } else if (!existingTreeNode) {
+ const sorted = sortTree(t.concat(n));
+ t.splice(0, t.length + 1, ...sorted);
+ }
+ return t;
+ };
+
+ for (let i = 0, l = fromTree.length; i < l; i += 1) {
+ recurseTree(fromTree[i], toTree);
+ }
+
+ return toTree;
+};
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index b3508f36cf9..cd1afb6ba83 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -15,7 +15,6 @@ export default class Issue {
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
- Issue.initMergeRequests();
if (document.querySelector('#related-branches')) {
Issue.initRelatedBranches();
}
@@ -143,19 +142,6 @@ export default class Issue {
}
}
- static initMergeRequests() {
- var $container;
- $container = $('#merge-requests');
- return axios
- .get($container.data('url'))
- .then(({ data }) => {
- if ('html' in data) {
- $container.html(data.html);
- }
- })
- .catch(() => flash('Failed to load referenced merge requests'));
- }
-
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index d08e8ba0c4b..529b6386221 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,12 +1,9 @@
import Vue from 'vue';
-import sanitize from 'sanitize-html';
import issuableApp from './components/app.vue';
+import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor';
export default function initIssueableApp() {
- const initialDataEl = document.getElementById('js-issuable-app-initial-data');
- const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/&quot;/g, '"'));
-
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
@@ -14,7 +11,7 @@ export default function initIssueableApp() {
},
render(createElement) {
return createElement('issuable-app', {
- props,
+ props: parseIssuableData(),
});
},
});
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
new file mode 100644
index 00000000000..05e384adad3
--- /dev/null
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -0,0 +1,15 @@
+import sanitize from 'sanitize-html';
+
+export const parseIssuableData = () => {
+ try {
+ const initialDataEl = document.getElementById('js-issuable-app-initial-data');
+
+ return JSON.parse(sanitize(initialDataEl.textContent).replace(/&quot;/g, '"'));
+ } catch (e) {
+ console.error(e); // eslint-disable-line no-console
+
+ return {};
+ }
+};
+
+export default {};
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index 668fcf3d673..04f910b6b80 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -49,7 +49,7 @@ export default {
<div class="text-content">
<h4 class="js-job-empty-state-title text-center">{{ title }}</h4>
- <p v-if="content" class="js-job-empty-state-content">{{ content }}</p>
+ <p v-if="content" class="js-job-empty-state-content text-center">{{ content }}</p>
<div v-if="action" class="text-center">
<gl-link
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index dbadd224251..0670e2b06b9 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -15,6 +15,7 @@ import ErasedBlock from './erased_block.vue';
import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue';
+import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
import Sidebar from './sidebar.vue';
import { sprintf } from '~/locale';
import delayedJobMixin from '../mixins/delayed_job_mixin';
@@ -32,6 +33,7 @@ export default {
Log,
LogTopBar,
StuckBlock,
+ UnmetPrerequisitesBlock,
Sidebar,
GlLoadingIcon,
SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
@@ -48,6 +50,11 @@ export default {
required: false,
default: null,
},
+ deploymentHelpUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
endpoint: {
type: String,
required: true,
@@ -82,6 +89,7 @@ export default {
]),
...mapGetters([
'headerTime',
+ 'hasUnmetPrerequisitesFailure',
'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel',
'hasEnvironment',
@@ -210,7 +218,10 @@ export default {
/>
</div>
- <callout v-if="shouldRenderCalloutMessage" :message="job.callout_message" />
+ <callout
+ v-if="shouldRenderCalloutMessage && !hasUnmetPrerequisitesFailure"
+ :message="job.callout_message"
+ />
</header>
<!-- EO Header Section -->
@@ -223,6 +234,12 @@ export default {
:runners-path="runnerSettingsUrl"
/>
+ <unmet-prerequisites-block
+ v-if="hasUnmetPrerequisitesFailure"
+ class="js-job-failed"
+ :help-path="deploymentHelpUrl"
+ />
+
<shared-runner
v-if="shouldRenderSharedRunnerLimitWarning"
class="js-shared-runner-limit"
diff --git a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue
new file mode 100644
index 00000000000..25a8da84873
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+/**
+ * Renders Unmet Prerequisites block for job's view.
+ */
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="bs-callout bs-callout-danger">
+ <p class="js-failed-unmet-prerequisites append-bottom-0">
+ {{
+ s__(`Job|This job failed because the necessary resources were not successfully created.`)
+ }}
+
+ <gl-link :href="helpPath" class="js-help-path">
+ <strong> {{ __('More information') }} </strong>
+ </gl-link>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index a32e945627c..25132449458 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -12,6 +12,7 @@ export default () => {
render(createElement) {
return createElement('job-app', {
props: {
+ deploymentHelpUrl: element.dataset.deploymentHelpUrl,
runnerHelpUrl: element.dataset.runnerHelpUrl,
runnerSettingsUrl: element.dataset.runnerSettingsUrl,
endpoint: element.dataset.endpoint,
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 73c1cbc3a99..406b1a2e375 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -3,6 +3,9 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
+export const hasUnmetPrerequisitesFailure = state =>
+ state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites';
+
export const shouldRenderCalloutMessage = state =>
!_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index cca4927c115..7d21a216443 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -11,6 +11,7 @@ import CreateLabelDropdown from './create_label';
import flash from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
+import { isEE } from '~/lib/utils/common_utils';
export default class LabelsSelect {
constructor(els, options = {}) {
@@ -86,8 +87,9 @@ export default class LabelsSelect {
return this.value;
})
.get();
+ const scopedLabels = $dropdown.data('scopedLabels');
+ const scopedLabelsDocumentationLink = $dropdown.data('scopedLabelsDocumentationLink');
const { handleClick } = options;
-
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
@@ -132,8 +134,49 @@ export default class LabelsSelect {
template = LabelsSelect.getLabelTemplate({
labels: data.labels,
issueUpdateURL,
+ enableScopedLabels: scopedLabels,
+ scopedLabelsDocumentationLink,
});
labelCount = data.labels.length;
+
+ // EE Specific
+ if (isEE) {
+ /**
+ * For Scoped labels, the last label selected with the
+ * same key will be applied to the current issueable.
+ *
+ * If these are the labels - priority::1, priority::2; and if
+ * we apply them in the same order, only priority::2 will stick
+ * with the issuable.
+ *
+ * In the current dropdown implementation, we keep track of all
+ * the labels selected via a hidden DOM element. Since a User
+ * can select priority::1 and priority::2 at the same time, the
+ * DOM will have 2 hidden input and the dropdown will show both
+ * the items selected but in reality server only applied
+ * priority::2.
+ *
+ * We find all the labels then find all the labels server accepted
+ * and then remove the excess ones.
+ */
+ const toRemoveIds = Array.from(
+ $form.find(`input[type="hidden"][name="${fieldName}"]`),
+ )
+ .map(el => el.value)
+ .map(Number);
+
+ data.labels.forEach(label => {
+ const index = toRemoveIds.indexOf(label.id);
+ toRemoveIds.splice(index, 1);
+ });
+
+ toRemoveIds.forEach(id => {
+ $form
+ .find(`input[type="hidden"][name="${fieldName}"][value="${id}"]`)
+ .last()
+ .remove();
+ });
+ }
} else {
template = '<span class="no-value">None</span>';
}
@@ -358,6 +401,7 @@ export default class LabelsSelect {
} else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
+ $dropdown.data('glDropdown').clearMenu();
}
}
}
@@ -471,19 +515,62 @@ export default class LabelsSelect {
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
- const tpl = _.template(
+
+ const labelTemplate = _.template(
[
- '<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
- '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<span class="badge label has-tooltip color-label" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels, escapeStr }) %>" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;">',
'<%- label.title %>',
'</span>',
'</a>',
+ ].join(''),
+ );
+
+ const infoIconTemplate = _.template(
+ [
+ '<a href="<%= scopedLabelsDocumentationLink %>" class="label scoped-label" target="_blank" rel="noopener">',
+ '<i class="fa fa-question-circle" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;"></i>',
+ '</a>',
+ ].join(''),
+ );
+
+ const tooltipTitleTemplate = _.template(
+ [
+ '<% if (isScopedLabel(label) && enableScopedLabels) { %>',
+ "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>",
+ '<br />',
+ '<%= escapeStr(label.description) %>',
+ '<% } else { %>',
+ '<%= escapeStr(label.description) %>',
+ '<% } %>',
+ ].join(''),
+ );
+
+ const isScopedLabel = label => label.title.indexOf('::') !== -1;
+
+ const tpl = _.template(
+ [
+ '<% _.each(labels, function(label){ %>',
+ '<% if (isScopedLabel(label) && enableScopedLabels) { %>',
+ '<span class="d-inline-block position-relative scoped-label-wrapper">',
+ '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>',
+ '<%= infoIconTemplate({ label, scopedLabelsDocumentationLink, escapeStr }) %>',
+ '</span>',
+ '<% } else { %>',
+ '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>',
+ '<% } %>',
'<% }); %>',
].join(''),
);
- return tpl(tplData);
+ return tpl({
+ ...tplData,
+ labelTemplate,
+ infoIconTemplate,
+ tooltipTitleTemplate,
+ isScopedLabel,
+ escapeStr: _.escape,
+ });
}
bindEvents() {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 59930f8d4a3..2906604da57 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -7,7 +7,7 @@ import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
import { isObject } from './type_utility';
-import BreakpointInstance from '../../breakpoints';
+import breakpointInstance from '../../breakpoints';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
@@ -198,11 +198,10 @@ export const contentTop = () => {
const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
const headerHeight = $('.navbar-gitlab').outerHeight() || 0;
const diffFilesChanged = $('.js-diff-files-changed').outerHeight() || 0;
- const mdScreenOrBigger = ['lg', 'md'].includes(BreakpointInstance.getBreakpointSize());
+ const isDesktop = breakpointInstance.isDesktop();
const diffFileTitleBar =
- (mdScreenOrBigger && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0;
- const compareVersionsHeaderHeight =
- (mdScreenOrBigger && $('.mr-version-controls').outerHeight()) || 0;
+ (isDesktop && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0;
+ const compareVersionsHeaderHeight = (isDesktop && $('.mr-version-controls').outerHeight()) || 0;
return (
perfBar +
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index d3fe8f77bd4..4d6327840db 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -92,7 +92,7 @@ export const getTimeago = () => {
const timeAgoLocaleRemaining = [
() => [s__('Timeago|just now'), s__('Timeago|right now')],
- () => [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
+ () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')],
() => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
() => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
() => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
@@ -121,7 +121,7 @@ export const getTimeago = () => {
const timeAgoLocale = [
() => [s__('Timeago|just now'), s__('Timeago|right now')],
- () => [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
+ () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')],
() => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
() => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
() => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
@@ -160,7 +160,11 @@ export const getTimeago = () => {
* @param {Boolean} setTimeago
*/
export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
- getTimeago().render($timeagoEls, timeagoLanguageCode);
+ getTimeago();
+
+ $timeagoEls.each((i, el) => {
+ $(el).text(timeagoInstance.format($(el).attr('datetime'), timeagoLanguageCode));
+ });
if (!setTimeago) {
return;
diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js
new file mode 100644
index 00000000000..4f7eff2cca1
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/highlight.js
@@ -0,0 +1,44 @@
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import _ from 'underscore';
+import sanitize from 'sanitize-html';
+
+/**
+ * Wraps substring matches with HTML `<span>` elements.
+ * Inputs are sanitized before highlighting, so this
+ * filter is safe to use with `v-html` (as long as `matchPrefix`
+ * and `matchSuffix` are not being dynamically generated).
+ *
+ * Note that this function can't be used inside `v-html` as a filter
+ * (Vue filters cannot be used inside `v-html`).
+ *
+ * @param {String} string The string to highlight
+ * @param {String} match The substring match to highlight in the string
+ * @param {String} matchPrefix The string to insert at the beginning of a match
+ * @param {String} matchSuffix The string to insert at the end of a match
+ */
+export default function highlight(string, match = '', matchPrefix = '<b>', matchSuffix = '</b>') {
+ if (_.isUndefined(string) || _.isNull(string)) {
+ return '';
+ }
+
+ if (_.isUndefined(match) || _.isNull(match) || match === '') {
+ return string;
+ }
+
+ const sanitizedValue = sanitize(string.toString(), { allowedTags: [] });
+
+ // occurences is an array of character indices that should be
+ // highlighted in the original string, i.e. [3, 4, 5, 7]
+ const occurences = fuzzaldrinPlus.match(sanitizedValue, match.toString());
+
+ return sanitizedValue
+ .split('')
+ .map((character, i) => {
+ if (_.contains(occurences, i)) {
+ return `${matchPrefix}${character}${matchSuffix}`;
+ }
+
+ return character;
+ })
+ .join('');
+}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index c49b1bb5a2f..1b7f8732c65 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,3 +1,5 @@
+import _ from 'underscore';
+
/**
* Adds a , to a string composed by numbers, at every 3 chars.
*
@@ -160,3 +162,33 @@ export const splitCamelCase = string =>
.replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
.trim();
+
+/**
+ * Intelligently truncates an item's namespace by doing two things:
+ * 1. Only include group names in path by removing the item name
+ * 2. Only include the first and last group names in the path
+ * when the namespace includes more than 2 groups
+ *
+ * @param {String} string A string namespace,
+ * i.e. "My Group / My Subgroup / My Project"
+ */
+export const truncateNamespace = (string = '') => {
+ if (_.isNull(string) || !_.isString(string)) {
+ return '';
+ }
+
+ const namespaceArray = string.split(' / ');
+
+ if (namespaceArray.length === 1) {
+ return string;
+ }
+
+ namespaceArray.splice(-1, 1);
+ let namespace = namespaceArray.join(' / ');
+
+ if (namespaceArray.length > 2) {
+ namespace = `${namespaceArray[0]} / ... / ${namespaceArray.pop()}`;
+ }
+
+ return namespace;
+};
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index d453dc1fdb7..afe8d87a8d6 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,16 +1,18 @@
<script>
-import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { chartHeight, graphTypes, lineTypes } from '../../constants';
+import { makeDataSeries } from '~/helpers/monitor_helper';
let debouncedResize;
export default {
components: {
GlAreaChart,
+ GlChartSeriesLabel,
Icon,
},
inheritAttrs: false,
@@ -41,10 +43,10 @@ export default {
required: false,
default: () => [],
},
- alertData: {
- type: Object,
+ thresholds: {
+ type: Array,
required: false,
- default: () => ({}),
+ default: () => [],
},
},
data() {
@@ -63,7 +65,10 @@ export default {
},
computed: {
chartData() {
- return this.graphData.queries.map(query => {
+ // Transforms & supplements query data to render appropriate labels & styles
+ // Input: [{ queryAttributes1 }, { queryAttributes2 }]
+ // Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
+ return this.graphData.queries.reduce((acc, query) => {
const { appearance } = query;
const lineType =
appearance && appearance.line && appearance.line.type
@@ -74,9 +79,8 @@ export default {
? appearance.line.width
: undefined;
- return {
+ const series = makeDataSeries(query.result, {
name: this.formatLegendLabel(query),
- data: this.concatenateResults(query.result),
lineStyle: {
type: lineType,
width: lineWidth,
@@ -87,8 +91,10 @@ export default {
? appearance.area.opacity
: undefined,
},
- };
- });
+ });
+
+ return acc.concat(series);
+ }, []);
},
chartOptions() {
return {
@@ -119,6 +125,9 @@ export default {
},
earliestDatapoint() {
return this.chartData.reduce((acc, series) => {
+ if (!series.data.length) {
+ return acc;
+ }
const [[timestamp]] = series.data.sort(([a], [b]) => {
if (a < b) {
return -1;
@@ -129,6 +138,9 @@ export default {
return timestamp < acc || acc === null ? timestamp : acc;
}, null);
},
+ isMultiSeries() {
+ return this.tooltip.content.length > 1;
+ },
recentDeployments() {
return this.deploymentData.reduce((acc, deployment) => {
if (deployment.created_at >= this.earliestDatapoint) {
@@ -175,9 +187,6 @@ export default {
this.setSvg('scroll-handle');
},
methods: {
- concatenateResults(results) {
- return results.reduce((acc, result) => acc.concat(result.values), []);
- },
formatLegendLabel(query) {
return `${query.label}`;
},
@@ -192,7 +201,7 @@ export default {
);
this.tooltip.sha = deploy.sha.substring(0, 8);
} else {
- const { seriesName } = seriesData;
+ const { seriesName, color } = seriesData;
// seriesData.value contains the chart's [x, y] value pair
// seriesData.value[1] is threfore the chart y value
const value = seriesData.value[1].toFixed(3);
@@ -200,6 +209,7 @@ export default {
this.tooltip.content.push({
name: seriesName,
value,
+ color,
});
}
});
@@ -236,29 +246,38 @@ export default {
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
- :thresholds="alertData"
+ :thresholds="thresholds"
:width="width"
:height="height"
@updated="onChartUpdated"
>
- <template slot="tooltipTitle">
- <div v-if="tooltip.isDeployment">
+ <template v-if="tooltip.isDeployment">
+ <template slot="tooltipTitle">
{{ __('Deployed') }}
- </div>
- {{ tooltip.title }}
- </template>
- <template slot="tooltipContent">
- <div v-if="tooltip.isDeployment" class="d-flex align-items-center">
+ </template>
+ <div slot="tooltipContent" class="d-flex align-items-center">
<icon name="commit" class="mr-2" />
{{ tooltip.sha }}
</div>
- <template v-else>
+ </template>
+ <template v-else>
+ <template slot="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template slot="tooltipContent">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
- {{ content.name }} {{ content.value }}
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
</div>
</template>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index ba6a17827f7..f2bd4150b6d 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,5 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import '~/vue_shared/mixins/is_ee';
@@ -9,6 +10,8 @@ import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
+import { timeWindows } from '../constants';
+import { getTimeDiff } from '../utils';
const sidebarAnimationDuration = 150;
let sidebarMutationObserver;
@@ -87,6 +90,10 @@ export default {
type: String,
required: true,
},
+ showTimeWindowDropdown: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -94,6 +101,7 @@ export default {
state: 'gettingStarted',
showEmptyState: true,
elWidth: 0,
+ selectedTimeWindow: '',
};
},
created() {
@@ -102,6 +110,9 @@ export default {
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
+
+ this.timeWindows = timeWindows;
+ this.selectedTimeWindow = this.timeWindows.eightHours;
},
beforeDestroy() {
if (sidebarMutationObserver) {
@@ -142,8 +153,13 @@ export default {
}
},
methods: {
- getGraphAlerts(graphId) {
- return this.alertData ? this.alertData[graphId] || {} : {};
+ getGraphAlerts(queries) {
+ if (!this.allAlerts) return {};
+ const metricIdsForChart = queries.map(q => q.metricId);
+ return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
+ },
+ getGraphAlertValues(queries) {
+ return Object.values(this.getGraphAlerts(queries));
},
getGraphsData() {
this.state = 'loading';
@@ -160,33 +176,73 @@ export default {
this.state = 'unableToConnect';
});
},
+ getGraphsDataWithTime(timeFrame) {
+ this.state = 'loading';
+ this.showEmptyState = true;
+ this.service
+ .getGraphsData(getTimeDiff(this.timeWindows[timeFrame]))
+ .then(data => {
+ this.store.storeMetrics(data);
+ this.selectedTimeWindow = this.timeWindows[timeFrame];
+ })
+ .catch(() => {
+ Flash(s__('Metrics|Not enough data to display'));
+ })
+ .finally(() => {
+ this.showEmptyState = false;
+ });
+ },
onSidebarMutation() {
setTimeout(() => {
this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration);
},
+ activeTimeWindow(key) {
+ return this.timeWindows[key] === this.selectedTimeWindow;
+ },
},
};
</script>
<template>
<div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default">
- <div v-if="environmentsEndpoint" class="environments d-flex align-items-center">
- <strong>{{ s__('Metrics|Environment') }}</strong>
- <gl-dropdown
- class="prepend-left-10 js-environments-dropdown"
- toggle-class="dropdown-menu-toggle"
- :text="currentEnvironmentName"
- :disabled="store.environmentsData.length === 0"
- >
- <gl-dropdown-item
- v-for="environment in store.environmentsData"
- :key="environment.id"
- :active="environment.name === currentEnvironmentName"
- active-class="is-active"
- >{{ environment.name }}</gl-dropdown-item
+ <div
+ v-if="environmentsEndpoint"
+ class="dropdowns d-flex align-items-center justify-content-between"
+ >
+ <div class="d-flex align-items-center">
+ <strong>{{ s__('Metrics|Environment') }}</strong>
+ <gl-dropdown
+ class="prepend-left-10 js-environments-dropdown"
+ toggle-class="dropdown-menu-toggle"
+ :text="currentEnvironmentName"
+ :disabled="store.environmentsData.length === 0"
+ >
+ <gl-dropdown-item
+ v-for="environment in store.environmentsData"
+ :key="environment.id"
+ :active="environment.name === currentEnvironmentName"
+ active-class="is-active"
+ >{{ environment.name }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
+ <div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
+ <strong>{{ s__('Metrics|Show last') }}</strong>
+ <gl-dropdown
+ class="prepend-left-10 js-time-window-dropdown"
+ toggle-class="dropdown-menu-toggle"
+ :text="selectedTimeWindow"
>
- </gl-dropdown>
+ <gl-dropdown-item
+ v-for="(value, key) in timeWindows"
+ :key="key"
+ :active="activeTimeWindow(key)"
+ @click="getGraphsDataWithTime(key)"
+ >{{ value }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
</div>
<graph-group
v-for="(groupData, index) in store.groups"
@@ -199,17 +255,15 @@ export default {
:key="graphIndex"
:graph-data="graphData"
:deployment-data="store.deploymentData"
- :alert-data="getGraphAlerts(graphData.id)"
+ :thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
group-id="monitor-area-chart"
>
<alert-widget
- v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData.id"
+ v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData"
:alerts-endpoint="alertsEndpoint"
- :label="getGraphLabel(graphData)"
- :current-alerts="getQueryAlerts(graphData)"
- :custom-metric-id="graphData.id"
- :alert-data="alertData[graphData.id]"
+ :relevant-queries="graphData.queries"
+ :alerts-to-manage="getGraphAlerts(graphData.queries)"
@setAlerts="setAlerts"
/>
</monitor-area-chart>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 55ecf3b5334..9e5d0d0fd28 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const chartHeight = 300;
export const graphTypes = {
@@ -7,3 +9,14 @@ export const graphTypes = {
export const lineTypes = {
default: 'solid',
};
+
+export const timeWindows = {
+ thirtyMinutes: __('30 minutes'),
+ threeHours: __('3 hours'),
+ eightHours: __('8 hours'),
+ oneDay: __('1 day'),
+ threeDays: __('3 days'),
+ oneWeek: __('1 week'),
+};
+
+export const msPerMinute = 60000;
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 9d78b5ea110..2b4ddd7afbc 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -14,6 +14,7 @@ export default () => {
props: {
...el.dataset,
hasMetrics: parseBoolean(el.dataset.hasMetrics),
+ showTimeWindowDropdown: gon.features.metricsTimeWindow,
},
});
},
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index 24b4acaf6da..5fcc2c8cfac 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -32,11 +32,11 @@ export default class MonitoringService {
this.environmentsEndpoint = environmentsEndpoint;
}
- getGraphsData() {
- return backOffRequest(() => axios.get(this.metricsEndpoint))
+ getGraphsData(params = {}) {
+ return backOffRequest(() => axios.get(this.metricsEndpoint, { params }))
.then(resp => resp.data)
.then(response => {
- if (!response || !response.data) {
+ if (!response || !response.data || !response.success) {
throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
}
return response.data;
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 70635059bd9..9761fe168be 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -27,10 +27,47 @@ 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 }]}
+// ]
+function groupQueriesByChartInfo(metrics) {
+ const metricsByChart = metrics.reduce((accumulator, metric) => {
+ const { id, queries, ...chart } = metric;
+
+ const chartKey = `${chart.title}|${chart.y_label}`;
+ accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
+
+ queries.forEach(queryAttrs =>
+ accumulator[chartKey].queries.push({ metricId: id.toString(), ...queryAttrs }),
+ );
+
+ return accumulator;
+ }, {});
+
+ return Object.values(metricsByChart);
+}
+
function normalizeMetrics(metrics) {
- return metrics.map(metric => {
+ 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(result => ({
...result,
values: result.values.map(([timestamp, value]) => [
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
new file mode 100644
index 00000000000..e379827b769
--- /dev/null
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -0,0 +1,34 @@
+import { timeWindows, msPerMinute } from './constants';
+
+/**
+ * method that converts a predetermined time window to minutes
+ * defaults to 8 hours as the default option
+ * @param {String} timeWindow - The time window to convert to minutes
+ * @returns {number} The time window in minutes
+ */
+const getTimeDifferenceMinutes = timeWindow => {
+ switch (timeWindow) {
+ case timeWindows.thirtyMinutes:
+ return 30;
+ case timeWindows.threeHours:
+ return 60 * 3;
+ case timeWindows.oneDay:
+ return 60 * 24 * 1;
+ case timeWindows.threeDays:
+ return 60 * 24 * 3;
+ case timeWindows.oneWeek:
+ return 60 * 24 * 7 * 1;
+ default:
+ return 60 * 8;
+ }
+};
+
+export const getTimeDiff = selectedTimeWindow => {
+ const end = Date.now();
+ const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow);
+ const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime();
+
+ return { start, end };
+};
+
+export default {};
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index fc73726857d..aabb77f6a85 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -86,9 +86,6 @@ export default {
},
computed: {
...mapGetters(['getUserDataByProp']),
- showReplyButton() {
- return gon.features && gon.features.replyToIndividualNotes && this.showReply;
- },
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
@@ -167,7 +164,7 @@ export default {
</a>
</div>
<reply-button
- v-if="showReplyButton"
+ v-if="showReply"
ref="replyButton"
class="js-reply-button"
@startReplying="$emit('startReplying')"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 57d6b181bd7..471323bfc83 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -61,6 +61,11 @@ export default {
required: false,
default: null,
},
+ diffFile: {
+ type: Object,
+ required: false,
+ default: null,
+ },
helpPagePath: {
type: String,
required: false,
@@ -102,9 +107,42 @@ export default {
}
return '#';
},
+ diffParams() {
+ if (this.diffFile) {
+ return {
+ filePath: this.diffFile.file_path,
+ refs: this.diffFile.diff_refs,
+ };
+ } else if (this.note && this.note.position) {
+ return {
+ filePath: this.note.position.new_path,
+ refs: this.note.position,
+ };
+ } else if (this.discussion && this.discussion.diff_file) {
+ return {
+ filePath: this.discussion.diff_file.file_path,
+ refs: this.discussion.diff_file.diff_refs,
+ };
+ }
+
+ return null;
+ },
markdownPreviewPath() {
const notable = this.getNoteableDataByProp('preview_note_path');
- return mergeUrlParams({ preview_suggestions: true }, notable);
+
+ const previewSuggestions = this.line && this.diffParams;
+ const params = previewSuggestions
+ ? {
+ preview_suggestions: previewSuggestions,
+ line: this.line.new_line,
+ file_path: this.diffParams.filePath,
+ base_sha: this.diffParams.refs.base_sha,
+ start_sha: this.diffParams.refs.start_sha,
+ head_sha: this.diffParams.refs.head_sha,
+ }
+ : {};
+
+ return mergeUrlParams(params, notable);
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
@@ -234,8 +272,8 @@ export default {
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleKeySubmit()"
@keydown.ctrl.enter="handleKeySubmit()"
- @keydown.up="editMyLastNote()"
- @keydown.esc="cancelHandler(true)"
+ @keydown.exact.up="editMyLastNote()"
+ @keydown.exact.esc="cancelHandler(true)"
@input="onInput"
></textarea>
</markdown-field>
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 6aed2492084..08545dcea46 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Flash from './flash';
+import { __ } from '~/locale';
export default function notificationsDropdown() {
$(document).on('click', '.update-notification', function updateNotificationCallback(e) {
@@ -27,7 +28,7 @@ export default function notificationsDropdown() {
.closest('.js-notification-dropdown')
.replaceWith(data.html);
} else {
- Flash('Failed to save new settings', 'alert');
+ Flash(__('Failed to save new settings'), 'alert');
}
});
}
diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js
index d3d125a1859..ad7276132b9 100644
--- a/app/assets/javascripts/pages/admin/groups/edit/index.js
+++ b/app/assets/javascripts/pages/admin/groups/edit/index.js
@@ -1,3 +1,3 @@
-import groupAvatar from '~/group_avatar';
+import initAvatarPicker from '~/avatar_picker';
-document.addEventListener('DOMContentLoaded', groupAvatar);
+document.addEventListener('DOMContentLoaded', initAvatarPicker);
diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js
index 21f1ce222ac..6de740ee9ce 100644
--- a/app/assets/javascripts/pages/admin/groups/new/index.js
+++ b/app/assets/javascripts/pages/admin/groups/new/index.js
@@ -1,9 +1,9 @@
import BindInOut from '../../../../behaviors/bind_in_out';
import Group from '../../../../group';
-import groupAvatar from '../../../../group_avatar';
+import initAvatarPicker from '~/avatar_picker';
document.addEventListener('DOMContentLoaded', () => {
BindInOut.initAll();
new Group(); // eslint-disable-line no-new
- groupAvatar();
+ initAvatarPicker();
});
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 01ef445c901..d036ff07d89 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,4 +1,4 @@
-import groupAvatar from '~/group_avatar';
+import initAvatarPicker from '~/avatar_picker';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
@@ -9,7 +9,7 @@ import groupsSelect from '~/groups_select';
import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
- groupAvatar();
+ initAvatarPicker();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
initSettingsPanels();
diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/groups/labels/edit/index.js
+++ b/app/assets/javascripts/pages/groups/labels/edit/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/groups/labels/new/index.js b/app/assets/javascripts/pages/groups/labels/new/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/groups/labels/new/index.js
+++ b/app/assets/javascripts/pages/groups/labels/new/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index b2f275dc5ea..57b53eb9e5d 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,9 +1,9 @@
import BindInOut from '~/behaviors/bind_in_out';
import Group from '~/group';
-import groupAvatar from '~/group_avatar';
+import initAvatarPicker from '~/avatar_picker';
document.addEventListener('DOMContentLoaded', () => {
BindInOut.initAll();
new Group(); // eslint-disable-line no-new
- groupAvatar();
+ initAvatarPicker();
});
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 899d5925956..278c35d3846 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -3,7 +3,7 @@ import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
-import fileUpload from '~/lib/utils/file_upload';
+import initAvatarPicker from '~/avatar_picker';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectPermissionsSettings from '../shared/permissions';
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
- fileUpload('.js-choose-project-avatar-button', '.js-project-avatar-input');
+ initAvatarPicker();
initProjectPermissionsSettings();
initConfirmDangerModal();
mountBadgeSettings(PROJECT_BADGE);
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 8987c8e3f47..0447d1f79fb 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -4,9 +4,11 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import initIssueableApp from '~/issue_show';
+import initRelatedMergeRequestsApp from '~/related_merge_requests';
export default function() {
initIssueableApp();
+ initRelatedMergeRequestsApp();
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/projects/labels/edit/index.js
+++ b/app/assets/javascripts/pages/projects/labels/edit/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js
index fa81ad914ba..83d6ac9fd14 100644
--- a/app/assets/javascripts/pages/projects/labels/new/index.js
+++ b/app/assets/javascripts/pages/projects/labels/new/index.js
@@ -1,3 +1,3 @@
-import Labels from '~/labels';
+import Labels from 'ee_else_ce/labels';
document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 2b32a6e4a98..0d5afe04e8e 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -57,6 +57,9 @@ export default {
},
},
computed: {
+ boundary() {
+ return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
+ },
status() {
return this.job && this.job.status ? this.job.status : {};
},
@@ -104,7 +107,7 @@ export default {
<div class="ci-job-component">
<gl-link
v-if="status.has_details"
- v-gl-tooltip
+ v-gl-tooltip="{ boundary, placement: 'bottom' }"
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
@@ -115,7 +118,7 @@ export default {
<div
v-else
- v-gl-tooltip
+ v-gl-tooltip="{ boundary, placement: 'bottom' }"
:title="tooltipText"
:class="cssClassJobName"
class="js-job-component-tooltip non-details-job-component"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 1bfab2a7fc0..02451839330 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -27,7 +27,8 @@ export default {
<template>
<span class="ci-job-name-component">
<ci-icon :status="status" />
-
- <span class="ci-status-text"> {{ name }} </span>
+ <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom">
+ {{ name }}
+ </span>
</span>
</template>
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
new file mode 100644
index 00000000000..52d4b75a3a1
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -0,0 +1,121 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { sprintf, n__, s__ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import { parseIssuableData } from '../../issue_show/utils/parse_data';
+
+export default {
+ name: 'RelatedMergeRequests',
+ components: {
+ Icon,
+ GlLoadingIcon,
+ RelatedIssuableItem,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectNamespace: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['isFetchingMergeRequests', 'mergeRequests', 'totalCount']),
+ closingMergeRequestsText() {
+ if (!this.hasClosingMergeRequest) {
+ return '';
+ }
+
+ const mrText = n__(
+ 'When this merge request is accepted',
+ 'When these merge requests are accepted',
+ this.totalCount,
+ );
+
+ return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText });
+ },
+ },
+ mounted() {
+ this.setInitialState({ apiEndpoint: this.endpoint });
+ this.fetchMergeRequests();
+ },
+ created() {
+ this.hasClosingMergeRequest = parseIssuableData().hasClosingMergeRequest;
+ },
+ methods: {
+ ...mapActions(['setInitialState', 'fetchMergeRequests']),
+ getAssignees(mr) {
+ if (mr.assignees) {
+ return mr.assignees;
+ }
+
+ return mr.assignee ? [mr.assignee] : [];
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)">
+ <div id="merge-requests" class="card-slim mt-3">
+ <div class="card-header">
+ <div class="card-title mt-0 mb-0 h5 merge-requests-title">
+ <span class="mr-1">
+ {{ __('Related merge requests') }}
+ </span>
+ <div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
+ <div class="mr-count-badge">
+ <div class="mr-count-badge-count">
+ <svg class="s16 mr-1 text-secondary">
+ <icon name="merge-request" class="mr-1 text-secondary" />
+ </svg>
+ <span class="js-items-count">{{ totalCount }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div>
+ <div
+ v-if="isFetchingMergeRequests"
+ class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon"
+ >
+ <gl-loading-icon label="Fetching related merge requests" class="py-2" />
+ </div>
+ <ul v-else class="content-list related-items-list">
+ <li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0">
+ <related-issuable-item
+ :id-key="mr.id"
+ :display-reference="mr.reference"
+ :title="mr.title"
+ :milestone="mr.milestone"
+ :assignees="getAssignees(mr)"
+ :created-at="mr.created_at"
+ :closed-at="mr.closed_at"
+ :merged-at="mr.merged_at"
+ :path="mr.web_url"
+ :state="mr.state"
+ :is-merge-request="true"
+ :pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
+ path-id-separator="!"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div
+ v-if="hasClosingMergeRequest && !isFetchingMergeRequests"
+ class="issue-closed-by-widget second-block"
+ >
+ {{ closingMergeRequestsText }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/related_merge_requests/index.js b/app/assets/javascripts/related_merge_requests/index.js
new file mode 100644
index 00000000000..092ff1df00f
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import RelatedMergeRequests from './components/related_merge_requests.vue';
+import createStore from './store';
+
+export default function initRelatedMergeRequests() {
+ const relatedMergeRequestsElement = document.querySelector('#js-related-merge-requests');
+
+ if (relatedMergeRequestsElement) {
+ const { endpoint, projectPath, projectNamespace } = relatedMergeRequestsElement.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: relatedMergeRequestsElement,
+ components: {
+ RelatedMergeRequests,
+ },
+ store: createStore(),
+ render: createElement =>
+ createElement('related-merge-requests', {
+ props: { endpoint, projectNamespace, projectPath },
+ }),
+ });
+ }
+}
diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js
new file mode 100644
index 00000000000..69abeaaf7db
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/store/actions.js
@@ -0,0 +1,37 @@
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { normalizeHeaders } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
+
+const REQUEST_PAGE_COUNT = 100;
+
+export const setInitialState = ({ commit }, props) => {
+ commit(types.SET_INITIAL_STATE, props);
+};
+
+export const requestData = ({ commit }) => commit(types.REQUEST_DATA);
+
+export const receiveDataSuccess = ({ commit }, data) => commit(types.RECEIVE_DATA_SUCCESS, data);
+
+export const receiveDataError = ({ commit }) => commit(types.RECEIVE_DATA_ERROR);
+
+export const fetchMergeRequests = ({ state, dispatch }) => {
+ dispatch('requestData');
+
+ return axios
+ .get(`${state.apiEndpoint}?per_page=${REQUEST_PAGE_COUNT}`)
+ .then(res => {
+ const { headers, data } = res;
+ const total = Number(normalizeHeaders(headers)['X-TOTAL']) || 0;
+
+ dispatch('receiveDataSuccess', { data, total });
+ })
+ .catch(() => {
+ dispatch('receiveDataError');
+ createFlash(s__('Something went wrong while fetching related merge requests.'));
+ });
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/related_merge_requests/store/index.js b/app/assets/javascripts/related_merge_requests/store/index.js
new file mode 100644
index 00000000000..dcb70c22bcb
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ state: createState(),
+ actions,
+ mutations,
+ });
diff --git a/app/assets/javascripts/related_merge_requests/store/mutation_types.js b/app/assets/javascripts/related_merge_requests/store/mutation_types.js
new file mode 100644
index 00000000000..31d4fe032e1
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/store/mutation_types.js
@@ -0,0 +1,4 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+export const REQUEST_DATA = 'REQUEST_DATA';
+export const RECEIVE_DATA_SUCCESS = 'RECEIVE_DATA_SUCCESS';
+export const RECEIVE_DATA_ERROR = 'RECEIVE_DATA_ERROR';
diff --git a/app/assets/javascripts/related_merge_requests/store/mutations.js b/app/assets/javascripts/related_merge_requests/store/mutations.js
new file mode 100644
index 00000000000..11ca28a5fb9
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/store/mutations.js
@@ -0,0 +1,19 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_STATE](state, { apiEndpoint }) {
+ state.apiEndpoint = apiEndpoint;
+ },
+ [types.REQUEST_DATA](state) {
+ state.isFetchingMergeRequests = true;
+ },
+ [types.RECEIVE_DATA_SUCCESS](state, { data, total }) {
+ state.isFetchingMergeRequests = false;
+ state.mergeRequests = data;
+ state.totalCount = total;
+ },
+ [types.RECEIVE_DATA_ERROR](state) {
+ state.isFetchingMergeRequests = false;
+ state.hasErrorFetchingMergeRequests = true;
+ },
+};
diff --git a/app/assets/javascripts/related_merge_requests/store/state.js b/app/assets/javascripts/related_merge_requests/store/state.js
new file mode 100644
index 00000000000..bc3468a025b
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/store/state.js
@@ -0,0 +1,7 @@
+export default () => ({
+ apiEndpoint: '',
+ isFetchingMergeRequests: false,
+ hasErrorFetchingMergeRequests: false,
+ mergeRequests: [],
+ totalCount: 0,
+});
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index 2946fbc6a1f..04fba43b2f3 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -13,6 +13,11 @@ export default {
type: String,
required: true,
},
+ statusIconSize: {
+ type: Number,
+ required: false,
+ default: 32,
+ },
},
computed: {
iconName() {
@@ -45,6 +50,6 @@ export default {
}"
class="report-block-list-icon"
>
- <icon :name="iconName" :size="32" />
+ <icon :name="iconName" :size="statusIconSize" />
</div>
</template>
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue
index 839e86bdf17..d2106f9ad2e 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
+ statusIconSize: {
+ type: Number,
+ required: false,
+ default: 32,
+ },
isNew: {
type: Boolean,
required: false,
@@ -34,7 +39,7 @@ export default {
</script>
<template>
<li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue">
- <issue-status-icon :status="status" class="append-right-5" />
+ <issue-status-icon :status="status" :status-icon-size="statusIconSize" class="append-right-5" />
<component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" />
</li>
diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue
new file mode 100644
index 00000000000..32c9d6eccb8
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/area.vue
@@ -0,0 +1,146 @@
+<script>
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import dateFormat from 'dateformat';
+import { X_INTERVAL } from '../constants';
+import { validateGraphData } from '../utils';
+
+let debouncedResize;
+
+export default {
+ components: {
+ GlAreaChart,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: validateGraphData,
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ tooltipPopoverTitle: '',
+ tooltipPopoverContent: '',
+ width: this.containerWidth,
+ };
+ },
+ computed: {
+ chartData() {
+ return this.graphData.queries.reduce((accumulator, query) => {
+ accumulator[query.unit] = query.result.reduce((acc, res) => acc.concat(res.values), []);
+ return accumulator;
+ }, {});
+ },
+ extractTimeData() {
+ return this.chartData.requests.map(data => data.time);
+ },
+ generateSeries() {
+ return {
+ name: 'Invocations',
+ type: 'line',
+ data: this.chartData.requests.map(data => [data.time, data.value]),
+ symbolSize: 0,
+ };
+ },
+ getInterval() {
+ const { result } = this.graphData.queries[0];
+
+ if (result.length === 0) {
+ return 1;
+ }
+
+ const split = result[0].values.reduce(
+ (acc, pair) => (pair.value > acc ? pair.value : acc),
+ 1,
+ );
+
+ return split < X_INTERVAL ? split : X_INTERVAL;
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ name: 'time',
+ type: 'time',
+ axisLabel: {
+ formatter: date => dateFormat(date, 'h:MM TT'),
+ },
+ data: this.extractTimeData,
+ nameTextStyle: {
+ padding: [18, 0, 0, 0],
+ },
+ },
+ yAxis: {
+ name: this.yAxisLabel,
+ nameTextStyle: {
+ padding: [0, 0, 36, 0],
+ },
+ splitNumber: this.getInterval,
+ },
+ legend: {
+ formatter: this.xAxisLabel,
+ },
+ series: this.generateSeries,
+ };
+ },
+ xAxisLabel() {
+ return this.graphData.queries.map(query => query.label).join(', ');
+ },
+ yAxisLabel() {
+ const [query] = this.graphData.queries;
+ return `${this.graphData.y_label} (${query.unit})`;
+ },
+ },
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ },
+ methods: {
+ formatTooltipText(params) {
+ const [seriesData] = params.seriesData;
+ this.tooltipPopoverTitle = dateFormat(params.value, 'dd mmm yyyy, h:MMTT');
+ this.tooltipPopoverContent = `${this.yAxisLabel}: ${seriesData.value[1]}`;
+ },
+ onResize() {
+ const { width } = this.$refs.areaChart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="prometheus-graph">
+ <div class="prometheus-graph-header">
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
+ <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
+ </div>
+ <gl-area-chart
+ ref="areaChart"
+ v-bind="$attrs"
+ :data="[]"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ :width="width"
+ :include-legend-avg-max="false"
+ >
+ <template slot="tooltipTitle">
+ {{ tooltipPopoverTitle }}
+ </template>
+ <template slot="tooltipContent">
+ {{ tooltipPopoverContent }}
+ </template>
+ </gl-area-chart>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue
index 4f89ad69129..b8906cfca4e 100644
--- a/app/assets/javascripts/serverless/components/function_details.vue
+++ b/app/assets/javascripts/serverless/components/function_details.vue
@@ -1,39 +1,77 @@
<script>
+import _ from 'underscore';
+import { mapState, mapActions, mapGetters } from 'vuex';
import PodBox from './pod_box.vue';
import Url from './url.vue';
+import AreaChart from './area.vue';
+import MissingPrometheus from './missing_prometheus.vue';
export default {
components: {
PodBox,
Url,
+ AreaChart,
+ MissingPrometheus,
},
props: {
func: {
type: Object,
required: true,
},
+ hasPrometheus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ elWidth: 0,
+ };
},
computed: {
name() {
return this.func.name;
},
description() {
- return this.func.description;
+ return _.isString(this.func.description) ? this.func.description : '';
},
funcUrl() {
return this.func.url;
},
podCount() {
- return this.func.podcount || 0;
+ return Number(this.func.podcount) || 0;
},
+ ...mapState(['graphData', 'hasPrometheusData']),
+ ...mapGetters(['hasPrometheusMissingData']),
+ },
+ created() {
+ this.fetchMetrics({
+ metricsPath: this.func.metricsUrl,
+ hasPrometheus: this.hasPrometheus,
+ });
+ },
+ mounted() {
+ this.elWidth = this.$el.clientWidth;
+ },
+ methods: {
+ ...mapActions(['fetchMetrics']),
},
};
</script>
<template>
<section id="serverless-function-details">
- <h3>{{ name }}</h3>
- <div class="append-bottom-default">
+ <h3 class="serverless-function-name">{{ name }}</h3>
+ <div class="append-bottom-default serverless-function-description">
<div v-for="(line, index) in description.split('\n')" :key="index">{{ line }}</div>
</div>
<url :uri="funcUrl" />
@@ -52,5 +90,13 @@ export default {
</p>
</div>
<div v-else><p>No pods loaded at this time.</p></div>
+
+ <area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" />
+ <missing-prometheus
+ v-if="!hasPrometheus || hasPrometheusMissingData"
+ :help-path="helpPath"
+ :clusters-path="clustersPath"
+ :missing-data="hasPrometheusMissingData"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue
index 773d18781fd..4b3bb078eae 100644
--- a/app/assets/javascripts/serverless/components/function_row.vue
+++ b/app/assets/javascripts/serverless/components/function_row.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import Url from './url.vue';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -19,6 +20,10 @@ export default {
return this.func.name;
},
description() {
+ if (!_.isString(this.func.description)) {
+ return '';
+ }
+
const desc = this.func.description.split('\n');
if (desc.length > 1) {
return desc[1];
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index 4bde409f906..f9b4e789563 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,5 +1,6 @@
<script>
-import { GlSkeletonLoading } from '@gitlab/ui';
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
@@ -9,14 +10,9 @@ export default {
EnvironmentRow,
FunctionRow,
EmptyState,
- GlSkeletonLoading,
+ GlLoadingIcon,
},
props: {
- functions: {
- type: Object,
- required: true,
- default: () => ({}),
- },
installed: {
type: Boolean,
required: true,
@@ -29,17 +25,23 @@ export default {
type: String,
required: true,
},
- loadingData: {
- type: Boolean,
- required: false,
- default: true,
- },
- hasFunctionData: {
- type: Boolean,
- required: false,
- default: true,
+ statusPath: {
+ type: String,
+ required: true,
},
},
+ computed: {
+ ...mapState(['isLoading', 'hasFunctionData']),
+ ...mapGetters(['getFunctions']),
+ },
+ created() {
+ this.fetchFunctions({
+ functionsPath: this.statusPath,
+ });
+ },
+ methods: {
+ ...mapActions(['fetchFunctions']),
+ },
};
</script>
@@ -47,14 +49,16 @@ export default {
<section id="serverless-functions">
<div v-if="installed">
<div v-if="hasFunctionData">
- <template v-if="loadingData">
- <div v-for="j in 3" :key="j" class="gl-responsive-table-row"><gl-skeleton-loading /></div>
- </template>
+ <gl-loading-icon
+ v-if="isLoading"
+ :size="2"
+ class="prepend-top-default append-bottom-default"
+ />
<template v-else>
<div class="groups-list-tree-container">
<ul class="content-list group-list-tree">
<environment-row
- v-for="(env, index) in functions"
+ v-for="(env, index) in getFunctions"
:key="index"
:env="env"
:env-name="index"
diff --git a/app/assets/javascripts/serverless/components/missing_prometheus.vue b/app/assets/javascripts/serverless/components/missing_prometheus.vue
new file mode 100644
index 00000000000..6c19434f202
--- /dev/null
+++ b/app/assets/javascripts/serverless/components/missing_prometheus.vue
@@ -0,0 +1,63 @@
+<script>
+import { GlButton, GlLink } from '@gitlab/ui';
+import { s__ } from '../../locale';
+
+export default {
+ components: {
+ GlButton,
+ GlLink,
+ },
+ props: {
+ clustersPath: {
+ type: String,
+ required: true,
+ },
+ helpPath: {
+ type: String,
+ required: true,
+ },
+ missingData: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ missingStateClass() {
+ return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state';
+ },
+ prometheusHelpPath() {
+ return `${this.helpPath}#prometheus-support`;
+ },
+ description() {
+ return this.missingData
+ ? s__(`ServerlessDetails|Invocation metrics loading or not available at this time.`)
+ : s__(
+ `ServerlessDetails|Function invocation metrics require Prometheus to be installed first.`,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row" :class="missingStateClass">
+ <div class="col-12">
+ <div class="text-content">
+ <h4 class="state-title text-left">{{ s__(`ServerlessDetails|Invocations`) }}</h4>
+ <p class="state-description">
+ {{ description }}
+ <gl-link :href="prometheusHelpPath">{{
+ s__(`ServerlessDetails|More information`)
+ }}</gl-link
+ >.
+ </p>
+
+ <div v-if="!missingData" class="text-left">
+ <gl-button :href="clustersPath" variant="success">
+ {{ s__('ServerlessDetails|Install Prometheus') }}
+ </gl-button>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
new file mode 100644
index 00000000000..35f77205f2c
--- /dev/null
+++ b/app/assets/javascripts/serverless/constants.js
@@ -0,0 +1,3 @@
+export const MAX_REQUESTS = 3; // max number of times to retry
+
+export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 47a510d5fb5..2d3f086ffee 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -1,13 +1,7 @@
-import Visibility from 'visibilityjs';
import Vue from 'vue';
-import { s__ } from '../locale';
-import Flash from '../flash';
-import Poll from '../lib/utils/poll';
-import ServerlessStore from './stores/serverless_store';
-import ServerlessDetailsStore from './stores/serverless_details_store';
-import GetFunctionsService from './services/get_functions_service';
import Functions from './components/functions.vue';
import FunctionDetails from './components/function_details.vue';
+import { createStore } from './store';
export default class Serverless {
constructor() {
@@ -19,10 +13,12 @@ export default class Serverless {
serviceUrl,
serviceNamespace,
servicePodcount,
+ serviceMetricsUrl,
+ prometheus,
+ clustersPath,
+ helpPath,
} = document.querySelector('.js-serverless-function-details-page').dataset;
const el = document.querySelector('#js-serverless-function-details');
- this.store = new ServerlessDetailsStore();
- const { store } = this;
const service = {
name: serviceName,
@@ -31,20 +27,19 @@ export default class Serverless {
url: serviceUrl,
namespace: serviceNamespace,
podcount: servicePodcount,
+ metricsUrl: serviceMetricsUrl,
};
- this.store.updateDetailedFunction(service);
this.functionDetails = new Vue({
el,
- data() {
- return {
- state: store.state,
- };
- },
+ store: createStore(),
render(createElement) {
return createElement(FunctionDetails, {
props: {
- func: this.state.functionDetail,
+ func: service,
+ hasPrometheus: prometheus !== undefined,
+ clustersPath,
+ helpPath,
},
});
},
@@ -54,95 +49,27 @@ export default class Serverless {
'.js-serverless-functions-page',
).dataset;
- this.service = new GetFunctionsService(statusPath);
- this.knativeInstalled = installed !== undefined;
- this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath);
- this.initServerless();
- this.functionLoadCount = 0;
-
- if (statusPath && this.knativeInstalled) {
- this.initPolling();
- }
- }
- }
-
- initServerless() {
- const { store } = this;
- const el = document.querySelector('#js-serverless-functions');
-
- this.functions = new Vue({
- el,
- data() {
- return {
- state: store.state,
- };
- },
- render(createElement) {
- return createElement(Functions, {
- props: {
- functions: this.state.functions,
- installed: this.state.installed,
- clustersPath: this.state.clustersPath,
- helpPath: this.state.helpPath,
- loadingData: this.state.loadingData,
- hasFunctionData: this.state.hasFunctionData,
- },
- });
- },
- });
- }
-
- initPolling() {
- this.poll = new Poll({
- resource: this.service,
- method: 'fetchData',
- successCallback: data => this.handleSuccess(data),
- errorCallback: () => Serverless.handleError(),
- });
-
- if (!Visibility.hidden()) {
- this.poll.makeRequest();
- } else {
- this.service
- .fetchData()
- .then(data => this.handleSuccess(data))
- .catch(() => Serverless.handleError());
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden() && !this.destroyed) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- }
-
- handleSuccess(data) {
- if (data.status === 200) {
- this.store.updateFunctionsFromServer(data.data);
- this.store.updateLoadingState(false);
- } else if (data.status === 204) {
- /* Time out after 3 attempts to retrieve data */
- this.functionLoadCount += 1;
- if (this.functionLoadCount === 3) {
- this.poll.stop();
- this.store.toggleNoFunctionData();
- }
+ const el = document.querySelector('#js-serverless-functions');
+ this.functions = new Vue({
+ el,
+ store: createStore(),
+ render(createElement) {
+ return createElement(Functions, {
+ props: {
+ installed: installed !== undefined,
+ clustersPath,
+ helpPath,
+ statusPath,
+ },
+ });
+ },
+ });
}
}
- static handleError() {
- Flash(s__('Serverless|An error occurred while retrieving serverless components'));
- }
-
destroy() {
this.destroyed = true;
- if (this.poll) {
- this.poll.stop();
- }
-
this.functions.$destroy();
this.functionDetails.$destroy();
}
diff --git a/app/assets/javascripts/serverless/services/get_functions_service.js b/app/assets/javascripts/serverless/services/get_functions_service.js
deleted file mode 100644
index 303b42dc66c..00000000000
--- a/app/assets/javascripts/serverless/services/get_functions_service.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-export default class GetFunctionsService {
- constructor(endpoint) {
- this.endpoint = endpoint;
- }
-
- fetchData() {
- return axios.get(this.endpoint);
- }
-}
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
new file mode 100644
index 00000000000..826501c9022
--- /dev/null
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -0,0 +1,113 @@
+import * as types from './mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
+import { MAX_REQUESTS } from '../constants';
+
+export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
+export const receiveFunctionsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
+export const receiveFunctionsNoDataSuccess = ({ commit }) =>
+ commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS);
+export const receiveFunctionsError = ({ commit }, error) =>
+ commit(types.RECEIVE_FUNCTIONS_ERROR, error);
+
+export const receiveMetricsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_METRICS_SUCCESS, data);
+export const receiveMetricsNoPrometheus = ({ commit }) =>
+ commit(types.RECEIVE_METRICS_NO_PROMETHEUS);
+export const receiveMetricsNoDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_METRICS_NODATA_SUCCESS, data);
+export const receiveMetricsError = ({ commit }, error) =>
+ commit(types.RECEIVE_METRICS_ERROR, error);
+
+export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
+ let retryCount = 0;
+
+ dispatch('requestFunctionsLoading');
+
+ backOff((next, stop) => {
+ axios
+ .get(functionsPath)
+ .then(response => {
+ if (response.status === statusCodes.NO_CONTENT) {
+ retryCount += 1;
+ if (retryCount < MAX_REQUESTS) {
+ next();
+ } else {
+ stop(null);
+ }
+ } else {
+ stop(response.data);
+ }
+ })
+ .catch(stop);
+ })
+ .then(data => {
+ if (data !== null) {
+ dispatch('receiveFunctionsSuccess', data);
+ } else {
+ dispatch('receiveFunctionsNoDataSuccess');
+ }
+ })
+ .catch(error => {
+ dispatch('receiveFunctionsError', error);
+ createFlash(error);
+ });
+};
+
+export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => {
+ let retryCount = 0;
+
+ if (!hasPrometheus) {
+ dispatch('receiveMetricsNoPrometheus');
+ return;
+ }
+
+ backOff((next, stop) => {
+ axios
+ .get(metricsPath)
+ .then(response => {
+ if (response.status === statusCodes.NO_CONTENT) {
+ retryCount += 1;
+ if (retryCount < MAX_REQUESTS) {
+ next();
+ } else {
+ dispatch('receiveMetricsNoDataSuccess');
+ stop(null);
+ }
+ } else {
+ stop(response.data);
+ }
+ })
+ .catch(stop);
+ })
+ .then(data => {
+ if (data === null) {
+ return;
+ }
+
+ const updatedMetric = data.metrics;
+ const queries = data.metrics.queries.map(query => ({
+ ...query,
+ result: query.result.map(result => ({
+ ...result,
+ values: result.values.map(([timestamp, value]) => ({
+ time: new Date(timestamp * 1000).toISOString(),
+ value: Number(value),
+ })),
+ })),
+ }));
+
+ updatedMetric.queries = queries;
+ dispatch('receiveMetricsSuccess', updatedMetric);
+ })
+ .catch(error => {
+ dispatch('receiveMetricsError', error);
+ createFlash(error);
+ });
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/serverless/store/getters.js b/app/assets/javascripts/serverless/store/getters.js
new file mode 100644
index 00000000000..071f663d9d2
--- /dev/null
+++ b/app/assets/javascripts/serverless/store/getters.js
@@ -0,0 +1,10 @@
+import { translate } from '../utils';
+
+export const hasPrometheusMissingData = state => state.hasPrometheus && !state.hasPrometheusData;
+
+// Convert the function list into a k/v grouping based on the environment scope
+
+export const getFunctions = state => translate(state.functions);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/serverless/store/index.js b/app/assets/javascripts/serverless/store/index.js
new file mode 100644
index 00000000000..5f72060633e
--- /dev/null
+++ b/app/assets/javascripts/serverless/store/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import createState from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: createState(),
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
new file mode 100644
index 00000000000..25b2f7ac38a
--- /dev/null
+++ b/app/assets/javascripts/serverless/store/mutation_types.js
@@ -0,0 +1,9 @@
+export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
+export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
+export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
+export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
+
+export const RECEIVE_METRICS_NO_PROMETHEUS = 'RECEIVE_METRICS_NO_PROMETHEUS';
+export const RECEIVE_METRICS_SUCCESS = 'RECEIVE_METRICS_SUCCESS';
+export const RECEIVE_METRICS_NODATA_SUCCESS = 'RECEIVE_METRICS_NODATA_SUCCESS';
+export const RECEIVE_METRICS_ERROR = 'RECEIVE_METRICS_ERROR';
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
new file mode 100644
index 00000000000..991f32a275d
--- /dev/null
+++ b/app/assets/javascripts/serverless/store/mutations.js
@@ -0,0 +1,38 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_FUNCTIONS_LOADING](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
+ state.functions = data;
+ state.isLoading = false;
+ state.hasFunctionData = true;
+ },
+ [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) {
+ state.isLoading = false;
+ state.hasFunctionData = false;
+ },
+ [types.RECEIVE_FUNCTIONS_ERROR](state, error) {
+ state.error = error;
+ state.hasFunctionData = false;
+ state.isLoading = false;
+ },
+ [types.RECEIVE_METRICS_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.hasPrometheusData = true;
+ state.graphData = data;
+ },
+ [types.RECEIVE_METRICS_NODATA_SUCCESS](state) {
+ state.isLoading = false;
+ state.hasPrometheusData = false;
+ },
+ [types.RECEIVE_METRICS_ERROR](state, error) {
+ state.hasPrometheusData = false;
+ state.error = error;
+ },
+ [types.RECEIVE_METRICS_NO_PROMETHEUS](state) {
+ state.hasPrometheusData = false;
+ state.hasPrometheus = false;
+ },
+};
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
new file mode 100644
index 00000000000..afc3f37d7ba
--- /dev/null
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -0,0 +1,13 @@
+export default () => ({
+ error: null,
+ isLoading: true,
+
+ // functions
+ functions: [],
+ hasFunctionData: true,
+
+ // function_details
+ hasPrometheus: true,
+ hasPrometheusData: false,
+ graphData: {},
+});
diff --git a/app/assets/javascripts/serverless/stores/serverless_details_store.js b/app/assets/javascripts/serverless/stores/serverless_details_store.js
deleted file mode 100644
index 5394d2cded1..00000000000
--- a/app/assets/javascripts/serverless/stores/serverless_details_store.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export default class ServerlessDetailsStore {
- constructor() {
- this.state = {
- functionDetail: {},
- };
- }
-
- updateDetailedFunction(func) {
- this.state.functionDetail = func;
- }
-}
diff --git a/app/assets/javascripts/serverless/stores/serverless_store.js b/app/assets/javascripts/serverless/stores/serverless_store.js
deleted file mode 100644
index 816d55a03f9..00000000000
--- a/app/assets/javascripts/serverless/stores/serverless_store.js
+++ /dev/null
@@ -1,29 +0,0 @@
-export default class ServerlessStore {
- constructor(knativeInstalled = false, clustersPath, helpPath) {
- this.state = {
- functions: {},
- hasFunctionData: true,
- loadingData: true,
- installed: knativeInstalled,
- clustersPath,
- helpPath,
- };
- }
-
- updateFunctionsFromServer(upstreamFunctions = []) {
- this.state.functions = upstreamFunctions.reduce((rv, func) => {
- const envs = rv;
- envs[func.environment_scope] = (rv[func.environment_scope] || []).concat([func]);
-
- return envs;
- }, {});
- }
-
- updateLoadingState(loadingData) {
- this.state.loadingData = loadingData;
- }
-
- toggleNoFunctionData() {
- this.state.hasFunctionData = false;
- }
-}
diff --git a/app/assets/javascripts/serverless/utils.js b/app/assets/javascripts/serverless/utils.js
new file mode 100644
index 00000000000..8b9e96ce9aa
--- /dev/null
+++ b/app/assets/javascripts/serverless/utils.js
@@ -0,0 +1,23 @@
+// Validate that the object coming in has valid query details and results
+export const validateGraphData = data =>
+ data.queries &&
+ Array.isArray(data.queries) &&
+ data.queries.filter(query => {
+ if (Array.isArray(query.result)) {
+ return query.result.filter(res => Array.isArray(res.values)).length === query.result.length;
+ }
+
+ return false;
+ }).length === data.queries.length;
+
+export const translate = functions =>
+ functions.reduce(
+ (acc, func) =>
+ Object.assign(acc, {
+ [func.environment_scope]: (acc[func.environment_scope] || []).concat([func]),
+ }),
+ {},
+ );
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 706e6ca19c3..57125c78cf6 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -50,6 +50,9 @@ export default {
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
},
+ buttonTooltip() {
+ return !this.collapsed ? undefined : this.buttonLabel;
+ },
collapsedButtonIconClasses() {
return this.isTodo ? 'todo-undone' : '';
},
@@ -69,7 +72,7 @@ export default {
<button
v-tooltip
:class="buttonClasses"
- :title="buttonLabel"
+ :title="buttonTooltip"
:aria-label="buttonLabel"
:data-issuable-id="issuableId"
:data-issuable-type="issuableType"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 50ab7ead582..361441640e1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -109,29 +109,31 @@ export default {
></div>
</div>
- <div v-if="mr.isOpen" class="branch-actions d-flex">
- <a
- v-if="!mr.sourceBranchRemoved"
- v-tooltip
- :href="webIdePath"
- :title="ideButtonTitle"
- :class="{ disabled: !mr.canPushToSourceBranch }"
- class="btn btn-default js-web-ide d-none d-md-inline-block append-right-8"
- data-placement="bottom"
- tabindex="0"
- role="button"
- >
- {{ s__('mrWidget|Open in Web IDE') }}
- </a>
- <button
- :disabled="mr.sourceBranchRemoved"
- data-target="#modal_merge_info"
- data-toggle="modal"
- class="btn btn-default js-check-out-branch append-right-default"
- type="button"
- >
- {{ s__('mrWidget|Check out branch') }}
- </button>
+ <div class="branch-actions d-flex">
+ <template v-if="mr.isOpen">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ v-tooltip
+ :href="webIdePath"
+ :title="ideButtonTitle"
+ :class="{ disabled: !mr.canPushToSourceBranch }"
+ class="btn btn-default js-web-ide d-none d-md-inline-block append-right-8"
+ data-placement="bottom"
+ tabindex="0"
+ role="button"
+ >
+ {{ s__('mrWidget|Open in Web IDE') }}
+ </a>
+ <button
+ :disabled="mr.sourceBranchRemoved"
+ data-target="#modal_merge_info"
+ data-toggle="modal"
+ class="btn btn-default js-check-out-branch append-right-default"
+ type="button"
+ >
+ {{ s__('mrWidget|Check out branch') }}
+ </button>
+ </template>
<span class="dropdown">
<button
type="button"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 780ced4d382..392eb6fb425 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -33,7 +33,7 @@ export default {
</script>
<template>
<div class="space-children d-flex append-right-10 widget-status-icon">
- <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon /></div>
+ <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="md" /></div>
<ci-icon v-else :status="statusObj" :size="24" />
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
index f085ef35ccc..b25aebd7c98 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue
@@ -102,7 +102,7 @@ export default {
:style="{
width: onionMaxPixelWidth,
height: onionMaxPixelHeight,
- 'user-select': dragging === true ? 'none' : '',
+ 'user-select': dragging ? 'none' : null,
}"
class="onion-skin-frame"
>
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
index 1c970b72a66..eddafc759a2 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -68,12 +68,10 @@ export default {
},
startDrag() {
this.dragging = true;
- document.body.style.userSelect = 'none';
document.body.addEventListener('mousemove', this.dragMove);
},
stopDrag() {
this.dragging = false;
- document.body.style.userSelect = '';
document.body.removeEventListener('mousemove', this.dragMove);
},
prepareSwipe() {
@@ -104,7 +102,13 @@ export default {
<template>
<div class="swipe view">
- <div ref="swipeFrame" class="swipe-frame">
+ <div
+ ref="swipeFrame"
+ :style="{
+ 'user-select': dragging ? 'none' : null,
+ }"
+ class="swipe-frame"
+ >
<image-viewer
key="swipeOldImg"
ref="swipeOldImg"
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 0cbcdbf2eb4..1bfa91500cb 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -39,7 +39,7 @@ export default {
},
data() {
return {
- mouseOver: false,
+ dropdownOpen: false,
};
},
computed: {
@@ -123,8 +123,8 @@ export default {
return this.$router.currentRoute.path === `/project${this.file.url}`;
},
- toggleHover(over) {
- this.mouseOver = over;
+ toggleDropdown(val) {
+ this.dropdownOpen = val;
},
},
};
@@ -140,8 +140,7 @@ export default {
class="file-row"
role="button"
@click="clickFile"
- @mouseover="toggleHover(true)"
- @mouseout="toggleHover(false)"
+ @mouseleave="toggleDropdown(false)"
>
<div class="file-row-name-container">
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
@@ -160,7 +159,8 @@ export default {
:is="extraComponent"
v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
:file="file"
- :mouse-over="mouseOver"
+ :dropdown-open="dropdownOpen"
+ @toggle="toggleDropdown($event)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 27cfa8abb24..d4d18614f93 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,15 +1,17 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
-import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
+import { sprintf } from '~/locale';
+import IssueMilestone from '../../components/issue/issue_milestone.vue';
+import IssueAssignees from '../../components/issue/issue_assignees.vue';
+import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
+import CiIcon from '../ci_icon.vue';
export default {
name: 'IssueItem',
components: {
IssueMilestone,
IssueAssignees,
+ CiIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -27,9 +29,9 @@ export default {
return sprintf(
'<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
{
- state: this.isOpen ? __('Opened') : __('Closed'),
- timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords,
- timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp,
+ state: this.stateText,
+ timeInWords: this.stateTimeInWords,
+ timestamp: this.stateTimestamp,
},
);
},
@@ -84,6 +86,11 @@ export default {
{{ pathIdSeparator }}{{ itemId }}
</div>
<div class="item-meta-child d-flex align-items-center">
+ <span v-if="hasPipeline" class="mr-ci-status pr-2">
+ <a :href="pipelineStatus.details_path">
+ <ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
+ </a>
+ </span>
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
new file mode 100644
index 00000000000..d1aba99ac22
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js
@@ -0,0 +1,20 @@
+/* eslint-disable import/prefer-default-export */
+
+function trimFirstCharOfLineContent(text) {
+ if (!text) {
+ return text;
+ }
+
+ return text.replace(/^( |\+|-)/, '');
+}
+
+function cleanSuggestionLine(line = {}) {
+ return {
+ ...line,
+ text: trimFirstCharOfLineContent(line.text),
+ };
+}
+
+export function selectDiffLines(lines) {
+ return lines.filter(line => line.type !== 'match').map(line => cleanSuggestionLine(line));
+}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index eccf73e227c..0f3b3568414 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -76,6 +76,7 @@ export default {
hasSuggestion: false,
markdownPreviewLoading: false,
previewMarkdown: false,
+ suggestions: this.note.suggestions || [],
};
},
computed: {
@@ -109,9 +110,6 @@ export default {
}
return lineNumber;
},
- suggestions() {
- return this.note.suggestions || [];
- },
lineType() {
return this.line ? this.line.type : '';
},
@@ -175,6 +173,7 @@ export default {
this.referencedCommands = data.references.commands;
this.referencedUsers = data.references.users;
this.hasSuggestion = data.references.suggestions && data.references.suggestions.length;
+ this.suggestions = data.references.suggestions;
}
this.$nextTick()
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index cc6ecdb0395..a5a5b2ef415 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -38,7 +38,7 @@ export default {
].join('\n');
},
mdSuggestion() {
- return ['```suggestion', `{text}`, '```'].join('\n');
+ return ['```suggestion:-0+0', `{text}`, '```'].join('\n');
},
},
mounted() {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index a351ca62c94..2eb4ec12a4a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -1,24 +1,14 @@
<script>
import SuggestionDiffHeader from './suggestion_diff_header.vue';
+import SuggestionDiffRow from './suggestion_diff_row.vue';
+import { selectDiffLines } from '../lib/utils/diff_utils';
export default {
components: {
SuggestionDiffHeader,
+ SuggestionDiffRow,
},
props: {
- newLines: {
- type: Array,
- required: true,
- },
- fromContent: {
- type: String,
- required: false,
- default: '',
- },
- fromLine: {
- type: Number,
- required: true,
- },
suggestion: {
type: Object,
required: true,
@@ -33,6 +23,11 @@ export default {
required: true,
},
},
+ computed: {
+ lines() {
+ return selectDiffLines(this.suggestion.diff_lines);
+ },
+ },
methods: {
applySuggestion(callback) {
this.$emit('apply', { suggestionId: this.suggestion.id, callback });
@@ -52,22 +47,11 @@ export default {
/>
<table class="mb-3 md-suggestion-diff js-syntax-highlight code">
<tbody>
- <!-- Old Line -->
- <tr class="line_holder old">
- <td class="diff-line-num old_line qa-old-diff-line-number old">{{ fromLine }}</td>
- <td class="diff-line-num new_line old"></td>
- <td class="line_content old">
- <span>{{ fromContent }}</span>
- </td>
- </tr>
- <!-- New Line(s) -->
- <tr v-for="(line, key) of newLines" :key="key" class="line_holder new">
- <td class="diff-line-num old_line new"></td>
- <td class="diff-line-num new_line qa-new-diff-line-number new">{{ line.lineNumber }}</td>
- <td class="line_content new">
- <span>{{ line.content }}</span>
- </td>
- </tr>
+ <suggestion-diff-row
+ v-for="(line, index) of lines"
+ :key="`${index}-${line.text}`"
+ :line="line"
+ />
</tbody>
</table>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
new file mode 100644
index 00000000000..cafd3a515ea
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -0,0 +1,32 @@
+<script>
+export default {
+ name: 'SuggestionDiffRow',
+ props: {
+ line: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ lineType() {
+ return this.line.type;
+ },
+ },
+};
+</script>
+
+<template>
+ <tr class="line_holder" :class="lineType">
+ <td class="diff-line-num old_line" :class="lineType">
+ {{ line.old_line }}
+ </td>
+ <td class="diff-line-num new_line" :class="lineType">
+ {{ line.new_line }}
+ </td>
+ <td class="line_content" :class="lineType">
+ <span v-if="line.text">{{ line.text }}</span>
+ <!-- TODO: replace this hack with zero-width whitespace when we have rich_text from BE -->
+ <span v-else>&#8203;</span>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 177d78cb904..8d3705e1e4a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -6,16 +6,6 @@ import Flash from '~/flash';
export default {
components: { SuggestionDiff },
props: {
- fromLine: {
- type: Number,
- required: false,
- default: 0,
- },
- fromContent: {
- type: String,
- required: false,
- default: '',
- },
lineType: {
type: String,
required: false,
@@ -71,41 +61,19 @@ export default {
suggestionElements.forEach((suggestionEl, i) => {
const suggestionParentEl = suggestionEl.parentElement;
- const newLines = this.extractNewLines(suggestionParentEl);
- const diffComponent = this.generateDiff(newLines, i);
+ const diffComponent = this.generateDiff(i);
diffComponent.$mount(suggestionParentEl);
});
this.isRendered = true;
},
- extractNewLines(suggestionEl) {
- // extracts the suggested lines from the markdown
- // calculates a line number for each line
-
- const newLines = suggestionEl.querySelectorAll('.line');
- const fromLine = this.suggestions.length ? this.suggestions[0].from_line : this.fromLine;
- const lines = [];
-
- newLines.forEach((line, i) => {
- const content = `${line.innerText}\n`;
- const lineNumber = fromLine + i;
- lines.push({ content, lineNumber });
- });
-
- return lines;
- },
- generateDiff(newLines, suggestionIndex) {
- // generates the diff <suggestion-diff /> component
- // all `suggestion` markdown will be swapped out by this component
-
+ generateDiff(suggestionIndex) {
const { suggestions, disabled, helpPagePath } = this;
const suggestion =
suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {};
- const fromContent = suggestion.from_content || this.fromContent;
- const fromLine = suggestion.from_line || this.fromLine;
const SuggestionDiffComponent = Vue.extend(SuggestionDiff);
const suggestionDiff = new SuggestionDiffComponent({
- propsData: { newLines, fromLine, fromContent, disabled, suggestion, helpPagePath },
+ propsData: { disabled, suggestion, helpPagePath },
});
suggestionDiff.$on('apply', ({ suggestionId, callback }) => {
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
new file mode 100644
index 00000000000..071bae7f665
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
+import highlight from '~/lib/utils/highlight';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+import _ from 'underscore';
+
+export default {
+ name: 'ProjectListItem',
+ components: {
+ Icon,
+ ProjectAvatar,
+ GlButton,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ validator: p => _.isFinite(p.id) && _.isString(p.name) && _.isString(p.name_with_namespace),
+ },
+ selected: {
+ type: Boolean,
+ required: true,
+ },
+ matcher: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ truncatedNamespace() {
+ return truncateNamespace(this.project.name_with_namespace);
+ },
+ highlightedProjectName() {
+ return highlight(this.project.name, this.matcher);
+ },
+ },
+ methods: {
+ onClick() {
+ this.$emit('click');
+ },
+ },
+};
+</script>
+<template>
+ <gl-button
+ class="d-flex align-items-center btn pt-1 pb-1 border-0 project-list-item"
+ @click="onClick"
+ >
+ <icon
+ class="prepend-left-10 append-right-10 flex-shrink-0 position-top-0 js-selected-icon"
+ :class="{ 'js-selected visible': selected, 'js-unselected invisible': !selected }"
+ name="mobile-issue-close"
+ />
+ <project-avatar class="flex-shrink-0 js-project-avatar" :project="project" :size="32" />
+ <div class="d-flex flex-wrap project-namespace-name-container">
+ <div
+ v-if="truncatedNamespace"
+ :title="project.name_with_namespace"
+ class="text-secondary text-truncate js-project-namespace"
+ >
+ {{ truncatedNamespace }}
+ <span v-if="truncatedNamespace" class="text-secondary">/&nbsp;</span>
+ </div>
+ <div
+ :title="project.name"
+ class="js-project-name text-truncate"
+ v-html="highlightedProjectName"
+ ></div>
+ </div>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
new file mode 100644
index 00000000000..596fd48f96a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -0,0 +1,103 @@
+<script>
+import _ from 'underscore';
+import { GlLoadingIcon } from '@gitlab/ui';
+import ProjectListItem from './project_list_item.vue';
+
+const SEARCH_INPUT_TIMEOUT_MS = 500;
+
+export default {
+ name: 'ProjectSelector',
+ components: {
+ GlLoadingIcon,
+ ProjectListItem,
+ },
+ props: {
+ projectSearchResults: {
+ type: Array,
+ required: true,
+ },
+ selectedProjects: {
+ type: Array,
+ required: true,
+ },
+ showNoResultsMessage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showMinimumSearchQueryMessage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showLoadingIndicator: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showSearchErrorMessage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ searchQuery: '',
+ };
+ },
+ methods: {
+ projectClicked(project) {
+ this.$emit('projectClicked', project);
+ },
+ isSelected(project) {
+ return Boolean(_.findWhere(this.selectedProjects, { id: project.id }));
+ },
+ focusSearchInput() {
+ this.$refs.searchInput.focus();
+ },
+ onInput: _.debounce(function debouncedOnInput() {
+ this.$emit('searched', this.searchQuery);
+ }, SEARCH_INPUT_TIMEOUT_MS),
+ },
+};
+</script>
+<template>
+ <div>
+ <input
+ ref="searchInput"
+ v-model="searchQuery"
+ :placeholder="__('Search your projects')"
+ type="search"
+ class="form-control mb-3 js-project-selector-input"
+ autofocus
+ @input="onInput"
+ />
+ <div class="d-flex flex-column">
+ <gl-loading-icon v-if="showLoadingIndicator" :size="2" class="py-2 px-4" />
+ <div v-if="!showLoadingIndicator" class="d-flex flex-column">
+ <project-list-item
+ v-for="project in projectSearchResults"
+ :key="project.id"
+ :selected="isSelected(project)"
+ :project="project"
+ :matcher="searchQuery"
+ class="js-project-list-item"
+ @click="projectClicked(project)"
+ />
+ </div>
+ <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message">
+ {{ __('Sorry, no projects matched your search') }}
+ </div>
+ <div
+ v-if="showMinimumSearchQueryMessage"
+ class="text-muted ml-2 js-minimum-search-query-message"
+ >
+ {{ __('Enter at least three characters to search') }}
+ </div>
+ <div v-if="showSearchErrorMessage" class="text-danger ml-2 js-search-error-message">
+ {{ __('Something went wrong, unable to search projects') }}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue
new file mode 100644
index 00000000000..1f3d248e991
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/resizable_chart/resizable_chart_container.vue
@@ -0,0 +1,40 @@
+<script>
+import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
+import $ from 'jquery';
+
+export default {
+ data() {
+ return {
+ width: 0,
+ height: 0,
+ };
+ },
+ beforeDestroy() {
+ this.contentResizeHandler.off('content.resize', this.debouncedResize);
+ window.removeEventListener('resize', this.debouncedResize);
+ },
+ created() {
+ this.debouncedResize = debounceByAnimationFrame(this.onResize);
+
+ // Handle when we explicictly trigger a custom resize event
+ this.contentResizeHandler = $(document).on('content.resize', this.debouncedResize);
+
+ // Handle window resize
+ window.addEventListener('resize', this.debouncedResize);
+ },
+ methods: {
+ onResize() {
+ // Slot dimensions
+ const { clientWidth, clientHeight } = this.$refs.chartWrapper;
+ this.width = clientWidth;
+ this.height = clientHeight;
+ },
+ },
+};
+</script>
+
+<template>
+ <div ref="chartWrapper">
+ <slot :width="width" :height="height"> </slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index f66e81b1e08..9c258c4651f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -75,6 +75,16 @@ export default {
required: false,
default: false,
},
+ enableScopedLabels: {
+ type: Boolean,
+ require: false,
+ default: false,
+ },
+ scopedLabelsDocumentationLink: {
+ type: String,
+ require: false,
+ default: '#',
+ },
},
computed: {
hiddenInputName() {
@@ -123,7 +133,12 @@ export default {
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title :can-edit="canEdit" />
- <dropdown-value :labels="context.labels" :label-filter-base-path="labelFilterBasePath">
+ <dropdown-value
+ :labels="context.labels"
+ :label-filter-base-path="labelFilterBasePath"
+ :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
+ :enable-scoped-labels="enableScopedLabels"
+ >
<slot></slot>
</dropdown-value>
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none;">
@@ -142,6 +157,8 @@ export default {
:namespace="namespace"
:labels="context.labels"
:show-extra-options="!showCreate"
+ :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
+ :enable-scoped-labels="enableScopedLabels"
/>
<div
class="dropdown-menu dropdown-select dropdown-menu-paging
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index 498b507d11d..1eed8907bb7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -31,6 +31,16 @@ export default {
type: Boolean,
required: true,
},
+ enableScopedLabels: {
+ type: Boolean,
+ require: false,
+ default: false,
+ },
+ scopedLabelsDocumentationLink: {
+ type: String,
+ require: false,
+ default: '#',
+ },
},
computed: {
dropdownToggleText() {
@@ -61,6 +71,8 @@ export default {
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
+ :data-scoped-labels="enableScopedLabels"
+ :data-scoped-labels-documentation-link="scopedLabelsDocumentationLink"
type="button"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
data-toggle="dropdown"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
index 6faf3fafad1..ddc488adbcb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -1,9 +1,11 @@
<script>
-import tooltip from '~/vue_shared/directives/tooltip';
+import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue';
+import DropdownValueRegularLabel from './dropdown_value_regular_label.vue';
export default {
- directives: {
- tooltip,
+ components: {
+ DropdownValueScopedLabel,
+ DropdownValueRegularLabel,
},
props: {
labels: {
@@ -14,6 +16,16 @@ export default {
type: String,
required: true,
},
+ enableScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ scopedLabelsDocumentationLink: {
+ type: String,
+ required: false,
+ default: '#',
+ },
},
computed: {
isEmpty() {
@@ -30,6 +42,12 @@ export default {
backgroundColor: label.color,
};
},
+ scopedLabelsDescription({ description = '' }) {
+ return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`;
+ },
+ showScopedLabels({ title = '' }) {
+ return this.enableScopedLabels && title.indexOf('::') !== -1;
+ },
},
};
</script>
@@ -44,17 +62,24 @@ export default {
<span v-if="isEmpty" class="text-secondary">
<slot>{{ __('None') }}</slot>
</span>
- <a v-for="label in labels" v-else :key="label.id" :href="labelFilterUrl(label)">
- <span
- v-tooltip
- :style="labelStyle(label)"
- :title="label.description"
- class="badge color-label"
- data-placement="bottom"
- data-container="body"
- >
- {{ label.title }}
- </span>
- </a>
+
+ <template v-for="label in labels" v-else>
+ <dropdown-value-scoped-label
+ v-if="showScopedLabels(label)"
+ :key="label.id"
+ :label="label"
+ :label-filter-url="labelFilterUrl(label)"
+ :label-style="labelStyle(label)"
+ :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
+ />
+
+ <dropdown-value-regular-label
+ v-else
+ :key="label.id"
+ :label="label"
+ :label-filter-url="labelFilterUrl(label)"
+ :label-style="labelStyle(label)"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
new file mode 100644
index 00000000000..282b181f11e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlLink, GlTooltip } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTooltip,
+ GlLink,
+ },
+ props: {
+ label: {
+ type: Object,
+ required: true,
+ },
+ labelStyle: {
+ type: Object,
+ required: true,
+ },
+ labelFilterUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <a ref="regularLabelRef" :href="labelFilterUrl">
+ <span :style="labelStyle" class="badge color-label">
+ {{ label.title }}
+ </span>
+ <gl-tooltip :target="() => $refs.regularLabelRef" placement="top" boundary="viewport">
+ {{ label.description }}
+ </gl-tooltip>
+ </a>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
new file mode 100644
index 00000000000..ad5a86de166
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlLink, GlTooltip } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTooltip,
+ GlLink,
+ },
+ props: {
+ label: {
+ type: Object,
+ required: true,
+ },
+ labelStyle: {
+ type: Object,
+ required: true,
+ },
+ scopedLabelsDocumentationLink: {
+ type: String,
+ required: true,
+ },
+ labelFilterUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="d-inline-block position-relative scoped-label-wrapper">
+ <a :href="labelFilterUrl">
+ <span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label">
+ {{ label.title }}
+ </span>
+ <gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport">
+ <span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
+ ><br />
+ {{ label.description }}
+ </gl-tooltip>
+ </a>
+
+ <gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label"
+ ><i class="fa fa-question-circle" :style="labelStyle"></i
+ ></gl-link>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
index 455ae832234..8e0e4baa75a 100644
--- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
@@ -58,6 +59,11 @@ const mixins = {
required: false,
default: '',
},
+ mergedAt: {
+ type: String,
+ required: false,
+ default: '',
+ },
milestone: {
type: Object,
required: false,
@@ -83,6 +89,16 @@ const mixins = {
required: false,
default: false,
},
+ isMergeRequest: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ pipelineStatus: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
components: {
icon,
@@ -95,12 +111,18 @@ const mixins = {
hasState() {
return this.state && this.state.length > 0;
},
+ hasPipeline() {
+ return this.isMergeRequest && this.pipelineStatus && Object.keys(this.pipelineStatus).length;
+ },
isOpen() {
return this.state === 'opened';
},
isClosed() {
return this.state === 'closed';
},
+ isMerged() {
+ return this.state === 'merged';
+ },
hasTitle() {
return this.title.length > 0;
},
@@ -108,9 +130,17 @@ const mixins = {
return !_.isEmpty(this.milestone);
},
iconName() {
+ if (this.isMergeRequest && this.isMerged) {
+ return 'merge';
+ }
+
return this.isOpen ? 'issue-open-m' : 'issue-close';
},
iconClass() {
+ if (this.isMergeRequest && this.isClosed) {
+ return 'merge-request-status closed issue-token-state-icon-closed';
+ }
+
return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
},
computedLinkElementType() {
@@ -131,12 +161,44 @@ const mixins = {
createdAtTimestamp() {
return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
},
+ mergedAtTimestamp() {
+ return this.mergedAt ? formatDate(new Date(this.mergedAt)) : '';
+ },
+ mergedAtInWords() {
+ return this.mergedAt ? this.timeFormated(this.mergedAt) : '';
+ },
closedAtInWords() {
return this.closedAt ? this.timeFormated(this.closedAt) : '';
},
closedAtTimestamp() {
return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
},
+ stateText() {
+ if (this.isMerged) {
+ return __('Merged');
+ }
+
+ return this.isOpen ? __('Opened') : __('Closed');
+ },
+ stateTimeInWords() {
+ if (this.isMerged) {
+ return this.mergedAtInWords;
+ }
+
+ return this.isOpen ? this.createdAtInWords : this.closedAtInWords;
+ },
+ stateTimestamp() {
+ if (this.isMerged) {
+ return this.mergedAtTimestamp;
+ }
+
+ return this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp;
+ },
+ pipelineStatusTooltip() {
+ return this.hasPipeline
+ ? sprintf(__('Pipeline: %{status}'), { status: this.pipelineStatus.label })
+ : '';
+ },
},
methods: {
onRemoveRequest() {
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 86189143525..8aaa9772715 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -6,40 +6,29 @@
*= require cropper.css
*/
-/*
- * Welcome to GitLab css!
- * If you need to add or modify UI component that is common for many pages
- * like a table or typography then make changes in the framework/ directory.
- * If you need to add unique style that should affect only one page - use pages/
- * directory.
- */
-
+// Welcome to GitLab css!
+// If you need to add or modify UI component that is common for many pages
+// like a table or typography then make changes in the framework/ directory.
+// If you need to add unique style that should affect only one page - use pages/
+// directory.
@import "../../../node_modules/at.js/dist/css/jquery.atwho";
@import "../../../node_modules/pikaday/scss/pikaday";
@import "../../../node_modules/dropzone/dist/basic";
@import "../../../node_modules/select2/select2";
-/*
- * GitLab UI framework
- */
+// GitLab UI framework
@import "framework";
-/*
- * Font icons
- */
+// Font icons
@import "font-awesome";
-/*
- * Page specific styles (issues, projects etc):
- */
+// Page specific styles (issues, projects etc):
@import "pages/**/*";
-/*
- * Component specific styles, will be moved to gitlab-ui
- */
+// Component specific styles, will be moved to gitlab-ui
@import "components/**/*";
-/*
- * Styles for JS behaviors.
- */
+// Styles for JS behaviors.
@import "behaviors";
+
+@import "utilities";
diff --git a/app/assets/stylesheets/components/project_list_item.scss b/app/assets/stylesheets/components/project_list_item.scss
new file mode 100644
index 00000000000..8e7c2c4398c
--- /dev/null
+++ b/app/assets/stylesheets/components/project_list_item.scss
@@ -0,0 +1,24 @@
+.project-list-item {
+ &:not(:disabled):not(.disabled) {
+ &:focus,
+ &:active,
+ &:focus:active {
+ outline: none;
+ box-shadow: none;
+ }
+ }
+}
+
+// When housed inside a modal, the edge of each item
+// should extend to the edge of the modal.
+.modal-body {
+ .project-list-item {
+ border-radius: 0;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ .project-namespace-name-container {
+ overflow: hidden;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 0d8e4afa76f..643b20c56bc 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -28,6 +28,10 @@
background-color: $red-100;
border-color: $red-200;
color: $red-700;
+
+ a {
+ color: $red-700;
+ }
}
.bs-callout-warning {
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
index 7207e5119ce..d9b0e4558ad 100644
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -66,6 +66,7 @@
}
}
+.ci-variable-masked-item,
.ci-variable-protected-item {
flex: 0 1 auto;
display: flex;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index d72597a6147..db6c107210e 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -428,6 +428,7 @@ img.emoji {
.mw-460 { max-width: 460px; }
.mw-6em { max-width: 6em; }
+.mw-70p { max-width: 70%; }
.min-height-0 { min-height: 0; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b90db135b4a..efcd35a2e0e 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -287,7 +287,7 @@
list-style: none;
padding: 0 1px;
- a,
+ a:not(.btn),
button,
.menu-item {
@include dropdown-link;
@@ -351,6 +351,10 @@
// Expects up to 3 digits on the badge
margin-right: 40px;
}
+
+ .dropdown-menu-content {
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
+ }
}
.droplab-dropdown {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 8d38310e8e6..53d3645cd63 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -331,6 +331,10 @@ span.idiff {
padding: 5px $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
+
+ &.is-stuck {
+ border-radius: 0;
+ }
}
.file-header-content {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index c36c15a85be..1c23c14c2de 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -204,8 +204,10 @@ label {
margin-top: #{$grid-size / 2};
}
-.gl-field-error {
+.gl-field-error,
+.invalid-feedback {
color: $red-500;
+ font-size: $gl-font-size;
}
.gl-show-field-errors {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7d9781ffb87..da1f196afdb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -110,6 +110,84 @@ $gray-800: #4f4f4f;
$gray-900: #2e2e2e;
$gray-950: #1f1f1f;
+$greens: (
+ '50': $green-50,
+ '100': $green-100,
+ '200': $green-200,
+ '300': $green-300,
+ '400': $green-400,
+ '500': $green-500,
+ '600': $green-600,
+ '700': $green-700,
+ '800': $green-800,
+ '900': $green-900,
+ '950': $green-950
+);
+
+$blues: (
+ '50': $blue-50,
+ '100': $blue-100,
+ '200': $blue-200,
+ '300': $blue-300,
+ '400': $blue-400,
+ '500': $blue-500,
+ '600': $blue-600,
+ '700': $blue-700,
+ '800': $blue-800,
+ '900': $blue-900,
+ '950': $blue-950
+);
+
+$oranges: (
+ '50': $orange-50,
+ '100': $orange-100,
+ '200': $orange-200,
+ '300': $orange-300,
+ '400': $orange-400,
+ '500': $orange-500,
+ '600': $orange-600,
+ '700': $orange-700,
+ '800': $orange-800,
+ '900': $orange-900,
+ '950': $orange-950
+);
+
+$reds: (
+ '50': $red-50,
+ '100': $red-100,
+ '200': $red-200,
+ '300': $red-300,
+ '400': $red-400,
+ '500': $red-500,
+ '600': $red-600,
+ '700': $red-700,
+ '800': $red-800,
+ '900': $red-900,
+ '950': $red-950
+);
+
+$grays: (
+ '50': $gray-50,
+ '100': $gray-100,
+ '200': $gray-200,
+ '300': $gray-300,
+ '400': $gray-400,
+ '500': $gray-500,
+ '600': $gray-600,
+ '700': $gray-700,
+ '800': $gray-800,
+ '900': $gray-900,
+ '950': $gray-950
+);
+
+$color-ranges: (
+ 'primary': $blues,
+ 'secondary': $grays,
+ 'success': $greens,
+ 'warning': $oranges,
+ 'danger': $reds
+);
+
// GitLab themes
$indigo-50: #f7f7ff;
@@ -219,6 +297,15 @@ $gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c;
$gl-header-color: #4c4e54;
+$type-scale: (
+ 1: 12px,
+ 2: 14px,
+ 3: 16px,
+ 4: 20px,
+ 5: 28px,
+ 6: 42px
+);
+
/*
* Lists
*/
@@ -575,6 +662,11 @@ $feature-toggle-text-color: #fff;
$feature-toggle-color-enabled: #4a8bee;
/*
+ * Monitor Charts
+ */
+$chart-tooltip-max-width: 512px;
+
+/*
Stat Graph
*/
$stat-graph-common-bg: #f3f3f3;
@@ -718,3 +810,8 @@ $compare-branches-sticky-header-height: 68px;
- Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242
*/
$enable-validation-icons: false;
+
+/*
+Licenses
+*/
+$license-header-cell-width: 150px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 02364180c35..5ea96392afa 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -7,12 +7,15 @@
cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) {
- $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height;
+ // The `-1` below is to prevent two borders from clashing up against eachother -
+ // the bottom of the compare-versions header and the top of the file header
+ $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height - 1;
position: -webkit-sticky;
position: sticky;
top: $mr-file-header-top;
z-index: 102;
+ height: $mr-version-controls-height;
&::before {
content: '';
@@ -54,7 +57,8 @@
background-color: $gray-normal;
}
- a {
+ a,
+ button {
color: $gray-700;
}
@@ -154,11 +158,34 @@
.swipe-wrap {
overflow: hidden;
- border-left: 1px solid $gl-gray-400;
+ border-right: 1px solid $gl-gray-400;
position: absolute;
display: block;
top: 13px;
right: 7px;
+
+ &.left-oriented {
+ /* only for commit view (different swipe viewer) */
+ border-right: 0;
+ border-left: 1px solid $gl-gray-400;
+ }
+ }
+
+ .frame {
+ top: 0;
+ right: 0;
+
+ &.old-diff {
+ /* only for commit / compare view */
+ position: absolute;
+ }
+
+ &.deleted {
+ margin: 0;
+ display: block;
+ top: 13px;
+ right: 7px;
+ }
}
.swipe-bar {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 0eb854ecf98..8e1ee51628d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -204,7 +204,7 @@
}
.prometheus-graphs {
- .environments {
+ .dropdowns {
.dropdown-menu-toggle {
svg {
position: absolute;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6415d902ca6..9be3f8138a0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -110,6 +110,16 @@
font-size: 0;
margin-bottom: -5px;
}
+
+ .scoped-label-wrapper {
+ .color-label {
+ padding-right: $gl-padding-24;
+ }
+
+ .scoped-label {
+ right: 12px;
+ }
+ }
}
.right-sidebar {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 6f98b4f7f13..e7fd7fab32b 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -402,3 +402,39 @@
.priority-labels-empty-state .svg-content img {
max-width: $priority-label-empty-state-width;
}
+
+.scoped-label-tooltip-title {
+ color: $indigo-300;
+}
+
+.scoped-label-wrapper {
+ &.label-link .color-label a {
+ color: inherit;
+ }
+
+ .color-label {
+ padding-right: $gl-padding-24;
+ }
+
+ .scoped-label {
+ position: absolute;
+ top: 4px;
+ right: 8px;
+ padding: 0;
+ margin: 0;
+ line-height: $gl-line-height;
+ }
+}
+
+// Label inside title of Delete Label Modal
+.modal-header .page-title {
+ .scoped-label-wrapper {
+ .scoped-label {
+ line-height: 20px;
+ }
+
+ span.color-label {
+ padding-right: $gl-padding-24;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7f8b8ea8100..86b58c1b1b2 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -595,7 +595,6 @@
color: $gl-text-color;
}
-
.git-merge-container {
justify-content: space-between;
flex: 1;
@@ -737,7 +736,6 @@
background: $gray-light;
color: $gl-text-color;
margin-top: -1px;
- border-top: 1px solid $border-color;
.mr-version-menus-container {
display: flex;
@@ -760,6 +758,7 @@
.content-block {
padding: $gl-padding-top $gl-padding;
+ border-bottom: 0;
}
.comments-disabled-notif {
@@ -784,16 +783,18 @@
padding-right: 5px;
}
+ // Shortening button height by 1px to make compare-versions
+ // header 56px and fit into our 8px design grid
+ button {
+ height: 34px;
+ }
+
@include media-breakpoint-up(md) {
position: -webkit-sticky;
position: sticky;
top: $header-height + $mr-tabs-height;
- width: 100%;
-
- &.is-fileTreeOpen {
- margin-left: -16px;
- width: calc(100% + 32px);
- }
+ margin-left: -16px;
+ width: calc(100% + 32px);
.mr-version-menus-container {
flex-wrap: nowrap;
@@ -805,7 +806,8 @@
}
}
-.merge-request-tabs-holder {
+.merge-request-tabs-holder,
+.epic-tabs-holder {
top: $header-height;
z-index: 250;
background-color: $white-light;
@@ -823,11 +825,6 @@
@include media-breakpoint-down(xs) {
right: 0;
}
-
- .merge-request-tabs-container {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
- }
}
.nav-links {
@@ -835,11 +832,21 @@
}
}
-.with-performance-bar .merge-request-tabs-holder {
- top: $header-height + $performance-bar-height;
+.merge-request-tabs-holder.affix .merge-request-tabs-container,
+.epic-tabs-holder.affix .epic-tabs-container {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
}
-.merge-request-tabs {
+.with-performance-bar {
+ .merge-request-tabs-holder,
+ .epic-tabs-holder {
+ top: $header-height + $performance-bar-height;
+ }
+}
+
+.merge-request-tabs,
+.epic-tabs {
display: flex;
flex-wrap: nowrap;
margin-bottom: 0;
@@ -847,7 +854,8 @@
}
.limit-container-width {
- .merge-request-tabs-container {
+ .merge-request-tabs-container,
+ .epic-tabs-container {
max-width: $limited-layout-width;
margin-left: auto;
margin-right: auto;
@@ -860,7 +868,8 @@
}
}
-.merge-request-tabs-container {
+.merge-request-tabs-container,
+.epic-tabs-container {
display: flex;
justify-content: space-between;
@@ -878,10 +887,9 @@
}
.limit-container-width:not(.container-limited) {
- .merge-request-tabs-holder:not(.affix) {
- .merge-request-tabs-container {
- max-width: $limited-layout-width - ($gl-padding * 2);
- }
+ .merge-request-tabs-holder:not(.affix) .merge-request-tabs-container,
+ .epic-tabs-holder:not(.affix) .epic-tabs-container {
+ max-width: $limited-layout-width - ($gl-padding * 2);
}
}
diff --git a/app/assets/stylesheets/pages/monitor.scss b/app/assets/stylesheets/pages/monitor.scss
new file mode 100644
index 00000000000..25ff5abd774
--- /dev/null
+++ b/app/assets/stylesheets/pages/monitor.scss
@@ -0,0 +1,5 @@
+.chart-tooltip > .popover {
+ min-width: 0;
+ width: max-content;
+ max-width: $chart-tooltip-max-width;
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 0c334e919de..fd07415a52f 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -44,7 +44,6 @@ $note-form-margin-left: 72px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
margin: $gl-padding 0;
- overflow: auto;
&.system-note,
&.note-form {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index bb08440fda8..3e6aa43175e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -712,15 +712,9 @@
top: 8px;
}
+.ci-build-text,
.ci-status-text {
- max-width: 110px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: bottom;
- display: inline-block;
- position: relative;
- font-weight: $gl-font-weight-normal;
+ font-weight: 200;
}
@mixin mini-pipeline-graph-color(
@@ -912,26 +906,6 @@ button.mini-pipeline-graph-dropdown-toggle {
flex: 1;
}
- // build name
- .ci-build-text,
- .ci-status-text {
- font-weight: 200;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- max-width: 70%;
- margin-left: 2px;
- display: inline-block;
-
- &::after {
- content: '';
- display: block;
- }
-
- @include media-breakpoint-down(xs) {
- max-width: 60%;
- }
- }
.ci-status-icon {
@extend .append-right-8;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bcb306d97d5..792c618fd40 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -209,8 +209,7 @@
}
}
- .access-request-link,
- .home-panel-topic-list {
+ .access-request-link {
padding-left: $gl-padding-8;
border-left: 1px solid $gl-text-color-secondary;
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
new file mode 100644
index 00000000000..3648ec5e239
--- /dev/null
+++ b/app/assets/stylesheets/utilities.scss
@@ -0,0 +1,17 @@
+@each $variant, $range in $color-ranges {
+ @each $suffix, $color in $range {
+ #{'.bg-#{$variant}-#{$suffix}'} {
+ background-color: $color;
+ }
+
+ #{'.text-#{$variant}-#{$suffix}'} {
+ color: $color;
+ }
+ }
+}
+
+@each $index, $size in $type-scale {
+ #{'.text-#{$index}'} {
+ font-size: $size;
+ }
+}
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index e0ecdb0c0e9..15f7ef881c8 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -89,7 +89,8 @@ class Admin::GroupsController < Admin::ApplicationController
:request_access_enabled,
:visibility_level,
:require_two_factor_authentication,
- :two_factor_grace_period
+ :two_factor_grace_period,
+ :project_creation_level
]
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 05d88429cfe..85aeecbf90b 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -8,7 +8,7 @@ module IssuableActions
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
before_action only: :show do
- push_frontend_feature_flag(:reply_to_individual_notes, default_enabled: true)
+ push_frontend_feature_flag(:scoped_labels, default_enabled: true)
end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index c529aabf797..6d6e0cc6c7f 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -100,6 +100,7 @@ module IssuableCollections
if @project
options[:project_id] = @project.id
+ options[:attempt_project_search_optimizations] = true
elsif @group
options[:group_id] = @group.id
options[:include_subgroups] = true
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index f72d25fc54c..2a9729b6ffd 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -20,7 +20,7 @@ module PreviewMarkdown
body: view_context.markdown(result[:text], markdown_params),
references: {
users: result[:users],
- suggestions: result[:suggestions],
+ suggestions: SuggestionSerializer.new.represent_diff(result[:suggestions]),
commands: view_context.markdown(result[:commands])
}
}
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 75329b05a6f..1a97b39d3ae 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -46,7 +46,10 @@ class DashboardController < Dashboard::ApplicationController
end
def check_filters_presence!
- @no_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) }
+ no_scalar_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) }
+ no_array_filters_set = finder_type.array_params.none? { |k, _| params.key?(k) }
+
+ @no_filters_set = no_scalar_filters_set && no_array_filters_set
return unless @no_filters_set
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 4f641de0357..b44e3b0fff4 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -41,7 +41,7 @@ module Groups
end
def variable_params_attributes
- %i[id key secret_value protected _destroy]
+ %i[id key secret_value protected masked _destroy]
end
def authorize_admin_build!
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 0192b1c253e..e936d771502 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -124,8 +124,8 @@ class GroupsController < Groups::ApplicationController
flash[:notice] = "Group '#{@group.name}' was successfully transferred."
redirect_to group_path(@group)
else
- flash.now[:alert] = service.error
- render :edit
+ flash[:alert] = service.error
+ redirect_to edit_group_path(@group)
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -187,7 +187,8 @@ class GroupsController < Groups::ApplicationController
:create_chat_team,
:chat_team_name,
:require_two_factor_authentication,
- :two_factor_grace_period
+ :two_factor_grace_period,
+ :project_creation_level
]
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 6504fd6c08a..781eac7f080 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -88,4 +88,10 @@ class Projects::ApplicationController < ApplicationController
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
end
+
+ def allow_gitaly_ref_name_caching
+ ::Gitlab::GitalyClient.allow_ref_name_caching do
+ yield
+ end
+ end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 2510a31c9b3..a49ede04de7 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -14,6 +14,8 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :validate_ref!, except: :commits_root
before_action :set_commits, except: :commits_root
+ around_action :allow_gitaly_ref_name_caching
+
def commits_root
redirect_to project_commits_path(@project, @project.default_branch)
end
diff --git a/app/controllers/projects/environments/prometheus_api_controller.rb b/app/controllers/projects/environments/prometheus_api_controller.rb
new file mode 100644
index 00000000000..fd3320637b0
--- /dev/null
+++ b/app/controllers/projects/environments/prometheus_api_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class Projects::Environments::PrometheusApiController < Projects::ApplicationController
+ before_action :authorize_read_prometheus!
+ before_action :environment
+
+ def proxy
+ result = Prometheus::ProxyService.new(
+ environment,
+ request.method,
+ params[:proxy_path],
+ params.permit!
+ ).execute
+
+ if result.nil?
+ return render status: :accepted, json: {
+ status: _('processing'),
+ message: _('Not ready yet. Try again later.')
+ }
+ end
+
+ if result[:status] == :success
+ render status: result[:http_status], json: result[:body]
+ else
+ render(
+ status: result[:http_status] || :bad_request,
+ json: { status: result[:status], message: result[:message] }
+ )
+ end
+ end
+
+ private
+
+ def environment
+ @environment ||= project.environments.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index e9cd475a199..301449cfa90 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,6 +10,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
+ before_action only: [:metrics, :additional_metrics] do
+ push_frontend_feature_flag(:metrics_time_window)
+ end
def index
@environments = project.environments
@@ -114,7 +117,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
terminal = environment.terminals.try(:first)
if terminal
set_workhorse_internal_api_content_type
- render json: Gitlab::Workhorse.terminal_websocket(terminal)
+ render json: Gitlab::Workhorse.channel_websocket(terminal)
else
render html: 'Not found', status: :not_found
end
@@ -146,7 +149,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def additional_metrics
respond_to do |format|
format.json do
- additional_metrics = environment.additional_metrics || {}
+ additional_metrics = environment.additional_metrics(*metrics_params) || {}
render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content
end
@@ -186,6 +189,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment ||= project.environments.find(params[:id])
end
+ def metrics_params
+ return unless Feature.enabled?(:metrics_time_window, project)
+ return unless params[:start].present? || params[:end].present?
+
+ params.require([:start, :end]).values_at(:start, :end)
+ end
+
def search_environment_names
return [] unless params[:query]
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 55d5fce9214..85628dd32d8 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -98,10 +98,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def repo_type
parse_repo_path unless defined?(@repo_type)
- # When there a project did not exist, the parsed repo_type would be empty.
- # In that case, we want to continue with a regular project repository. As we
- # could create the project if the user pushing is allowed to do so.
- @repo_type || Gitlab::GlRepository::PROJECT
+
+ @repo_type
end
def handle_basic_authentication(login, password)
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 35cc32d3e63..2a4933e7bc2 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -157,7 +157,7 @@ class Projects::JobsController < Projects::ApplicationController
# GET .../terminal.ws : implemented in gitlab-workhorse
def terminal_websocket_authorize
set_workhorse_internal_api_content_type
- render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification)
+ render json: Gitlab::Workhorse.channel_websocket(@build.terminal_specification)
end
private
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 34cb0416965..39ba2a651d4 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -16,6 +16,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
+ around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
+
def index
@merge_requests = @issuables
@@ -315,9 +317,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def serializer
- ::Gitlab::GitalyClient.allow_ref_name_caching do
- MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
- end
+ MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
end
def define_edit_vars
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index c306ba3ffcf..22c4b8eef1f 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -8,6 +8,8 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+ around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
+
wrap_parameters Ci::Pipeline
POLLING_INTERVAL = 10_000
@@ -148,12 +150,10 @@ class Projects::PipelinesController < Projects::ApplicationController
private
def serialize_pipelines
- ::Gitlab::GitalyClient.allow_ref_name_caching do
- PipelineSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .represent(@pipelines, disable_coverage: true, preload: true)
- end
+ PipelineSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ .represent(@pipelines, disable_coverage: true, preload: true)
end
def render_show
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 4eeaeb860ee..3b4215b766e 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -23,7 +23,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
append_sha = false if @filename == shortname
end
- send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
+ send_git_archive @repository, ref: @ref, path: params[:path], format: params[:format], append_sha: append_sha
rescue => ex
logger.error("#{self.class.name}: #{ex}")
git_not_found!
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 39eca10134f..8c3d141c888 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -7,19 +7,14 @@ module Projects
before_action :authorize_read_cluster!
- INDEX_PRIMING_INTERVAL = 15_000
- INDEX_POLLING_INTERVAL = 60_000
-
def index
respond_to do |format|
format.json do
functions = finder.execute
if functions.any?
- Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL)
render json: serialize_function(functions)
else
- Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL)
head :no_content
end
end
@@ -33,6 +28,8 @@ module Projects
def show
@service = serialize_function(finder.service(params[:environment_id], params[:id]))
+ @prometheus = finder.has_prometheus?(params[:environment_id])
+
return not_found if @service.nil?
respond_to do |format|
@@ -44,10 +41,24 @@ module Projects
end
end
+ def metrics
+ respond_to do |format|
+ format.json do
+ metrics = finder.invocation_metrics(params[:environment_id], params[:id])
+
+ if metrics.nil?
+ head :no_content
+ else
+ render json: metrics
+ end
+ end
+ end
+ end
+
private
def finder
- Projects::Serverless::FunctionsFinder.new(project.clusters)
+ Projects::Serverless::FunctionsFinder.new(project)
end
def serialize_function(function)
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index bb658bfcc19..05a79d59ffd 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -38,6 +38,6 @@ class Projects::VariablesController < Projects::ApplicationController
end
def variable_params_attributes
- %i[id key secret_value protected _destroy]
+ %i[id key secret_value protected masked _destroy]
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index da2420633ef..88910c91763 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -16,7 +16,10 @@ class Projects::WikisController < Projects::ApplicationController
end
def pages
- @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
+ @wiki_pages = Kaminari.paginate_array(
+ @project_wiki.pages(sort: params[:sort], direction: params[:direction])
+ ).page(params[:page])
+
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index f76e6663995..94258e0e90a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -26,6 +26,8 @@ class ProjectsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity]
+ around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
+
layout :determine_layout
def index
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 6eab8c5ee51..64c88505a16 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -53,7 +53,6 @@ class IssuableFinder
assignee_username
author_id
author_username
- label_name
milestone_title
my_reaction_emoji
search
@@ -84,7 +83,7 @@ class IssuableFinder
# https://www.postgresql.org/docs/current/static/queries-with.html
items = by_search(items)
- items = sort(items) unless use_cte_for_count?
+ items = sort(items)
items
end
@@ -92,7 +91,6 @@ class IssuableFinder
def filter_items(items)
items = by_project(items)
items = by_group(items)
- items = by_subquery(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
@@ -132,10 +130,12 @@ class IssuableFinder
# This does not apply when we are using a CTE for the search, as the labels
# GROUP BY is inside the subquery in that case, so we set labels_count to 1.
#
- # We always use CTE when searching in Groups if the feature flag is enabled,
- # but never when searching in Projects.
+ # Groups and projects have separate feature flags to suggest the use
+ # of a CTE. The CTE will not be used if the sort doesn't support it,
+ # but will always be used for the counts here as we ignore sorting
+ # anyway.
labels_count = label_names.any? ? label_names.count : 1
- labels_count = 1 if use_cte_for_count?
+ labels_count = 1 if use_cte_for_search?
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[count_key(key)] += value / labels_count
@@ -309,15 +309,14 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- def use_subquery_for_search?
- strong_memoize(:use_subquery_for_search) do
- !force_cte? && attempt_group_search_optimizations?
- end
- end
+ def use_cte_for_search?
+ strong_memoize(:use_cte_for_search) do
+ next false unless search
+ next false unless Gitlab::Database.postgresql?
+ # Only simple unsorted & simple sorts can use CTE
+ next false if params[:sort].present? && !params[:sort].in?(klass.simple_sorts.keys)
- def use_cte_for_count?
- strong_memoize(:use_cte_for_count) do
- force_cte? && attempt_group_search_optimizations?
+ attempt_group_search_optimizations? || attempt_project_search_optimizations?
end
end
@@ -332,12 +331,15 @@ class IssuableFinder
end
def attempt_group_search_optimizations?
- search &&
- Gitlab::Database.postgresql? &&
- params[:attempt_group_search_optimizations] &&
+ params[:attempt_group_search_optimizations] &&
Feature.enabled?(:attempt_group_search_optimizations, default_enabled: true)
end
+ def attempt_project_search_optimizations?
+ params[:attempt_project_search_optimizations] &&
+ Feature.enabled?(:attempt_project_search_optimizations)
+ end
+
def count_key(value)
Array(value).last.to_sym
end
@@ -408,20 +410,11 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- # Wrap projects and groups in a subquery if the conditions are met.
- def by_subquery(items)
- if use_subquery_for_search?
- klass.where(id: items.select(:id)) # rubocop: disable CodeReuse/ActiveRecord
- else
- items
- end
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_search(items)
return items unless search
- if use_cte_for_count?
+ if use_cte_for_search?
cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
cte << items
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 84689ff5dc7..29947bc94d5 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -40,7 +40,8 @@ class MergeRequestsFinder < IssuableFinder
items = by_commit(super)
items = by_source_branch(items)
items = by_wip(items)
- by_target_branch(items)
+ items = by_target_branch(items)
+ by_source_project_id(items)
end
private
@@ -74,6 +75,16 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
+ def source_project_id
+ @source_project_id ||= params[:source_project_id].presence
+ end
+
+ def by_source_project_id(items)
+ return items unless source_project_id
+
+ items.where(source_project_id: source_project_id)
+ end
+
def by_wip(items)
if params[:wip] == 'yes'
items.where(wip_match(items.arel_table))
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index 2f2816a4a08..d9802598c64 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -3,8 +3,9 @@
module Projects
module Serverless
class FunctionsFinder
- def initialize(clusters)
- @clusters = clusters
+ def initialize(project)
+ @clusters = project.clusters
+ @project = project
end
def execute
@@ -19,6 +20,23 @@ module Projects
knative_service(environment_scope, name)&.first
end
+ def invocation_metrics(environment_scope, name)
+ return unless prometheus_adapter&.can_query?
+
+ cluster = clusters_with_knative_installed.preload_knative.find do |c|
+ environment_scope == c.environment_scope
+ end
+
+ func = ::Serverless::Function.new(@project, name, cluster.platform_kubernetes&.actual_namespace)
+ prometheus_adapter.query(:knative_invocation, func)
+ end
+
+ def has_prometheus?(environment_scope)
+ clusters_with_knative_installed.preload_knative.to_a.any? do |cluster|
+ environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
+ end
+ end
+
private
def knative_service(environment_scope, name)
@@ -55,6 +73,12 @@ module Projects
def clusters_with_knative_installed
@clusters.with_knative_installed
end
+
+ # rubocop: disable CodeReuse/ServiceClass
+ def prometheus_adapter
+ @prometheus_adapter ||= ::Prometheus::AdapterService.new(@project).prometheus_adapter
+ end
+ # rubocop: enable CodeReuse/ServiceClass
end
end
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 06d26309b5b..53efd9042b1 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -1,13 +1,44 @@
# frozen_string_literal: true
class GitlabSchema < GraphQL::Schema
+ # Took our current most complicated query in use, issues.graphql,
+ # with a complexity of 19, and added a 20 point buffer to it.
+ # These values will evolve over time.
+ DEFAULT_MAX_COMPLEXITY = 40
+ AUTHENTICATED_COMPLEXITY = 50
+ ADMIN_COMPLEXITY = 60
+
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections
+ use Gitlab::Graphql::Tracing
+
+ query_analyzer Gitlab::Graphql::QueryAnalyzers::LogQueryComplexity.analyzer
query(Types::QueryType)
default_max_page_size 100
+
+ max_complexity DEFAULT_MAX_COMPLEXITY
+
mutation(Types::MutationType)
+
+ def self.execute(query_str = nil, **kwargs)
+ kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
+
+ super(query_str, **kwargs)
+ end
+
+ def self.max_query_complexity(ctx)
+ current_user = ctx&.fetch(:current_user, nil)
+
+ if current_user&.admin
+ ADMIN_COMPLEXITY
+ elsif current_user
+ AUTHENTICATED_COMPLEXITY
+ else
+ DEFAULT_MAX_COMPLEXITY
+ end
+ end
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 2b2ea64c00b..8c8b8a82d3e 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -3,5 +3,14 @@
module Types
class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize
+
+ DEFAULT_COMPLEXITY = 1
+
+ def initialize(*args, **kwargs, &block)
+ # complexity is already defaulted to 1, but let's make it explicit
+ kwargs[:complexity] ||= DEFAULT_COMPLEXITY
+
+ super(*args, **kwargs, &block)
+ end
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 18696293b97..de7d6570a3e 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -3,10 +3,12 @@
module Types
module Ci
class PipelineType < BaseObject
- expose_permissions Types::PermissionTypes::Ci::Pipeline
-
graphql_name 'Pipeline'
+ authorize :read_pipeline
+
+ expose_permissions Types::PermissionTypes::Ci::Pipeline
+
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 5ad3ea52930..adb137dfee3 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -2,10 +2,12 @@
module Types
class IssueType < BaseObject
- expose_permissions Types::PermissionTypes::Issue
-
graphql_name 'Issue'
+ authorize :read_issue
+
+ expose_permissions Types::PermissionTypes::Issue
+
present_using IssuePresenter
field :iid, GraphQL::ID_TYPE, null: false
@@ -15,16 +17,14 @@ module Types
field :author, Types::UserType,
null: false,
- resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find },
- authorize: :read_user
+ resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
field :assignees, Types::UserType.connection_type, null: true
field :labels, Types::LabelType.connection_type, null: true
field :milestone, Types::MilestoneType,
null: true,
- resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find },
- authorize: :read_milestone
+ resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
field :due_date, Types::TimeType, null: true
field :confidential, GraphQL::BOOLEAN_TYPE, null: false
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 1ed27a14e33..120ffe0dfde 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -2,12 +2,14 @@
module Types
class MergeRequestType < BaseObject
+ graphql_name 'MergeRequest'
+
+ authorize :read_merge_request
+
expose_permissions Types::PermissionTypes::MergeRequest
present_using MergeRequestPresenter
- graphql_name 'MergeRequest'
-
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: false
@@ -48,7 +50,7 @@ module Types
field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
- field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline, authorize: :read_pipeline
+ field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline
field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver
end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index af31b572c9a..2772fbec86f 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -4,6 +4,8 @@ module Types
class MilestoneType < BaseObject
graphql_name 'Milestone'
+ authorize :read_milestone
+
field :description, GraphQL::STRING_TYPE, null: true
field :title, GraphQL::STRING_TYPE, null: false
field :state, GraphQL::STRING_TYPE, null: false
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index b96c2f3afb2..fbb4eddd13c 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -2,10 +2,12 @@
module Types
class ProjectType < BaseObject
- expose_permissions Types::PermissionTypes::Project
-
graphql_name 'Project'
+ authorize :read_project
+
+ expose_permissions Types::PermissionTypes::Project
+
field :id, GraphQL::ID_TYPE, null: false
field :full_path, GraphQL::ID_TYPE, null: false
@@ -67,14 +69,12 @@ module Types
field :merge_requests,
Types::MergeRequestType.connection_type,
null: true,
- resolver: Resolvers::MergeRequestsResolver,
- authorize: :read_merge_request
+ resolver: Resolvers::MergeRequestsResolver
field :merge_request,
Types::MergeRequestType,
null: true,
- resolver: Resolvers::MergeRequestsResolver.single,
- authorize: :read_merge_request
+ resolver: Resolvers::MergeRequestsResolver.single
field :issues,
Types::IssueType.connection_type,
@@ -88,7 +88,7 @@ module Types
field :pipelines,
Types::Ci::PipelineType.connection_type,
- null: false,
+ null: true,
resolver: Resolvers::ProjectPipelinesResolver
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 472fe5d6ec2..0f655ab9d03 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -7,8 +7,7 @@ module Types
field :project, Types::ProjectType,
null: true,
resolver: Resolvers::ProjectResolver,
- description: "Find a project",
- authorize: :read_project
+ description: "Find a project"
field :metadata, Types::MetadataType,
null: true,
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index a13e65207df..6b53554314b 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -4,6 +4,8 @@ module Types
class UserType < BaseObject
graphql_name 'User'
+ authorize :read_user
+
present_using UserPresenter
field :name, GraphQL::STRING_TYPE, null: false
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index e635f608237..e275e4278a4 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -137,6 +137,7 @@ module ApplicationSettingsHelper
:default_artifacts_expire_in,
:default_branch_protection,
:default_group_visibility,
+ :default_project_creation,
:default_project_visibility,
:default_projects_limit,
:default_snippet_visibility,
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index d6fff1c36da..7e631053b54 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -77,7 +77,7 @@ module BlobHelper
project,
ref,
path,
- label: "Replace",
+ label: _("Replace"),
action: "replace",
btn_class: "default",
modal_type: "upload"
@@ -89,7 +89,7 @@ module BlobHelper
project,
ref,
path,
- label: "Delete",
+ label: _("Delete"),
action: "delete",
btn_class: "remove",
modal_type: "remove"
@@ -101,14 +101,14 @@ module BlobHelper
end
def leave_edit_message
- "Leave edit mode?\nAll unsaved changes will be lost."
+ _("Leave edit mode? All unsaved changes will be lost.")
end
def editing_preview_title(filename)
if Gitlab::MarkupHelper.previewable?(filename)
- 'Preview'
+ _('Preview')
else
- 'Preview changes'
+ _('Preview changes')
end
end
@@ -201,14 +201,14 @@ module BlobHelper
return if blob.empty?
return if blob.binary? || blob.stored_externally?
- title = 'Open raw'
+ title = _('Open raw')
link_to icon('file-code-o'), blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def download_blob_button(blob)
return if blob.empty?
- title = 'Download'
+ title = _('Download')
link_to sprite_icon('download'), blob_raw_path(inline: false), download: @path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 3c8caec3fe5..a5fe6bb8f07 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -4,12 +4,12 @@ module BuildsHelper
def build_summary(build, skip: false)
if build.has_trace?
if skip
- link_to "View job trace", pipeline_job_url(build.pipeline, build)
+ link_to _("View job trace"), pipeline_job_url(build.pipeline, build)
else
build.trace.html(last_lines: 10).html_safe
end
else
- "No job trace"
+ _("No job trace")
end
end
@@ -31,7 +31,7 @@ module BuildsHelper
def build_failed_issue_options
{
- title: "Job Failed ##{@build.id}",
+ title: _("Job Failed #%{build_id}") % { build_id: @build.id },
description: project_job_url(@project, @build)
}
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 494c754e7d5..03adbfa204f 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -21,7 +21,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
- title = data[:title] || 'Copy to clipboard'
+ title = data[:title] || _('Copy to clipboard')
button_text = data[:button_text] || ''
hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
index e3728804c2a..88ce311a1d4 100644
--- a/app/helpers/ci_variables_helper.rb
+++ b/app/helpers/ci_variables_helper.rb
@@ -12,4 +12,12 @@ module CiVariablesHelper
ci_variable_protected_by_default?
end
end
+
+ def ci_variable_masked?(variable, only_key_value)
+ if variable && !only_key_value
+ variable.masked
+ else
+ true
+ end
+ end
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 5705ee54cee..8b3d270e873 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -4,8 +4,7 @@ module FormHelper
def form_errors(model, type: 'form')
return unless model.errors.any?
- pluralized = 'error'.pluralize(model.errors.count)
- headline = "The #{type} contains the following #{pluralized}:"
+ headline = n_('The %{type} contains the following error:', 'The %{type} contains the following errors:', model.errors.count) % { type: type }
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
@@ -24,7 +23,7 @@ module FormHelper
title: 'Select assignee',
filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee',
- placeholder: 'Search users',
+ placeholder: _('Search users'),
data: {
first_user: current_user&.username,
null_user: true,
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 0622cdfc196..52c49498e9b 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -277,6 +277,8 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
+ data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
+
if parent.is_a?(Group)
data[:groupPath] = parent.path
else
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index bd53add80ca..e91e8f85515 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -46,7 +46,7 @@ module LabelsHelper
if block_given?
link_to link, class: css_class, &block
else
- link_to render_colored_label(label, tooltip: tooltip), link, class: css_class
+ render_label(label, tooltip: tooltip, link: link, css: css_class)
end
end
@@ -78,19 +78,33 @@ module LabelsHelper
end
end
- def render_colored_label(label, label_suffix = '', tooltip: true)
+ def render_label(label, tooltip: true, link: nil, css: nil)
+ # if scoped label is used then EE wraps label tag with scoped label
+ # doc link
+ html = render_colored_label(label, tooltip: tooltip)
+ html = link_to(html, link, class: css) if link
+
+ html
+ end
+
+ def render_colored_label(label, label_suffix: '', tooltip: true, title: nil)
text_color = text_color_for_bg(label.color)
+ title ||= tooltip ? label_tooltip_title(label) : ''
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) +
- %(style="background-color: #{label.color}; color: #{text_color}" ) +
- %(title="#{escape_once(label.description)}" data-container="body">) +
+ %(data-html="true" style="background-color: #{label.color}; color: #{text_color}" ) +
+ %(title="#{escape_once(title)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
span.html_safe
end
+ def label_tooltip_title(label)
+ label.description
+ end
+
def suggested_colors
[
'#0033CC',
@@ -181,8 +195,8 @@ module LabelsHelper
def label_deletion_confirm_text(label)
case label
- when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
- when ProjectLabel then 'Remove this label? Are you sure?'
+ when GroupLabel then _('Remove this label? This will affect all projects within the group. Are you sure?')
+ when ProjectLabel then _('Remove this label? Are you sure?')
end
end
@@ -231,6 +245,37 @@ module LabelsHelper
labels.sort_by(&:title)
end
+ def label_dropdown_data(project, opts = {})
+ {
+ toggle: "dropdown",
+ field_name: opts[:field_name] || "label_name[]",
+ show_no: "true",
+ show_any: "true",
+ project_id: project&.try(:id),
+ namespace_path: project&.try(:namespace)&.try(:full_path),
+ project_path: project&.try(:path)
+ }.merge(opts)
+ end
+
+ def sidebar_label_dropdown_data(issuable_type, issuable_sidebar)
+ label_dropdown_data(nil, {
+ default_label: "Labels",
+ field_name: "#{issuable_type}[label_names][]",
+ ability_name: issuable_type,
+ namespace_path: issuable_sidebar[:namespace_path],
+ project_path: issuable_sidebar[:project_path],
+ issue_update: issuable_sidebar[:issuable_json_path],
+ labels: issuable_sidebar[:project_labels_path],
+ display: 'static'
+ })
+ end
+
+ def label_from_hash(hash)
+ klass = hash[:group_id] ? GroupLabel : ProjectLabel
+
+ klass.new(hash.slice(:color, :description, :title, :group_id, :project_id))
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
- module_function :render_colored_label, :text_color_for_bg, :escape_once
+ module_function :render_colored_label, :text_color_for_bg, :escape_once, :label_tooltip_title
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index ea3bcfc791a..572d68cb4a3 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -49,6 +49,13 @@ module NamespacesHelper
end
end
+ def namespaces_options_with_developer_maintainer_access(options = {})
+ selected = options.delete(:selected) || :current_user
+ options[:groups] = current_user.manageable_groups_with_routes(include_groups_with_developer_maintainer_access: true)
+
+ namespaces_options(selected, options)
+ end
+
private
# Many importers create a temporary Group, so use the real
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index f2abb241753..009dd70c2c9 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -299,6 +299,10 @@ module ProjectsHelper
}.to_json
end
+ def directory?
+ @path.present?
+ end
+
private
def get_project_nav_tabs(project, current_user)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 69520e33774..a62c00df60b 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -30,7 +30,7 @@ module SearchHelper
to = collection.offset_value + collection.to_a.size
count = collection.total_count
- "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
+ s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"") % { from: from, to: to, count: count, scope: scope.humanize(capitalize: false), term: term }
end
def find_project_for_result_blob(projects, result)
@@ -59,31 +59,31 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { category: "Settings", label: "User settings", url: profile_path },
- { category: "Settings", label: "SSH Keys", url: profile_keys_path },
- { category: "Settings", label: "Dashboard", url: root_path }
+ { category: "Settings", label: _("User settings"), url: profile_path },
+ { category: "Settings", label: _("SSH Keys"), url: profile_keys_path },
+ { category: "Settings", label: _("Dashboard"), url: root_path }
]
end
# Autocomplete results for settings pages, for admins
def default_autocomplete_admin
[
- { category: "Settings", label: "Admin Section", url: admin_root_path }
+ { category: "Settings", label: _("Admin Section"), url: admin_root_path }
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
- { category: "Help", label: "API Help", url: help_page_path("api/README") },
- { category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") },
- { category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") },
- { category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
- { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
- { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
- { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
- { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") },
- { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }
+ { category: "Help", label: _("API Help"), url: help_page_path("api/README") },
+ { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
+ { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
+ { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
+ { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
+ { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") },
+ { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
+ { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") },
+ { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") }
]
end
@@ -93,16 +93,16 @@ module SearchHelper
ref = @ref || @project.repository.root_ref
[
- { category: "In this project", label: "Files", url: project_tree_path(@project, ref) },
- { category: "In this project", label: "Commits", url: project_commits_path(@project, ref) },
- { category: "In this project", label: "Network", url: project_network_path(@project, ref) },
- { category: "In this project", label: "Graph", url: project_graph_path(@project, ref) },
- { category: "In this project", label: "Issues", url: project_issues_path(@project) },
- { category: "In this project", label: "Merge Requests", url: project_merge_requests_path(@project) },
- { category: "In this project", label: "Milestones", url: project_milestones_path(@project) },
- { category: "In this project", label: "Snippets", url: project_snippets_path(@project) },
- { category: "In this project", label: "Members", url: project_project_members_path(@project) },
- { category: "In this project", label: "Wiki", url: project_wikis_path(@project) }
+ { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
+ { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) },
+ { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
+ { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) },
+ { category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
+ { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) },
+ { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
+ { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
+ { category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
+ { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
]
else
[]
@@ -162,7 +162,7 @@ module SearchHelper
opts =
{
id: "filtered-search-#{type}",
- placeholder: 'Search or filter results...',
+ placeholder: _('Search or filter results...'),
data: {
'username-params' => UserSerializer.new.represent(@users)
},
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index c5bab877c00..4690b6ffbe1 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -86,17 +86,17 @@ module TreeHelper
end
def edit_in_new_fork_notice_now
- "You're not allowed to make changes to this project directly." +
- " A fork of this project is being created that you can make changes in, so you can submit a merge request."
+ _("You're not allowed to make changes to this project directly. "\
+ "A fork of this project is being created that you can make changes in, so you can submit a merge request.")
end
def edit_in_new_fork_notice
- "You're not allowed to make changes to this project directly." +
- " A fork of this project has been created that you can make changes in, so you can submit a merge request."
+ _("You're not allowed to make changes to this project directly. "\
+ "A fork of this project has been created that you can make changes in, so you can submit a merge request.")
end
def edit_in_new_fork_notice_action(action)
- edit_in_new_fork_notice + " Try to #{action} this file again."
+ edit_in_new_fork_notice + _(" Try to %{action} this file again.") % { action: action }
end
def commit_in_fork_help
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 712f0f808dd..9deb783d289 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -42,11 +42,11 @@ module VisibilityLevelHelper
def group_visibility_level_description(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
- "The group and its projects can only be viewed by members."
+ _("The group and its projects can only be viewed by members.")
when Gitlab::VisibilityLevel::INTERNAL
- "The group and any internal projects can be viewed by any logged in user."
+ _("The group and any internal projects can be viewed by any logged in user.")
when Gitlab::VisibilityLevel::PUBLIC
- "The group and any public projects can be viewed without any authentication."
+ _("The group and any public projects can be viewed without any authentication.")
end
end
@@ -54,20 +54,20 @@ module VisibilityLevelHelper
case level
when Gitlab::VisibilityLevel::PRIVATE
if snippet.is_a? ProjectSnippet
- "The snippet is visible only to project members."
+ _("The snippet is visible only to project members.")
else
- "The snippet is visible only to me."
+ _("The snippet is visible only to me.")
end
when Gitlab::VisibilityLevel::INTERNAL
- "The snippet is visible to any logged in user."
+ _("The snippet is visible to any logged in user.")
when Gitlab::VisibilityLevel::PUBLIC
- "The snippet can be accessed without any authentication."
+ _("The snippet can be accessed without any authentication.")
end
end
def restricted_visibility_level_description(level)
level_name = Gitlab::VisibilityLevel.level_name(level)
- "#{level_name.capitalize} visibility has been restricted by the administrator."
+ _("%{level_name} visibility has been restricted by the administrator.") % { level_name: level_name.capitalize }
end
def disallowed_visibility_level_description(level, form_model)
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 647f34e57ed..edd48f82729 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -47,4 +47,24 @@ module WikiHelper
def wiki_attachment_upload_url
expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
end
+
+ def wiki_sort_controls(project, sort, direction)
+ sort ||= ProjectWiki::TITLE_ORDER
+ link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
+ reversed_direction = direction == 'desc' ? 'asc' : 'desc'
+ icon_class = direction == 'desc' ? 'highest' : 'lowest'
+
+ link_to(project_wikis_pages_path(project, sort: sort, direction: reversed_direction),
+ type: 'button', class: link_class, title: _('Sort direction')) do
+ sprite_icon("sort-#{icon_class}", size: 16)
+ end
+ end
+
+ def wiki_sort_title(key)
+ if key == ProjectWiki::CREATED_AT_ORDER
+ s_("Wiki|Created date")
+ else
+ s_("Wiki|Title")
+ end
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 9e91e4ab4b9..7ec8505b33a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -223,4 +223,11 @@ class ApplicationSetting < ApplicationRecord
reset_memoized_terms
end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
+
+ def self.create_from_defaults
+ super
+ rescue ActiveRecord::RecordNotUnique
+ # We already have an ApplicationSetting record, so just return it.
+ current_without_cache
+ end
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 265aa1d4965..b413ffddb9d 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -26,6 +26,7 @@ module ApplicationSettingImplementation
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_project_creation: Settings.gitlab['default_project_creation'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1bd517641ac..b8a76e662b0 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -83,8 +83,13 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do
- where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
- '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
+ if Feature.enabled?(:ci_enable_legacy_artifacts)
+ where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
+ '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
+ else
+ where('EXISTS (?)',
+ Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
+ end
end
scope :with_existing_job_artifacts, ->(query) do
@@ -135,6 +140,8 @@ module Ci
where("EXISTS (?)", matcher)
end
+ ##
+ # TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
@@ -775,7 +782,7 @@ module Ci
private
def erase_old_artifacts!
- # TODO: To be removed once we get rid of
+ # TODO: To be removed once we get rid of ci_enable_legacy_artifacts feature flag
remove_artifacts_file!
remove_artifacts_metadata!
save
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 061eff090f5..80dbb150085 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -6,6 +6,8 @@ module Ci
class BuildRunnerSession < ApplicationRecord
extend Gitlab::Ci::Model
+ TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'.freeze
+
self.table_name = 'ci_builds_runner_session'
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
@@ -14,11 +16,21 @@ module Ci
validates :url, url: { protocols: %w(https) }
def terminal_specification
- return {} unless url.present?
+ wss_url = Gitlab::UrlHelpers.as_wss(self.url)
+ return {} unless wss_url.present?
+
+ wss_url = "#{wss_url}/exec"
+ channel_specification(wss_url, TERMINAL_SUBPROTOCOL)
+ end
+
+ private
+
+ def channel_specification(url, subprotocol)
+ return {} if subprotocol.blank? || url.blank?
{
- subprotocols: ['terminal.gitlab.com'].freeze,
- url: "#{url}/exec".sub("https://", "wss://"),
+ subprotocols: Array(subprotocol),
+ url: url,
headers: { Authorization: [authorization.presence] }.compact,
ca_pem: certificate.presence
}
diff --git a/app/models/concerns/artifact_migratable.rb b/app/models/concerns/artifact_migratable.rb
index cbd63ba8876..7c9f579b480 100644
--- a/app/models/concerns/artifact_migratable.rb
+++ b/app/models/concerns/artifact_migratable.rb
@@ -13,7 +13,7 @@ module ArtifactMigratable
end
def artifacts?
- !artifacts_expired? && artifacts_file.exists?
+ !artifacts_expired? && artifacts_file&.exists?
end
def artifacts_metadata?
@@ -43,4 +43,16 @@ module ArtifactMigratable
def artifacts_size
read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
end
+
+ def legacy_artifacts_file
+ return unless Feature.enabled?(:ci_enable_legacy_artifacts)
+
+ super
+ end
+
+ def legacy_artifacts_metadata
+ return unless Feature.enabled?(:ci_enable_legacy_artifacts)
+
+ super
+ end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 51a8395c013..17f94b4bd9b 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -172,6 +172,10 @@ module Issuable
fuzzy_search(query, matched_columns)
end
+ def simple_sorts
+ super.except('name_asc', 'name_desc')
+ end
+
def sort_by_attribute(method, excluded_labels: [])
sorted =
case method.to_s
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index a29e80fe0c1..258c819f243 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -36,7 +36,7 @@ module PrometheusAdapter
def calculate_reactive_cache(query_class_name, *args)
return unless prometheus_client
- data = Kernel.const_get(query_class_name).new(prometheus_client_wrapper).query(*args)
+ data = Object.const_get(query_class_name, false).new(prometheus_client_wrapper).query(*args)
{
success: true,
data: data,
@@ -51,7 +51,7 @@ module PrometheusAdapter
end
def build_query_args(*args)
- args.map(&:id)
+ args.map { |arg| arg.respond_to?(:id) ? arg.id : arg }
end
end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 1ab3b3ddc46..1e09cd89550 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -29,6 +29,40 @@
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes.
#
+# The background worker needs to find or generate the object on which
+# `with_reactive_cache` was called.
+# The default behaviour can be overridden by defining a custom
+# `reactive_cache_worker_finder`.
+# Otherwise the background worker will use the class name and primary key to get
+# the object using the ActiveRecord find_by method.
+#
+# class Bar
+# include ReactiveCaching
+#
+# self.reactive_cache_key = ->() { ["bar", "thing"] }
+# self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+#
+# def self.from_cache(var1, var2)
+# # This method will be called by the background worker with "bar1" and
+# # "bar2" as arguments.
+# new(var1, var2)
+# end
+#
+# def initialize(var1, var2)
+# # ...
+# end
+#
+# def calculate_reactive_cache
+# # Expensive operation here. The return value of this method is cached
+# end
+#
+# def result
+# with_reactive_cache("bar1", "bar2") do |data|
+# # ...
+# end
+# end
+# end
+#
# Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
@@ -52,6 +86,7 @@ module ReactiveCaching
class_attribute :reactive_cache_key
class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_refresh_interval
+ class_attribute :reactive_cache_worker_finder
# defaults
self.reactive_cache_lease_timeout = 2.minutes
@@ -59,6 +94,10 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
+ self.reactive_cache_worker_finder = ->(id, *_args) do
+ find_by(primary_key => id)
+ end
+
def calculate_reactive_cache(*args)
raise NotImplementedError
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 29e48f0c5f7..df1a9e3fe6e 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -21,19 +21,21 @@ module Sortable
class_methods do
def order_by(method)
- case method.to_s
- when 'created_asc' then order_created_asc
- when 'created_date' then order_created_desc
- when 'created_desc' then order_created_desc
- when 'id_asc' then order_id_asc
- when 'id_desc' then order_id_desc
- when 'name_asc' then order_name_asc
- when 'name_desc' then order_name_desc
- when 'updated_asc' then order_updated_asc
- when 'updated_desc' then order_updated_desc
- else
- all
- end
+ simple_sorts.fetch(method.to_s, -> { all }).call
+ end
+
+ def simple_sorts
+ {
+ 'created_asc' => -> { order_created_asc },
+ 'created_date' => -> { order_created_desc },
+ 'created_desc' => -> { order_created_desc },
+ 'id_asc' => -> { order_id_asc },
+ 'id_desc' => -> { order_id_desc },
+ 'name_asc' => -> { order_name_asc },
+ 'name_desc' => -> { order_name_desc },
+ 'updated_asc' => -> { order_updated_asc },
+ 'updated_desc' => -> { order_updated_desc }
+ }
end
private
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 25373c7a1f7..fa29a83e517 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -170,8 +170,10 @@ class Environment < ApplicationRecord
prometheus_adapter.query(:environment, self) if has_metrics?
end
- def additional_metrics
- prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics?
+ def additional_metrics(*args)
+ return unless has_metrics?
+
+ prometheus_adapter.query(:additional_metrics_environment, self, *args.map(&:to_f))
end
# rubocop: disable CodeReuse/ServiceClass
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index c5b2492bbf6..572cb12b26a 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -4,7 +4,7 @@ class GlobalLabel
attr_accessor :title, :labels
alias_attribute :name, :title
- delegate :color, :text_color, :description, to: :@first_label
+ delegate :color, :text_color, :description, :scoped_label?, to: :@first_label
def for_display
@first_label
diff --git a/app/models/group.rb b/app/models/group.rb
index c77586c4cdc..8bc9b75f0a9 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -228,22 +228,21 @@ class Group < Namespace
def has_owner?(user)
return false unless user
- members_with_parents.owners.where(user_id: user).any?
+ members_with_parents.owners.exists?(user_id: user)
end
def has_maintainer?(user)
return false unless user
- members_with_parents.maintainers.where(user_id: user).any?
+ members_with_parents.maintainers.exists?(user_id: user)
end
# @deprecated
alias_method :has_master?, :has_maintainer?
# Check if user is a last owner of the group.
- # Parent owners are ignored for nested groups.
def last_owner?(user)
- owners.include?(user) && owners.size == 1
+ has_owner?(user) && members_with_parents.owners.size == 1
end
def ldap_synced?
@@ -405,6 +404,10 @@ class Group < Namespace
Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true)
end
+ def project_creation_level
+ super || ::Gitlab::CurrentSettings.default_project_creation
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb
index 3b6b68a9c5f..d926e39f96e 100644
--- a/app/models/individual_note_discussion.rb
+++ b/app/models/individual_note_discussion.rb
@@ -14,7 +14,7 @@ class IndividualNoteDiscussion < Discussion
end
def can_convert_to_discussion?
- noteable.supports_replying_to_individual_notes? && Feature.enabled?(:reply_to_individual_notes, default_enabled: true)
+ noteable.supports_replying_to_individual_notes?
end
def convert_to_discussion!(save: false)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 268706a6aea..23ddd708396 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -13,6 +13,11 @@ class ProjectWiki
CouldNotCreateWikiError = Class.new(StandardError)
SIDEBAR = '_sidebar'
+ TITLE_ORDER = 'title'
+ CREATED_AT_ORDER = 'created_at'
+ DIRECTION_DESC = 'desc'
+ DIRECTION_ASC = 'asc'
+
# Returns a string describing what went wrong after
# an operation fails.
attr_reader :error_message
@@ -82,8 +87,15 @@ class ProjectWiki
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def pages(limit: 0)
- wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
+ def pages(limit: 0, sort: nil, direction: DIRECTION_ASC)
+ sort ||= TITLE_ORDER
+ direction_desc = direction == DIRECTION_DESC
+
+ wiki.pages(
+ limit: limit, sort: sort, direction_desc: direction_desc
+ ).map do |page|
+ WikiPage.new(self, page, true)
+ end
end
# Finds a page within the repository based on a tile
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 574ce12b309..51ab2247a03 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -299,13 +299,14 @@ class Repository
end
end
- def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
+ def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:, path: nil)
raw_repository.archive_metadata(
ref,
storage_path,
project.path,
format,
- append_sha: append_sha
+ append_sha: append_sha,
+ path: path
)
end
diff --git a/app/models/serverless/function.rb b/app/models/serverless/function.rb
new file mode 100644
index 00000000000..5d4f8e0c9e2
--- /dev/null
+++ b/app/models/serverless/function.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Serverless
+ class Function
+ attr_accessor :name, :namespace
+
+ def initialize(project, name, namespace)
+ @project = project
+ @name = name
+ @namespace = namespace
+ end
+
+ def id
+ @project.id.to_s + "/" + @name + "/" + @namespace
+ end
+
+ def self.find_by_id(id)
+ array = id.split("/")
+ project = Project.find_by_id(array[0])
+ name = array[1]
+ namespace = array[2]
+
+ self.new(project, name, namespace)
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index b426d100537..d3524bfd6ae 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -105,6 +105,7 @@ class User < ApplicationRecord
has_many :groups, through: :group_members
has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
+ has_many :developer_groups, -> { where(members: { access_level: ::Gitlab::Access::DEVELOPER }) }, through: :group_members, source: :group
has_many :owned_or_maintainers_groups,
-> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
@@ -159,7 +160,7 @@ class User < ApplicationRecord
# Validations
#
# Note: devise :validatable above adds validations for :email and :password
- validates :name, presence: true
+ validates :name, presence: true, length: { maximum: 128 }
validates :email, confirmation: true
validates :notification_email, presence: true
validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email }
@@ -883,7 +884,12 @@ class User < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def several_namespaces?
- owned_groups.any? || maintainers_groups.any?
+ union_sql = ::Gitlab::SQL::Union.new(
+ [owned_groups,
+ maintainers_groups,
+ groups_with_developer_maintainer_project_access]).to_sql
+
+ ::Group.from("(#{union_sql}) #{::Group.table_name}").any?
end
def namespace_id
@@ -1169,12 +1175,24 @@ class User < ApplicationRecord
@manageable_namespaces ||= [namespace] + manageable_groups
end
- def manageable_groups
- Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
+ def manageable_groups(include_groups_with_developer_maintainer_access: false)
+ owned_and_maintainer_group_hierarchy = Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants
+
+ if include_groups_with_developer_maintainer_access
+ union_sql = ::Gitlab::SQL::Union.new(
+ [owned_and_maintainer_group_hierarchy,
+ groups_with_developer_maintainer_project_access]).to_sql
+
+ ::Group.from("(#{union_sql}) #{::Group.table_name}")
+ else
+ owned_and_maintainer_group_hierarchy
+ end
end
- def manageable_groups_with_routes
- manageable_groups.eager_load(:route).order('routes.path')
+ def manageable_groups_with_routes(include_groups_with_developer_maintainer_access: false)
+ manageable_groups(include_groups_with_developer_maintainer_access: include_groups_with_developer_maintainer_access)
+ .eager_load(:route)
+ .order('routes.path')
end
def namespaces
@@ -1573,4 +1591,16 @@ class User < ApplicationRecord
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
+
+ def groups_with_developer_maintainer_project_access
+ project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]
+
+ if ::Gitlab::CurrentSettings.default_project_creation == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS
+ project_creation_levels << nil
+ end
+
+ developer_groups_hierarchy = ::Gitlab::ObjectHierarchy.new(developer_groups).base_and_descendants
+ ::Group.where(id: developer_groups_hierarchy.select(:id),
+ project_creation_level: project_creation_levels)
+ end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 64daba81dcf..909da4316d0 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -28,16 +28,15 @@ class WikiPage
def self.group_by_directory(pages)
return [] if pages.blank?
- pages.sort_by { |page| [page.directory, page.slug] }
- .group_by(&:directory)
- .map do |dir, pages|
- if dir.present?
- WikiDirectory.new(dir, pages)
- else
- pages
- end
- end
- .flatten
+ pages.each_with_object([]) do |page, grouped_pages|
+ next grouped_pages << page unless page.directory.present?
+
+ directory = grouped_pages.find { |dir| dir.slug == page.directory }
+
+ next directory.pages << page if directory
+
+ grouped_pages << WikiDirectory.new(page.directory, [page])
+ end
end
def self.unhyphenize(name)
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index db49d3bed9c..eb2e536e8e9 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -35,6 +35,14 @@ class GroupPolicy < BasePolicy
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
+ condition(:create_projects_disabled) do
+ @subject.project_creation_level == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS
+ end
+
+ condition(:developer_maintainer_access) do
+ @subject.project_creation_level == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS
+ end
+
rule { public_group }.policy do
enable :read_group
enable :read_list
@@ -115,6 +123,9 @@ class GroupPolicy < BasePolicy
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
+ rule { developer & developer_maintainer_access }.enable :create_projects
+ rule { create_projects_disabled }.prevent :create_projects
+
def access_level
return GroupMember::NO_ACCESS if @user.nil?
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 75825c8fac0..26d7d6e84c4 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -204,6 +204,7 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_sentry_issue
enable :read_release
+ enable :read_prometheus
end
# We define `:public_user_access` separately because there are cases in gitlab-ee
diff --git a/app/presenters/ci/bridge_presenter.rb b/app/presenters/ci/bridge_presenter.rb
new file mode 100644
index 00000000000..ee11cffe355
--- /dev/null
+++ b/app/presenters/ci/bridge_presenter.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Ci
+ class BridgePresenter < CommitStatusPresenter
+ def detailed_status
+ @detailed_status ||= subject.detailed_status(user)
+ end
+ end
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 9ddce0d2c80..62c26809eeb 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -45,6 +45,8 @@ class BuildDetailsEntity < JobEntity
erase_project_job_path(project, build)
end
+ expose :failure_reason, if: -> (*) { build.failed? }
+
expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build|
terminal_project_job_path(project, build)
end
diff --git a/app/serializers/group_variable_entity.rb b/app/serializers/group_variable_entity.rb
index 0edab4a3092..19c5fa26f34 100644
--- a/app/serializers/group_variable_entity.rb
+++ b/app/serializers/group_variable_entity.rb
@@ -6,4 +6,5 @@ class GroupVariableEntity < Grape::Entity
expose :value
expose :protected?, as: :protected
+ expose :masked?, as: :masked
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index c3f7d4651fb..914ad628a99 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -42,6 +42,6 @@ class IssueEntity < IssuableEntity
end
expose :preview_note_path do |issue|
- preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.iid)
+ preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid)
end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index d673f8ae896..4831eb32c96 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -235,7 +235,7 @@ class MergeRequestWidgetEntity < IssuableEntity
end
expose :preview_note_path do |merge_request|
- preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.iid)
+ preview_markdown_path(merge_request.project, target_type: 'MergeRequest', target_id: merge_request.iid)
end
expose :merge_commit_path do |merge_request|
diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb
index c98dc1a1c4a..a46f8af1466 100644
--- a/app/serializers/projects/serverless/service_entity.rb
+++ b/app/serializers/projects/serverless/service_entity.rb
@@ -32,6 +32,13 @@ module Projects
service.dig('podcount')
end
+ expose :metrics_url do |service|
+ project_serverless_metrics_path(
+ request.project,
+ service.dig('environment_scope'),
+ service.dig('metadata', 'name')) + ".json"
+ end
+
expose :created_at do |service|
service.dig('metadata', 'creationTimestamp')
end
diff --git a/app/serializers/suggestion_entity.rb b/app/serializers/suggestion_entity.rb
index 4d0d4da10be..2dd62e19e29 100644
--- a/app/serializers/suggestion_entity.rb
+++ b/app/serializers/suggestion_entity.rb
@@ -3,6 +3,8 @@
class SuggestionEntity < API::Entities::Suggestion
include RequestAwareEntity
+ unexpose :from_line, :to_line, :from_content, :to_content
+ expose :diff_lines, using: DiffLineEntity
expose :current_user do
expose :can_apply do |suggestion|
Ability.allowed?(current_user, :apply_suggestion, suggestion)
diff --git a/app/serializers/suggestion_serializer.rb b/app/serializers/suggestion_serializer.rb
new file mode 100644
index 00000000000..010344f9fcd
--- /dev/null
+++ b/app/serializers/suggestion_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class SuggestionSerializer < BaseSerializer
+ entity SuggestionEntity
+
+ def represent_diff(resource)
+ represent(resource, { only: [:diff_lines] })
+ end
+end
diff --git a/app/serializers/variable_entity.rb b/app/serializers/variable_entity.rb
index 85cf367fe51..4d48e13cfca 100644
--- a/app/serializers/variable_entity.rb
+++ b/app/serializers/variable_entity.rb
@@ -6,4 +6,5 @@ class VariableEntity < Grape::Entity
expose :value
expose :protected?, as: :protected
+ expose :masked?, as: :masked
end
diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb
index c38b2656260..adaa68b1efb 100644
--- a/app/services/clusters/applications/base_helm_service.rb
+++ b/app/services/clusters/applications/base_helm_service.rb
@@ -13,16 +13,20 @@ module Clusters
def log_error(error)
meta = {
- exception: error.class.name,
error_code: error.respond_to?(:error_code) ? error.error_code : nil,
service: self.class.name,
app_id: app.id,
project_ids: app.cluster.project_ids,
- group_ids: app.cluster.group_ids,
- message: error.message
+ group_ids: app.cluster.group_ids
}
- logger.error(meta)
+ logger_meta = meta.merge(
+ exception: error.class.name,
+ message: error.message,
+ backtrace: Gitlab::Profiler.clean_backtrace(error.backtrace)
+ )
+
+ logger.error(logger_meta)
Gitlab::Sentry.track_acceptable_exception(error, extra: meta)
end
diff --git a/app/services/concerns/suggestible.rb b/app/services/concerns/suggestible.rb
index 0b9822b1909..0cba9bf1b8a 100644
--- a/app/services/concerns/suggestible.rb
+++ b/app/services/concerns/suggestible.rb
@@ -2,10 +2,17 @@
module Suggestible
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
# This translates into limiting suggestion changes to `suggestion:-100+100`.
MAX_LINES_CONTEXT = 100.freeze
+ def diff_lines
+ strong_memoize(:diff_lines) do
+ Gitlab::Diff::SuggestionDiff.new(self).diff_lines
+ end
+ end
+
def fetch_from_content
diff_file.new_blob_lines_between(from_line, to_line).join
end
diff --git a/app/services/groups/base_service.rb b/app/services/groups/base_service.rb
index 8c8acce5ca5..019cd047ae9 100644
--- a/app/services/groups/base_service.rb
+++ b/app/services/groups/base_service.rb
@@ -7,5 +7,11 @@ module Groups
def initialize(group, user, params = {})
@group, @current_user, @params = group, user, params.dup
end
+
+ private
+
+ def remove_unallowed_params
+ # overridden in EE
+ end
end
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 99ead467f74..74aad3b1c94 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -8,6 +8,8 @@ module Groups
end
def execute
+ remove_unallowed_params
+
@group = Group.new(params)
after_build_hook(@group, params)
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 787445180f0..73e1e00dc33 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -6,6 +6,7 @@ module Groups
def execute
reject_parent_id!
+ remove_unallowed_params
return false unless valid_visibility_level_change?(group, params[:visibility_level])
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 04dfcfbc22d..7a4ccf0d178 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -107,12 +107,13 @@ class IssuableBaseService < BaseService
@labels_service ||= ::Labels::AvailableLabelsService.new(current_user, parent, params)
end
- def process_label_ids(attributes, existing_label_ids: nil)
+ def process_label_ids(attributes, existing_label_ids: nil, extra_label_ids: [])
label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids)
remove_label_ids = attributes.delete(:remove_label_ids)
new_label_ids = existing_label_ids || label_ids || []
+ new_label_ids |= extra_label_ids
if add_label_ids.blank? && remove_label_ids.blank?
new_label_ids = label_ids if label_ids
@@ -147,7 +148,7 @@ class IssuableBaseService < BaseService
params.delete(:state_event)
params[:author] ||= current_user
- params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params)
+ params[:label_ids] = process_label_ids(params, extra_label_ids: issuable.label_ids.to_a)
issuable.assign_attributes(params)
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index 79c43b8e7d5..d3ef892875b 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -7,7 +7,7 @@ module MergeRequests
def execute(commit_status)
return if commit_status.allow_failure? || commit_status.retried?
- commit_status_merge_requests(commit_status) do |merge_request|
+ pipeline_merge_requests(commit_status.pipeline) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
@@ -16,7 +16,7 @@ module MergeRequests
# build is retried
#
def close(commit_status)
- commit_status_merge_requests(commit_status) do |merge_request|
+ pipeline_merge_requests(commit_status.pipeline) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 3e208241da5..8a9e5ebb014 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -55,15 +55,7 @@ module MergeRequests
end
def create_pipeline_for(merge_request, user)
- return unless Feature.enabled?(:ci_merge_request_pipeline,
- merge_request.source_project,
- default_enabled: true)
-
- ##
- # UpdateMergeRequestsWorker could be retried by an exception.
- # MR pipelines should not be recreated in such case.
- return if merge_request.merge_request_pipeline_exists?
- return if merge_request.has_no_commits?
+ return unless can_create_pipeline_for?(merge_request)
create_detached_merge_request_pipeline(merge_request, user)
end
@@ -80,6 +72,16 @@ module MergeRequests
end
end
+ def can_create_pipeline_for?(merge_request)
+ ##
+ # UpdateMergeRequestsWorker could be retried by an exception.
+ # pipelines for merge request should not be recreated in such case.
+ return false if merge_request.merge_request_pipeline_exists?
+ return false if merge_request.has_no_commits?
+
+ true
+ end
+
def can_use_merge_request_ref?(merge_request)
Feature.enabled?(:ci_use_merge_request_ref, project, default_enabled: true) &&
!merge_request.for_fork?
@@ -97,22 +99,11 @@ module MergeRequests
# rubocop: enable CodeReuse/ActiveRecord
def pipeline_merge_requests(pipeline)
- merge_requests_for(pipeline.ref).each do |merge_request|
+ pipeline.all_merge_requests.opened.each do |merge_request|
next unless pipeline == merge_request.head_pipeline
yield merge_request
end
end
-
- def commit_status_merge_requests(commit_status)
- merge_requests_for(commit_status.ref).each do |merge_request|
- pipeline = merge_request.head_pipeline
-
- next unless pipeline
- next unless pipeline.sha == commit_status.sha
-
- yield merge_request
- end
- end
end
end
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index c1655c38095..7386530f45f 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -17,7 +17,7 @@ class PreviewMarkdownService < BaseService
private
def explain_quick_actions(text)
- return text, [] unless %w(Issue MergeRequest Commit).include?(commands_target_type)
+ return text, [] unless %w(Issue MergeRequest Commit).include?(target_type)
quick_actions_service = QuickActions::InterpretService.new(project, current_user)
quick_actions_service.explain(text, find_commands_target)
@@ -30,22 +30,34 @@ class PreviewMarkdownService < BaseService
end
def find_suggestions(text)
- return [] unless params[:preview_suggestions]
+ return [] unless preview_sugestions?
- Banzai::SuggestionsParser.parse(text)
+ position = Gitlab::Diff::Position.new(new_path: params[:file_path],
+ new_line: params[:line].to_i,
+ base_sha: params[:base_sha],
+ head_sha: params[:head_sha],
+ start_sha: params[:start_sha])
+
+ Gitlab::Diff::SuggestionsParser.parse(text, position: position, project: project)
+ end
+
+ def preview_sugestions?
+ params[:preview_suggestions] &&
+ target_type == 'MergeRequest' &&
+ Ability.allowed?(current_user, :download_code, project)
end
def find_commands_target
QuickActions::TargetService
.new(project, current_user)
- .execute(commands_target_type, commands_target_id)
+ .execute(target_type, target_id)
end
- def commands_target_type
- params[:quick_actions_target_type]
+ def target_type
+ params[:target_type]
end
- def commands_target_id
- params[:quick_actions_target_id]
+ def target_id
+ params[:target_id]
end
end
diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb
new file mode 100644
index 00000000000..c5d2b84878b
--- /dev/null
+++ b/app/services/prometheus/proxy_service.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+module Prometheus
+ class ProxyService < BaseService
+ include ReactiveCaching
+ include Gitlab::Utils::StrongMemoize
+
+ self.reactive_cache_key = ->(service) { service.cache_key }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_lifetime = 1.minute
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ attr_accessor :proxyable, :method, :path, :params
+
+ PROXY_SUPPORT = {
+ 'query' => {
+ method: ['GET'],
+ params: %w(query time timeout)
+ },
+ 'query_range' => {
+ method: ['GET'],
+ params: %w(query start end step timeout)
+ }
+ }.freeze
+
+ def self.from_cache(proxyable_class_name, proxyable_id, method, path, params)
+ proxyable_class = begin
+ proxyable_class_name.constantize
+ rescue NameError
+ nil
+ end
+ return unless proxyable_class
+
+ proxyable = proxyable_class.find(proxyable_id)
+
+ new(proxyable, method, path, params)
+ end
+
+ # proxyable can be any model which responds to .prometheus_adapter
+ # like Environment.
+ def initialize(proxyable, method, path, params)
+ @proxyable = proxyable
+ @path = path
+
+ # Convert ActionController::Parameters to hash because reactive_cache_worker
+ # does not play nice with ActionController::Parameters.
+ @params = filter_params(params, path).to_hash
+
+ @method = method
+ end
+
+ def id
+ nil
+ end
+
+ def execute
+ return cannot_proxy_response unless can_proxy?
+ return no_prometheus_response unless can_query?
+
+ with_reactive_cache(*cache_key) do |result|
+ result
+ end
+ end
+
+ def calculate_reactive_cache(proxyable_class_name, proxyable_id, method, path, params)
+ return no_prometheus_response unless can_query?
+
+ response = prometheus_client_wrapper.proxy(path, params)
+
+ success(http_status: response.code, body: response.body)
+ rescue Gitlab::PrometheusClient::Error => err
+ service_unavailable_response(err)
+ end
+
+ def cache_key
+ [@proxyable.class.name, @proxyable.id, @method, @path, @params]
+ end
+
+ private
+
+ def service_unavailable_response(exception)
+ error(exception.message, :service_unavailable)
+ end
+
+ def no_prometheus_response
+ error('No prometheus server found', :service_unavailable)
+ end
+
+ def cannot_proxy_response
+ error('Proxy support for this API is not available currently')
+ end
+
+ def prometheus_adapter
+ strong_memoize(:prometheus_adapter) do
+ @proxyable.prometheus_adapter
+ end
+ end
+
+ def prometheus_client_wrapper
+ prometheus_adapter&.prometheus_client_wrapper
+ end
+
+ def can_query?
+ prometheus_adapter&.can_query?
+ end
+
+ def filter_params(params, path)
+ params.slice(*PROXY_SUPPORT.dig(path, :params))
+ end
+
+ def can_proxy?
+ PROXY_SUPPORT.dig(@path, :method)&.include?(@method)
+ end
+ end
+end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 3f503f3da28..30f7743c56e 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -26,7 +26,7 @@ module Users
end
end
- identity_attrs = params.slice(:extern_uid, :provider)
+ identity_attrs = params.slice(*identity_params)
unless identity_attrs.empty?
user.identities.build(identity_attrs)
@@ -37,6 +37,10 @@ module Users
private
+ def identity_params
+ [:extern_uid, :provider]
+ end
+
def can_create_user?
(current_user.nil? && Gitlab::CurrentSettings.allow_signup?) || current_user&.admin?
end
diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb
index a9afc104ed1..fac3c3dcb8f 100644
--- a/app/uploaders/legacy_artifact_uploader.rb
+++ b/app/uploaders/legacy_artifact_uploader.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+##
+# TODO: Remove this uploader when we remove :ci_enable_legacy_artifacts feature flag
+# See https://gitlab.com/gitlab-org/gitlab-ce/issues/58595
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 9a243e07936..00b51f92b12 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -46,6 +46,10 @@ module RecordsUploads
File.join(store_dir, filename.to_s)
end
+ def filename
+ upload&.path ? File.basename(upload.path) : super
+ end
+
private
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 8122d81f578..03ef2924617 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -5,7 +5,9 @@
.form-group
= f.label :default_branch_protection, class: 'label-bold'
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
- = render_if_exists 'admin/application_settings/project_creation_level', form: f, application_setting: @application_setting
+ .form-group
+ = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
+ = f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 5e05568e384..8fb38f6a690 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -8,7 +8,7 @@
.form-group.row.group-description-holder
= f.label :avatar, _("Group avatar"), class: 'col-form-label col-sm-2'
.col-sm-10
- = render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/choose_avatar_button', f: f
= render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index c787d7420b7..3b6fc85e70e 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -8,12 +8,12 @@
- if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
- %span.ci-build-text= subject.name
+ %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name
- else
.menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
- %span.ci-build-text= subject.name
+ %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name
- if status.has_action?
= link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index 90c59bec975..d07cbe4589c 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1,3 +1,3 @@
-= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want.')
+= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables')
diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml
index cb7779e2175..dbfa0a9e5a1 100644
--- a/app/views/ci/variables/_header.html.haml
+++ b/app/views/ci/variables/_header.html.haml
@@ -1,7 +1,7 @@
- expanded = local_assigns.fetch(:expanded)
%h4
- = _('Environment variables')
+ = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 16a7527c8ce..aecfdea10d9 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -7,12 +7,15 @@
- value = variable&.value
- is_protected_default = ci_variable_protected_by_default?
- is_protected = ci_variable_protected?(variable, only_key_value)
+- is_masked_default = true
+- is_masked = ci_variable_masked?(variable, only_key_value)
- id_input_name = "#{form_field}[variables_attributes][][id]"
- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
- key_input_name = "#{form_field}[variables_attributes][][key]"
- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
- protected_input_name = "#{form_field}[variables_attributes][][protected]"
+- masked_input_name = "#{form_field}[variables_attributes][][masked]"
%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } }
.ci-variable-row-body
@@ -22,7 +25,7 @@
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
- .ci-variable-body-item
+ .ci-variable-body-item.gl-show-field-errors
.form-control.js-secret-value-placeholder.qa-ci-variable-input-value{ class: ('hide' unless id) }
= '*' * 20
%textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ class: ('hide' if id),
@@ -30,6 +33,7 @@
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
= value
+ %p.masking-validation-error.gl-field-error.hide= s_("CiVariables|This variable will not be masked")
- unless only_key_value
.ci-variable-body-item.ci-variable-protected-item
.append-right-default
@@ -45,6 +49,20 @@
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ .ci-variable-body-item.ci-variable-masked-item
+ .append-right-default
+ = s_("CiVariable|Masked")
+ %button{ type: 'button',
+ class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_masked}",
+ "aria-label": s_("CiVariable|Toggle masked") }
+ %input{ type: "hidden",
+ class: 'js-ci-variable-input-masked js-project-feature-toggle-input',
+ name: masked_input_name,
+ value: is_masked,
+ data: { default: is_masked_default.to_s } }
+ %span.toggle-icon
+ = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
+ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
= render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable
%button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
= icon('minus-circle')
diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index 00582e19662..3e0f8955081 100644
--- a/app/views/clusters/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -7,25 +7,27 @@
- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
%p
- - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
+ - link_to_help_page = link_to(s_('ClusterIntegration|help page'),
+ help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page }
%p= link_to('Select a different Google account', @authorize_url)
-= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
- = form_errors(@gcp_cluster)
- .form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
+= bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20',
+ data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
+ = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
+ label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- if has_multiple_clusters?
- .form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
- = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
+ = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
+ class: 'label-bold' } do
+ = field.text_field :environment_scope, required: true, class: 'form-control',
+ title: 'Environment scope is required.', wrapper: false
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-bold'
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'),
+ class: 'label-bold'
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
@@ -47,9 +49,9 @@
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
- .form-group
- = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-bold'
- = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
+ = provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3',
+ title: s_('ClusterIntegration|Number of nodes must be a numerical value.'),
+ label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold'
.form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold'
@@ -64,13 +66,14 @@
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
- .form-check
- = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
- = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
- = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
+ = provider_gcp_field.check_box :legacy_abac, { label: s_('ClusterIntegration|RBAC-enabled cluster'),
+ label_class: 'label-bold' }, false, true
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md',
+ anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
.form-group
- = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
+ = field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
+ class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 136f98d0126..27b11e8469f 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,40 +1,39 @@
-= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field|
- = form_errors(@user_cluster)
- .form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
+= bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' },
+ url: clusterable.create_user_clusters_path, as: :cluster do |field|
+ = field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
+ label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- if has_multiple_clusters?
- .form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
- = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
- .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
+ = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
+ class: 'label-bold' } do
+ = field.text_field :environment_scope, required: true,
+ title: 'Environment scope is required.', wrapper: false
+ .form-text.text-muted
+ = s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
- .form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold'
- = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
-
- .form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
- = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
-
- .form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
- = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
+ = platform_kubernetes_field.url_field :api_url, required: true,
+ title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
+ label: s_('ClusterIntegration|API URL'), label_class: 'label-bold'
+ = platform_kubernetes_field.text_area :ca_cert,
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
+ label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold'
+ = platform_kubernetes_field.text_field :token, required: true,
+ title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'),
+ autocomplete: 'off', label_class: 'label-bold'
- if @user_cluster.allow_user_defined_namespace?
- .form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
- = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ = platform_kubernetes_field.text_field :namespace,
+ label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold'
- .form-group
- .form-check
- = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac'
- = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
- = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
+ = platform_kubernetes_field.form_group :authorization_type do
+ = platform_kubernetes_field.check_box :authorization_type,
+ { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
+ label_class: 'label-bold', inline: true }, 'rbac', 'abac'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md',
+ anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml
index b5ddca7ccb9..f9f8097cb38 100644
--- a/app/views/clusters/platforms/kubernetes/_form.html.haml
+++ b/app/views/clusters/platforms/kubernetes/_form.html.haml
@@ -1,58 +1,51 @@
-= form_for cluster, url: update_cluster_url_path, as: :cluster do |field|
- = form_errors(cluster)
-
- .form-group
- - if cluster.read_only_kubernetes_platform_fields?
- %label.append-bottom-10{ for: 'cluster-name' }
- = s_('ClusterIntegration|Kubernetes cluster name')
- .input-group
- %input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true }
- %span.input-group-append
- = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
- - else
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
- .input-group
- = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
+= bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' },
+ as: :cluster do |field|
+ - copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
+ class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ = field.text_field :name, class: 'js-select-on-focus cluster-name', required: true,
+ title: s_('ClusterIntegration|Cluster name is required.'),
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold',
+ input_group_class: 'gl-field-error-anchor', append: copy_name_btn
= field.fields_for :platform_kubernetes, platform do |platform_field|
- .form-group
- = platform_field.label :api_url, s_('ClusterIntegration|API URL')
- .input-group
- = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.read_only_kubernetes_platform_fields?
- - if cluster.read_only_kubernetes_platform_fields?
- %span.input-group-append
- = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
+ - copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
+ class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ = platform_field.text_field :api_url, class: 'js-select-on-focus', required: true,
+ title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ label: s_('ClusterIntegration|API URL'), label_class: 'label-bold',
+ input_group_class: 'gl-field-error-anchor', append: copy_api_url
+
+ - copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
+ class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ = platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5',
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
+ label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
+ input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn
- .form-group
- = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
- .input-group
- = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.read_only_kubernetes_platform_fields?
- - if cluster.read_only_kubernetes_platform_fields?
- %span.input-group-append.clipboard-addon
- = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
+ - show_token_btn = (platform_field.button s_('ClusterIntegration|Show'),
+ type: 'button', class: 'js-show-cluster-token btn btn-default')
+ - copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'),
+ class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
- .form-group
- = platform_field.label :token, s_('ClusterIntegration|Token')
- .input-group
- = platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.read_only_kubernetes_platform_fields?
- %span.input-group-append
- %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
- = s_('ClusterIntegration|Show')
- - if cluster.read_only_kubernetes_platform_fields?
- = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
+ = platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
+ required: true, title: s_('ClusterIntegration|Service token is required.'),
+ readonly: cluster.read_only_kubernetes_platform_fields?,
+ label: s_('ClusterIntegration|Service Token'), label_class: 'label-bold',
+ input_group_class: 'gl-field-error-anchor', append: show_token_btn + copy_token_btn
- if cluster.allow_user_defined_namespace?
- .form-group
- = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
- = platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
+ = platform_field.text_field :namespace, label: s_('ClusterIntegration|Project namespace (optional, unique)'),
+ label_class: 'label-bold'
- .form-group
- .form-check
- = platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
- = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ = platform_field.form_group :authorization_type do
+ = platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'),
+ label_class: 'label-bold', inline: true }, 'rbac', 'abac'
+ .form-text.text-muted
+ = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
+ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index ff59013ed67..7390c42aba2 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -9,6 +9,10 @@
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
%br/
%span.descr This setting can be overridden in each project.
+.form-group.row
+ = f.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'col-form-label col-sm-2'
+ .col-sm-10
+ = f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, @group.project_creation_level), {}, class: 'form-control'
.form-group.row
= f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2 pt-0'
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 51dcc9d0cda..6269678079a 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -27,7 +27,7 @@
.form-group.group-description-holder.col-sm-12
= f.label :avatar, _("Group avatar"), class: 'label-bold'
%div
- = render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/choose_avatar_button', f: f
.form-group.col-sm-12
%label.label-bold
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index 9ed71d19d32..c382a1ed168 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -23,10 +23,10 @@
.avatar-container.rect-avatar.s90
= group_icon(@group, alt: '', class: 'avatar group-avatar s90')
= f.label :avatar, _('Group avatar'), class: 'label-bold d-block'
- = render 'shared/choose_group_avatar_button', f: f
+ = render 'shared/choose_avatar_button', f: f
- if @group.avatar?
%hr
- = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted'
+ = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 6b0a6e7ed99..0a14830c666 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -18,6 +18,7 @@
%span.descr.text-muted= share_with_group_lock_help_text(@group)
= render 'groups/settings/lfs', f: f
+ = render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
diff --git a/app/views/groups/settings/_project_creation_level.html.haml b/app/views/groups/settings/_project_creation_level.html.haml
new file mode 100644
index 00000000000..9f711e6aade
--- /dev/null
+++ b/app/views/groups/settings/_project_creation_level.html.haml
@@ -0,0 +1,3 @@
+.form-group
+ = f.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'label-bold'
+ = f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, group.project_creation_level), {}, class: 'form-control'
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 4ac5a74c85c..50adc19f524 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -12,7 +12,7 @@
= @project.name
%span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
- .home-panel-metadata.d-flex.align-items-center.text-secondary
+ .home-panel-metadata.d-flex.flex-wrap.text-secondary
- if can?(current_user, :read_project, @project)
%span.text-secondary
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
@@ -20,7 +20,7 @@
%span.access-request-links.prepend-left-8
= render 'shared/members/access_request_links', source: @project
- if @project.tag_list.present?
- %span.home-panel-topic-list.d-inline-flex.prepend-left-8
+ %span.home-panel-topic-list.mt-2.w-100.d-inline-flex
= sprite_icon('tag', size: 16, css_class: 'icon append-right-4')
- @project.topics_to_show.each do |topic|
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 5129f11875c..1c1c7d832bd 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -19,9 +19,9 @@
= root_url
- namespace_id = namespace_id_from(params)
= f.select(:namespace_id,
- namespaces_options(namespace_id || :current_user,
- display_path: true,
- extra_group: namespace_id),
+ namespaces_options_with_developer_maintainer_access(selected: namespace_id,
+ display_path: true,
+ extra_group: namespace_id),
{},
{ class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }})
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
index 6d6bd79bc3c..07b9378ba97 100644
--- a/app/views/projects/blob/viewers/_route_map.html.haml
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
-= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index a5f73fb0197..f11c047e85a 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
= icon('spinner spin fw')
Validating Route Map…
-= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages')
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 4eb53faa6ff..acd63de2277 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -8,30 +8,20 @@
%span.sr-only= _('Select Archive Format')
= sprite_icon("arrow-down")
%ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- %li.dropdown-header
- #{ _('Source code') }
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'zip'), rel: 'nofollow', download: '' do
- %span= _('Download zip')
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.gz'), rel: 'nofollow', download: '' do
- %span= _('Download tar.gz')
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.bz2'), rel: 'nofollow', download: '' do
- %span= _('Download tar.bz2')
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar'), rel: 'nofollow', download: '' do
- %span= _('Download tar')
-
+ %li.dropdown-bold-header= _('Download source code')
+ %li.dropdown-menu-content
+ = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
+ - if directory?
+ %li.separator
+ %li.dropdown-bold-header= _('Download this directory')
+ %li.dropdown-menu-content
+ = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: @path
- if pipeline && pipeline.latest_builds_with_artifacts.any?
- %li.dropdown-header Artifacts
+ %li.separator
+ %li.dropdown-bold-header= _('Download artifacts')
- unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
+ %span.unclickable= ci_status_for_statuseable(project.pipeline_for(ref))
+ %li.dropdown-header= _('Previous Artifacts')
- pipeline.latest_builds_with_artifacts.each do |job|
%li
- = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
- %span
- #{s_('DownloadArtifacts|Download')} '#{job.name}'
+ = link_to job.name, latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: ''
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
new file mode 100644
index 00000000000..47a1704f946
--- /dev/null
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -0,0 +1,9 @@
+%ul
+ %li.d-inline-block.m-0.p-0
+ = link_to 'zip', project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: 'zip'), rel: 'nofollow', download: '', class: 'btn btn-primary btn-xs'
+ %li.d-inline-block.m-0.p-0
+ = link_to 'tar.gz', project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: 'tar.gz'), rel: 'nofollow', download: '', class: 'btn btn-xs'
+ %li.d-inline-block.m-0.p-0
+ = link_to 'tar.bz2', project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: 'tar.bz2'), rel: 'nofollow', download: '', class: 'btn btn-xs'
+ %li.d-inline-block.m-0.p-0
+ = link_to 'tar', project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: 'tar'), rel: 'nofollow', download: '', class: 'btn btn-xs'
diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml
index 6dffc7c4390..566dfe798c6 100644
--- a/app/views/projects/diffs/_replaced_image_diff.html.haml
+++ b/app/views/projects/diffs/_replaced_image_diff.html.haml
@@ -35,10 +35,10 @@
.swipe.view.hide
.swipe-frame
- .frame.deleted
+ .frame.deleted.old-diff
= image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false)
- .swipe-wrap
- = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path }
+ .swipe-wrap.left-oriented
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added old-diff js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path }
%span.swipe-bar
%span.top-handle
%span.bottom-handle
diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml
index 454f814795a..daac543b939 100644
--- a/app/views/projects/diffs/_single_image_diff.html.haml
+++ b/app/views/projects/diffs/_single_image_diff.html.haml
@@ -10,5 +10,5 @@
.image.js-single-image{ data: diff_view_data }
.wrap
- single_class_name = diff_file.deleted_file? ? 'deleted' : 'added'
- = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.file_path }
+ = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} old-diff js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.file_path }
%p.image-info= number_to_human_size(blob.size)
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a6a8ca489a9..98017bea0c9 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -40,23 +40,16 @@
= f.label :tag_list, "Topics", class: 'label-bold'
= f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control"
%p.form-text.text-muted Separate topics with commas.
- %fieldset.features
- %h5.prepend-top-0= _("Project avatar")
- .form-group
- - if @project.avatar?
- .avatar-container.rect-avatar.s160.append-bottom-15
- = project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160)
- - if @project.avatar_in_git
- %p.light
- = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git }
- .prepend-top-5.append-bottom-10
- %button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...")
- %span.file_name.prepend-left-default.js-filename= _("No file chosen")
- = f.file_field :avatar, class: "js-project-avatar-input hidden"
- .form-text.text-muted= _("The maximum file size allowed is 200KB.")
- - if @project.avatar?
- %hr
- = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
+
+ .form-group.prepend-top-default.append-bottom-20
+ .avatar-container.s90
+ = project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90')
+ = f.label :avatar, _('Project avatar'), class: 'label-bold d-block'
+ = render 'shared/choose_avatar_button', f: f
+ - if @project.avatar?
+ %hr
+ = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
+
= f.submit 'Save changes', class: "btn btn-success js-btn-success-general-project-settings"
%section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) }
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
deleted file mode 100644
index 38469ed4774..00000000000
--- a/app/views/projects/issues/_closed_by_box.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-.issue-closed-by-widget.second-block
- - pluralized_mr_this = merge_request_count > 1 ? "these" : "this"
- - pluralized_mr_is = merge_request_count > 1 ? "are" : "is"
- When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically.
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
deleted file mode 100644
index 6a66c2e57cc..00000000000
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- if @merge_requests.any?
- .card-slim.mt-3
- .card-header
- %h2.card-title.mt-0.mb-0.h5.merge-requests-title
- %span.mr-1.bold
- = _('Related merge requests')
- .d-inline-flex.lh-100.align-middle
- .mr-count-badge
- .mr-count-badge-count
- = sprite_icon('merge-request', size: 16, css_class: 'mr-1 text-secondary')
- = @merge_requests.count
- %ul.content-list.related-items-list
- - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- - @merge_requests.each do |merge_request|
- - merge_request = merge_request.present(current_user: current_user)
- %li.list-item.py-0.px-0
- .item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
- .item-contents
- .item-title.d-flex.align-items-center.mr-title
- = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-none d-xl-block append-right-8' }
- = link_to merge_request.title, merge_request_path(merge_request), { class: 'mr-title-link'}
- .item-meta
- = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-xl-none d-lg-block append-right-5' }
- %span.d-flex.align-items-center.append-right-8.mr-item-path.item-path-id.mt-0
- %span.path-id-text.bold.text-truncate{ data: { toggle: 'tooltip'}, title: merge_request.target_project.full_path }
- = merge_request.target_project.full_path
- = merge_request.to_reference
- %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
- - if merge_request.can_read_pipeline?
- = render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), tooltip_placement: 'bottom'
- - elsif has_any_head_pipeline
- = icon('blank fw')
-
- - if @closed_by_merge_requests.present?
- %p
- = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f8969fbb5a2..2c95ac6dbb3 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -77,12 +77,11 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- #merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } }
- // This element is filled in using JavaScript.
+ #js-related-merge-requests{ data: { endpoint: api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
- // This element is filled in using JavaScript.
+ -# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky
.row
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 475bae887ec..81a53f22f67 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -8,6 +8,7 @@
%div{ class: container_class }
#js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json),
+ deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'),
runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'),
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'),
build_options: javascript_build_options } }
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index 9da42fe99ac..4d1d078661d 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -2,10 +2,6 @@
- page_title _('CI / CD Charts')
%div{ class: container_class }
- .sub-header-block
- .oneline
- = _("A collection of graphs regarding Continuous Integration")
-
#charts.ci-charts
.row
.col-md-6
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 41fe704601e..bfcaa09ae8c 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -10,7 +10,7 @@
= form_errors(@pipeline)
.form-group.row
.col-sm-12
- = f.label :ref, s_('Pipeline|Create for'), class: 'col-form-label'
+ = f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide monospace',
@@ -28,7 +28,7 @@
= (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
.form-actions
- = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
+ = f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right'
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
index 635580eac5c..9c69aedfbfc 100644
--- a/app/views/projects/serverless/functions/index.html.haml
+++ b/app/views/projects/serverless/functions/index.html.haml
@@ -5,7 +5,10 @@
- status_path = project_serverless_functions_path(@project, format: :json)
- clusters_path = project_clusters_path(@project)
-.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } }
+.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path,
+ installed: @installed,
+ clusters_path: clusters_path,
+ help_path: help_page_path('user/project/clusters/serverless/index') } }
%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
.js-serverless-functions-notice
diff --git a/app/views/projects/serverless/functions/show.html.haml b/app/views/projects/serverless/functions/show.html.haml
index 29737b7014a..d1fe208ce60 100644
--- a/app/views/projects/serverless/functions/show.html.haml
+++ b/app/views/projects/serverless/functions/show.html.haml
@@ -1,14 +1,19 @@
- @no_container = true
- @content_class = "limit-container-width" unless fluid_layout
+- clusters_path = project_clusters_path(@project)
+- help_path = help_page_path('user/project/clusters/serverless/index')
- add_to_breadcrumbs('Serverless', project_serverless_functions_path(@project))
- page_title @service[:name]
-.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json } }
+.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json,
+ prometheus: @prometheus,
+ clusters_path: clusters_path,
+ help_path: help_path } }
+
%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] }
- .top-area.adjust
- .serverless-function-details#js-serverless-function-details
+ .serverless-function-details#js-serverless-function-details
.js-serverless-function-notice
.flash-container
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index fac68a36e79..fe74dc122c3 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -17,10 +17,11 @@
= s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
.card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' }
- %p.settings-message.text-center
- - kubernetes_cluster_link = help_page_path('user/project/clusters/index')
- - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link }
- = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe }
+ - if @project.all_clusters.empty?
+ %p.settings-message.text-center
+ - kubernetes_cluster_link = help_page_path('user/project/clusters/index')
+ - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link }
+ = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe }
%label.prepend-top-10
%strong= s_('CICD|Deployment strategy')
.form-check
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 94267b6e0cf..77fdf7f001c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -2,6 +2,7 @@
- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
- breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki")
+- sort_title = wiki_sort_title(params[:sort])
%div{ class: container_class }
.wiki-page-header
@@ -15,6 +16,18 @@
= icon('cloud-download')
= _("Clone repository")
+ .dropdown.inline.wiki-sort-dropdown
+ .btn-group{ role: 'group' }
+ .btn-group{ role: 'group' }
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
+ = sort_title
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
+ %li
+ = sortable_item(s_("Wiki|Title"), project_wikis_pages_path(@project, sort: ProjectWiki::TITLE_ORDER), sort_title)
+ = sortable_item(s_("Wiki|Created date"), project_wikis_pages_path(@project, sort: ProjectWiki::CREATED_AT_ORDER), sort_title)
+ = wiki_sort_controls(@project, params[:sort], params[:direction])
+
%ul.wiki-pages-list.content-list
= render @wiki_entries, context: 'pages'
diff --git a/app/views/shared/_choose_avatar_button.html.haml b/app/views/shared/_choose_avatar_button.html.haml
new file mode 100644
index 00000000000..0d46d047134
--- /dev/null
+++ b/app/views/shared/_choose_avatar_button.html.haml
@@ -0,0 +1,4 @@
+%button.btn.js-choose-avatar-button{ type: 'button' }= _("Choose file…")
+%span.file_name.js-avatar-filename= _("No file chosen")
+= f.file_field :avatar, class: "js-avatar-input hidden"
+.form-text.text-muted= _("The maximum file size allowed is 200KB.")
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
deleted file mode 100644
index 0552fe62090..00000000000
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...")
-%span.file_name.js-avatar-filename= _("No file chosen")
-= f.file_field :avatar, class: "js-group-avatar-input hidden"
-.form-text.text-muted= _("The maximum file size allowed is 200KB.")
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index b96380923ac..dbd3bbb43af 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
+ %h3.page-title Delete #{render_label(label, tooltip: false)} ?
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index c5ea15a7f63..6651f12f6de 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -7,7 +7,7 @@
- if defined?(@project)
= link_to_label(label, subject: @project, tooltip: false)
- else
- = render_colored_label(label, tooltip: false)
+ = render_label(label, tooltip: false)
.label-description
.append-right-default.prepend-left-default
- if label.description.present?
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 19159684420..47cc912a9a1 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -21,17 +21,11 @@
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
":data-selected" => "selectedLabels",
":data-labels" => "issue.assignableLabelsEndpoint",
- data: { toggle: "dropdown",
- field_name: "issue[label_names][]",
- show_no: "true",
- show_any: "true",
- project_id: @project&.try(:id),
- namespace_path: @namespace_path,
- project_path: @project.try(:path) } }
+ data: label_dropdown_data(@project, namespace_path: @namespace_path, field_name: "issue[label_names][]") }
%span.dropdown-toggle-text
{{ labelDropdownTitle }}
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
- if can?(current_user, :admin_label, current_board_parent)
- = render partial: "shared/issuable/label_page_create"
+ = render partial: "shared/issuable/label_page_create", locals: { show_add_list: true }
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index 25df2fe5cd6..b11cb8a3076 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -5,7 +5,7 @@
- supports_quick_actions = model.new_record?
- if supports_quick_actions
- - preview_url = preview_markdown_path(project, quick_actions_target_type: model.class.name)
+ - preview_url = preview_markdown_path(project, target_type: model.class.name)
- else
- preview_url = preview_markdown_path(project)
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index fd413bd68c8..416b4a34651 100644
--- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -4,5 +4,5 @@
.dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, board.parent)
- = render partial: "shared/issuable/label_page_create"
+ = render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' }
= dropdown_loading
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d5fb85ba0f3..f2c0c77a583 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -8,7 +8,7 @@
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"}
+- dropdown_data = label_dropdown_data(@project, labels: labels_filter_path_with_defaults, default_label: "Labels")
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 55edaa7eda4..d173e3c0192 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -1,4 +1,7 @@
- show_close = local_assigns.fetch(:show_close, true)
+- show_add_list = local_assigns.fetch(:show_add_list, false)
+- add_list = local_assigns.fetch(:add_list, false)
+- add_list_class = local_assigns.fetch(:add_list_class, '')
- subject = @project || @group
.dropdown-page-two.dropdown-new-label
= dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
@@ -12,6 +15,11 @@
.dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
%input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') }
+ - if show_add_list
+ .dropdown-label-input{ class: add_list_class }
+ %label
+ %input.js-add-list{ type: "checkbox", name: "add_list", checked: add_list }
+ %span= _('Add list')
.clearfix
%button.btn.btn-primary.float-left.js-new-label-btn{ type: "button" }
= _('Create')
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9596c1df20e..0798b1da4b7 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -105,10 +105,8 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- - selected_labels.each do |label|
- = link_to sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label[:title]) do
- %span.badge.color-label.has-tooltip{ style: "background-color: #{label[:color]}; color: #{label[:text_color]}", title: label[:description], data: { container: "body" } }
- = label[:title]
+ - selected_labels.each do |label_hash|
+ = render_label(label_from_hash(label_hash), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]))
- else
%span.no-value
= _('None')
@@ -116,7 +114,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable_type}[label_names][]", ability_name: issuable_type, show_no: "true", show_any: "true", namespace_path: issuable_sidebar[:namespace_path], project_path: issuable_sidebar[:project_path], issue_update: issuable_sidebar[:issuable_json_path], labels: issuable_sidebar[:project_labels_path], display: 'static' } }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 7619d0a2e9c..743ee1435e8 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -4,7 +4,9 @@
.form-group.row
= f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
- = f.text_field :title, class: "form-control qa-label-title", required: true, autofocus: true
+ = f.text_field :title, class: "form-control js-label-title qa-label-title", required: true, autofocus: true
+ = render_if_exists 'shared/labels/create_label_help_text'
+
.form-group.row
= f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index eba64daaadc..5863f52aa78 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -21,8 +21,7 @@
%span.issuable-number= issuable.to_reference
- labels.each do |label|
- = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
- - render_colored_label(label)
+ = render_label(label, link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }))
%span.assignee-icon
- assignees.each do |assignee|
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 6797520650d..6b0640bd8cb 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -5,8 +5,7 @@
%li.is-not-draggable
%span.label-row
%span.label-name
- = link_to milestones_label_path(options) do
- - render_colored_label(label, tooltip: false)
+ = render_label(label, tooltip: false, link: milestones_label_path(options))
%span.prepend-description-left
= markdown_field(label, :description)
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 6a1eea85fde..d91bc6e57c9 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -1,7 +1,7 @@
- supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true)
- supports_quick_actions = note_supports_quick_actions?(@note)
- if supports_quick_actions
- - preview_url = preview_markdown_path(@project, quick_actions_target_type: @note.noteable_type, quick_actions_target_id: @note.noteable_id)
+ - preview_url = preview_markdown_path(@project, target_type: @note.noteable_type, target_id: @note.noteable_id)
- else
- preview_url = preview_markdown_path(@project)
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 9ec8bcca4f3..b30864db802 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -3,7 +3,6 @@
class ReactiveCachingWorker
include ApplicationWorker
- # rubocop: disable CodeReuse/ActiveRecord
def perform(class_name, id, *args)
klass = begin
class_name.constantize
@@ -12,7 +11,9 @@ class ReactiveCachingWorker
end
return unless klass
- klass.find_by(klass.primary_key => id).try(:exclusively_update_reactive_cache!, *args)
+ klass
+ .reactive_cache_worker_finder
+ .call(id, *args)
+ .try(:exclusively_update_reactive_cache!, *args)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/changelogs/unreleased/13784-validate-variables-for-masking.yml b/changelogs/unreleased/13784-validate-variables-for-masking.yml
new file mode 100644
index 00000000000..e8e97fac3d2
--- /dev/null
+++ b/changelogs/unreleased/13784-validate-variables-for-masking.yml
@@ -0,0 +1,5 @@
+---
+title: Add control for masking variable values in runner logs
+merge_request: 26751
+author:
+type: added
diff --git a/changelogs/unreleased/24704-download-repository-path.yml b/changelogs/unreleased/24704-download-repository-path.yml
new file mode 100644
index 00000000000..ff3082bec45
--- /dev/null
+++ b/changelogs/unreleased/24704-download-repository-path.yml
@@ -0,0 +1,5 @@
+---
+title: Download a folder from repository
+merge_request: 26532
+author: kiameisomabes
+type: added
diff --git a/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml b/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml
new file mode 100644
index 00000000000..5942860a20f
--- /dev/null
+++ b/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml
@@ -0,0 +1,5 @@
+---
+title: Show download diff links for closed MRs
+merge_request: 26772
+author:
+type: changed
diff --git a/changelogs/unreleased/38564-cant-leave-subgroup.yml b/changelogs/unreleased/38564-cant-leave-subgroup.yml
new file mode 100644
index 00000000000..a6397062343
--- /dev/null
+++ b/changelogs/unreleased/38564-cant-leave-subgroup.yml
@@ -0,0 +1,5 @@
+---
+title: Allow removing last owner from subgroup if parent group has owners
+merge_request: 26718
+author:
+type: changed
diff --git a/changelogs/unreleased/48090-filter-sensitive-metric-labels.yml b/changelogs/unreleased/48090-filter-sensitive-metric-labels.yml
new file mode 100644
index 00000000000..e588fa79619
--- /dev/null
+++ b/changelogs/unreleased/48090-filter-sensitive-metric-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Remove `path` and `branch` labels from metrics
+merge_request: 26744
+author:
+type: fixed
diff --git a/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml b/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml
new file mode 100644
index 00000000000..5dd25d0ffc1
--- /dev/null
+++ b/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Fix long label overflow on metrics dashboard
+merge_request: 26775
+author:
+type: fixed
diff --git a/changelogs/unreleased/54417-graphql-type-authorization.yml b/changelogs/unreleased/54417-graphql-type-authorization.yml
new file mode 100644
index 00000000000..528b58a858a
--- /dev/null
+++ b/changelogs/unreleased/54417-graphql-type-authorization.yml
@@ -0,0 +1,5 @@
+---
+title: GraphQL Types can be made to always authorize access to resources of that Type
+merge_request: 25724
+author:
+type: added
diff --git a/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml
new file mode 100644
index 00000000000..3e3784d5413
--- /dev/null
+++ b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml
@@ -0,0 +1,5 @@
+---
+title: Show error when namespace/svc account missing
+merge_request: 26362
+author:
+type: added
diff --git a/changelogs/unreleased/55964-fix-email-encoding.yml b/changelogs/unreleased/55964-fix-email-encoding.yml
new file mode 100644
index 00000000000..2195a853702
--- /dev/null
+++ b/changelogs/unreleased/55964-fix-email-encoding.yml
@@ -0,0 +1,5 @@
+---
+title: Fix notfication emails having wrong encoding
+merge_request: 26931
+author:
+type: fixed
diff --git a/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml b/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml
new file mode 100644
index 00000000000..18bd51711d9
--- /dev/null
+++ b/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix image diff swipe view on commit and compare pages"
+merge_request: 26968
+author: ftab
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml b/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml
new file mode 100644
index 00000000000..a6953a68a76
--- /dev/null
+++ b/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve "Hide Kubernetes cluster warning if project has cluster related"
+merge_request: 26749
+author:
+type: fixed
diff --git a/changelogs/unreleased/57364-improve-diff-nav-header.yml b/changelogs/unreleased/57364-improve-diff-nav-header.yml
new file mode 100644
index 00000000000..95d119b949c
--- /dev/null
+++ b/changelogs/unreleased/57364-improve-diff-nav-header.yml
@@ -0,0 +1,5 @@
+---
+title: Make stylistic improvements to diff nav header
+merge_request: 26557
+author:
+type: fixed
diff --git a/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml b/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml
new file mode 100644
index 00000000000..c188d59fe94
--- /dev/null
+++ b/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml
@@ -0,0 +1,5 @@
+---
+title: Create a shortcut for a new MR in the Web IDE
+merge_request: 26792
+author:
+type: added
diff --git a/changelogs/unreleased/57493-add-limit-to-user-name.yml b/changelogs/unreleased/57493-add-limit-to-user-name.yml
new file mode 100644
index 00000000000..e6c78572d23
--- /dev/null
+++ b/changelogs/unreleased/57493-add-limit-to-user-name.yml
@@ -0,0 +1,5 @@
+---
+title: Set user.name limit to 128 characters
+merge_request: 26146
+author:
+type: changed
diff --git a/changelogs/unreleased/57602-create-cluster-validations.yml b/changelogs/unreleased/57602-create-cluster-validations.yml
new file mode 100644
index 00000000000..35349c1e9f4
--- /dev/null
+++ b/changelogs/unreleased/57602-create-cluster-validations.yml
@@ -0,0 +1,5 @@
+---
+title: Display cluster form validation error messages inline
+merge_request: 26502
+author:
+type: changed
diff --git a/changelogs/unreleased/57668-create-file-from-url.yml b/changelogs/unreleased/57668-create-file-from-url.yml
new file mode 100644
index 00000000000..b6033fa24ca
--- /dev/null
+++ b/changelogs/unreleased/57668-create-file-from-url.yml
@@ -0,0 +1,5 @@
+---
+title: Implemented support for creation of new files from URL in Web IDE
+merge_request: 26622
+author:
+type: added
diff --git a/changelogs/unreleased/58375-api-controller.yml b/changelogs/unreleased/58375-api-controller.yml
new file mode 100644
index 00000000000..60f21b37ae7
--- /dev/null
+++ b/changelogs/unreleased/58375-api-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Add a Prometheus API per environment
+merge_request: 26841
+author:
+type: added
diff --git a/changelogs/unreleased/58375-reactive-caching-changes.yml b/changelogs/unreleased/58375-reactive-caching-changes.yml
new file mode 100644
index 00000000000..cf73736b8ef
--- /dev/null
+++ b/changelogs/unreleased/58375-reactive-caching-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Allow reactive caching to be used in services
+merge_request: 26839
+author:
+type: added
diff --git a/changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml b/changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml
new file mode 100644
index 00000000000..058a120b500
--- /dev/null
+++ b/changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml
@@ -0,0 +1,5 @@
+---
+title: Add initial complexity limits to GraphQL queries
+merge_request: 26629
+author:
+type: performance
diff --git a/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml b/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml
new file mode 100644
index 00000000000..9f5881966c7
--- /dev/null
+++ b/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml
@@ -0,0 +1,5 @@
+---
+title: Allow task lists that follow a blockquote to work correctly
+merge_request: 26937
+author:
+type: fixed
diff --git a/changelogs/unreleased/58835-button-run-pipeline.yml b/changelogs/unreleased/58835-button-run-pipeline.yml
new file mode 100644
index 00000000000..39407a60780
--- /dev/null
+++ b/changelogs/unreleased/58835-button-run-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Changed button label at /pipelines/new
+merge_request: 26893
+author: antfobe,leonardofl
+type: other
diff --git a/changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml b/changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml
new file mode 100644
index 00000000000..2c6edf45ae2
--- /dev/null
+++ b/changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically set Prometheus step interval
+merge_request: 26441
+author:
+type: changed
diff --git a/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml b/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml
new file mode 100644
index 00000000000..9ab8d2b8596
--- /dev/null
+++ b/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml
@@ -0,0 +1,5 @@
+---
+title: Fix multiple series queries on metrics dashboard
+merge_request: 26514
+author:
+type: fixed
diff --git a/changelogs/unreleased/60068-avoid-null-domain-help-text.yml b/changelogs/unreleased/60068-avoid-null-domain-help-text.yml
new file mode 100644
index 00000000000..5305b8584a8
--- /dev/null
+++ b/changelogs/unreleased/60068-avoid-null-domain-help-text.yml
@@ -0,0 +1,5 @@
+---
+title: Do not display Ingress IP help text when there isn’t an Ingress IP assigned
+merge_request: 27057
+author:
+type: fixed
diff --git a/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml b/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml
new file mode 100644
index 00000000000..b773eb2720c
--- /dev/null
+++ b/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml
@@ -0,0 +1,5 @@
+---
+title: Rewrite related MRs widget with Vue
+merge_request: 27027
+author:
+type: other
diff --git a/changelogs/unreleased/add_backtrace_to_kubernetes_log.yml b/changelogs/unreleased/add_backtrace_to_kubernetes_log.yml
new file mode 100644
index 00000000000..26b8ac4b1ef
--- /dev/null
+++ b/changelogs/unreleased/add_backtrace_to_kubernetes_log.yml
@@ -0,0 +1,5 @@
+---
+title: Show error backtrace when logging errors to kubernetes.log
+merge_request: 25726
+author:
+type: other
diff --git a/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml b/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml
new file mode 100644
index 00000000000..61236b9b82b
--- /dev/null
+++ b/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Enable FindCommit caching for project and commits pages
+merge_request: 27048
+author:
+type: performance
diff --git a/changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml b/changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml
new file mode 100644
index 00000000000..731c9c10b00
--- /dev/null
+++ b/changelogs/unreleased/allow-to-use-untrusted-ruby-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to use untrusted Regexp via feature flag
+merge_request: 26905
+author:
+type: deprecated
diff --git a/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml
new file mode 100644
index 00000000000..10475824a75
--- /dev/null
+++ b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml
@@ -0,0 +1,5 @@
+---
+title: Change project avatar remove button to a link
+merge_request: 26589
+author:
+type: other
diff --git a/changelogs/unreleased/create-label-and-list-checkbox.yml b/changelogs/unreleased/create-label-and-list-checkbox.yml
new file mode 100644
index 00000000000..330372df1be
--- /dev/null
+++ b/changelogs/unreleased/create-label-and-list-checkbox.yml
@@ -0,0 +1,5 @@
+---
+title: 'Added "Add List" checkbox to create label dropdown to make creation of list optional'
+merge_request: 25716
+author: Tucker Chapman
+type: fixed
diff --git a/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml b/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml
new file mode 100644
index 00000000000..d99187d8d41
--- /dev/null
+++ b/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Drop legacy artifacts usage as there are no leftovers
+merge_request: 24294
+author:
+type: performance
diff --git a/changelogs/unreleased/duplicate-related-mrs.yml b/changelogs/unreleased/duplicate-related-mrs.yml
new file mode 100644
index 00000000000..0f5f6ede9f8
--- /dev/null
+++ b/changelogs/unreleased/duplicate-related-mrs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicates from issue related merge requests
+merge_request: 27067
+author:
+type: fixed
diff --git a/changelogs/unreleased/extend-cte-optimisations-to-projects.yml b/changelogs/unreleased/extend-cte-optimisations-to-projects.yml
new file mode 100644
index 00000000000..e5407127b2f
--- /dev/null
+++ b/changelogs/unreleased/extend-cte-optimisations-to-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up filtering issues in a project when searching
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml b/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml
new file mode 100644
index 00000000000..81cf5cb810d
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml
@@ -0,0 +1,5 @@
+---
+title: Use gitlabktl to build and deploy GitLab Serverless Functions
+merge_request: 26926
+author:
+type: added
diff --git a/changelogs/unreleased/fix-UI-links-to-route-map-info.yml b/changelogs/unreleased/fix-UI-links-to-route-map-info.yml
new file mode 100644
index 00000000000..bb506507080
--- /dev/null
+++ b/changelogs/unreleased/fix-UI-links-to-route-map-info.yml
@@ -0,0 +1,5 @@
+---
+title: Fix UI anchor links after docs refactor
+merge_request: 26890
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-include-ci-yaml.yml b/changelogs/unreleased/fix-include-ci-yaml.yml
new file mode 100644
index 00000000000..042413b89aa
--- /dev/null
+++ b/changelogs/unreleased/fix-include-ci-yaml.yml
@@ -0,0 +1,5 @@
+---
+title: Fix single string values for the 'include' keyword validation of gitlab-ci.yml.
+merge_request: 26998
+author: Paul Bonaud (@paulrbr)
+type: fixed
diff --git a/changelogs/unreleased/fix-issues-time-counter.yml b/changelogs/unreleased/fix-issues-time-counter.yml
new file mode 100644
index 00000000000..76f17063db5
--- /dev/null
+++ b/changelogs/unreleased/fix-issues-time-counter.yml
@@ -0,0 +1,5 @@
+---
+title: Make time counters show 'just now' for everything under one minute
+merge_request: 25992
+author: Sergiu Marton
+type: changed
diff --git a/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml b/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml
new file mode 100644
index 00000000000..9ccc79109d8
--- /dev/null
+++ b/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml
@@ -0,0 +1,5 @@
+---
+title: Fix MWPS does not work for merge request pipelines
+merge_request: 26906
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-missing-border.yml b/changelogs/unreleased/fix-missing-border.yml
deleted file mode 100644
index 21728223cb8..00000000000
--- a/changelogs/unreleased/fix-missing-border.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes missing border color in discussion card component
-merge_request: 26242
-author: Farhad Yasir
-type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.32.0.yml b/changelogs/unreleased/gitaly-version-v1.32.0.yml
new file mode 100644
index 00000000000..8413f31278e
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.32.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.32.0
+merge_request: 26989
+author:
+type: changed
diff --git a/changelogs/unreleased/gitaly-version-v1.33.0.yml b/changelogs/unreleased/gitaly-version-v1.33.0.yml
new file mode 100644
index 00000000000..d21e521a0bb
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.33.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.33.0
+merge_request: 27065
+author:
+type: changed
diff --git a/changelogs/unreleased/graphql-prometheus.yml b/changelogs/unreleased/graphql-prometheus.yml
new file mode 100644
index 00000000000..180577f3aec
--- /dev/null
+++ b/changelogs/unreleased/graphql-prometheus.yml
@@ -0,0 +1,5 @@
+---
+title: Added prometheus monitoring to GraphQL
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/id-51433-sort-wiki-by-date.yml b/changelogs/unreleased/id-51433-sort-wiki-by-date.yml
new file mode 100644
index 00000000000..86fcf195fa7
--- /dev/null
+++ b/changelogs/unreleased/id-51433-sort-wiki-by-date.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to sort wiki pages by date and title
+merge_request: 25365
+author:
+type: added
diff --git a/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml b/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml
new file mode 100644
index 00000000000..8f4f49896d7
--- /dev/null
+++ b/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml
@@ -0,0 +1,5 @@
+---
+title: Fix IDE detection of MR from fork with same branch name
+merge_request: 26986
+author:
+type: fixed
diff --git a/changelogs/unreleased/knative-prometheus.yml b/changelogs/unreleased/knative-prometheus.yml
new file mode 100644
index 00000000000..e24f53b7225
--- /dev/null
+++ b/changelogs/unreleased/knative-prometheus.yml
@@ -0,0 +1,5 @@
+---
+title: Add Knative metrics to Prometheus
+merge_request: 24663
+author: Chris Baumbauer <cab@cabnetworks.net>
+type: added
diff --git a/changelogs/unreleased/localize-notification-dropdown.yml b/changelogs/unreleased/localize-notification-dropdown.yml
new file mode 100644
index 00000000000..9599aaf344b
--- /dev/null
+++ b/changelogs/unreleased/localize-notification-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Localize notifications dropdown
+merge_request: 26844
+author:
+type: changed
diff --git a/changelogs/unreleased/minimized-multiple-queries-ce.yml b/changelogs/unreleased/minimized-multiple-queries-ce.yml
new file mode 100644
index 00000000000..d8c20d492d6
--- /dev/null
+++ b/changelogs/unreleased/minimized-multiple-queries-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Support multiple queries per chart on metrics dash
+merge_request: 25758
+author:
+type: added
diff --git a/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml
new file mode 100644
index 00000000000..34fd0c1b787
--- /dev/null
+++ b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml
@@ -0,0 +1,5 @@
+---
+title: Move allow developers to create projects in groups to Core
+merge_request: 25975
+author:
+type: added
diff --git a/changelogs/unreleased/osw-support-multi-line-suggestions.yml b/changelogs/unreleased/osw-support-multi-line-suggestions.yml
new file mode 100644
index 00000000000..8c8206c3822
--- /dev/null
+++ b/changelogs/unreleased/osw-support-multi-line-suggestions.yml
@@ -0,0 +1,5 @@
+---
+title: Support multi-line suggestions
+merge_request: 25211
+author:
+type: added
diff --git a/changelogs/unreleased/recreate-all-diffs-on-import.yml b/changelogs/unreleased/recreate-all-diffs-on-import.yml
deleted file mode 100644
index fd9124372f3..00000000000
--- a/changelogs/unreleased/recreate-all-diffs-on-import.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Force to recreate all MR diffs on import
-merge_request: 26480
-author:
-type: fixed
diff --git a/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml b/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml
new file mode 100644
index 00000000000..0e090592101
--- /dev/null
+++ b/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml
@@ -0,0 +1,5 @@
+---
+title: Removes the undescriptive CI Charts header
+merge_request: !26869
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-fix-project-branches-merge-status.yml b/changelogs/unreleased/sh-fix-project-branches-merge-status.yml
deleted file mode 100644
index 65f41b3faf9..00000000000
--- a/changelogs/unreleased/sh-fix-project-branches-merge-status.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix API /project/:id/branches not returning correct merge status
-merge_request: 26785
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml b/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml
new file mode 100644
index 00000000000..3d1501cd667
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml
@@ -0,0 +1,5 @@
+---
+title: Fix real-time updates for projects that contain a reserved word
+merge_request: 27060
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-ref-name-caching.yml b/changelogs/unreleased/sh-fix-ref-name-caching.yml
new file mode 100644
index 00000000000..6abd86688b4
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-ref-name-caching.yml
@@ -0,0 +1,5 @@
+---
+title: Fix and expand Gitaly FindCommit caching
+merge_request: 27018
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-rugged-tree-entries.yml b/changelogs/unreleased/sh-fix-rugged-tree-entries.yml
deleted file mode 100644
index 97b27678905..00000000000
--- a/changelogs/unreleased/sh-fix-rugged-tree-entries.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid excessive recursive calls with Rugged TreeEntries
-merge_request: 26813
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-force-gc-after-import.yml b/changelogs/unreleased/sh-force-gc-after-import.yml
deleted file mode 100644
index 755d66c1607..00000000000
--- a/changelogs/unreleased/sh-force-gc-after-import.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Force a full GC after importing a project
-merge_request: 26803
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-update-rails-5-0-7-2.yml b/changelogs/unreleased/sh-update-rails-5-0-7-2.yml
new file mode 100644
index 00000000000..b0bc08d4760
--- /dev/null
+++ b/changelogs/unreleased/sh-update-rails-5-0-7-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update Rails to 5.0.7.2
+merge_request: 27022
+author:
+type: security
diff --git a/changelogs/unreleased/stop-signing-avatar-paths.yml b/changelogs/unreleased/stop-signing-avatar-paths.yml
new file mode 100644
index 00000000000..2c2493f0f21
--- /dev/null
+++ b/changelogs/unreleased/stop-signing-avatar-paths.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up generation of avatar URLs when using object storage
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/update-gitlab-shell.yml b/changelogs/unreleased/update-gitlab-shell.yml
new file mode 100644
index 00000000000..446a5fe1ea9
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-shell.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Shell to v9.0.0
+merge_request: 27002
+author:
+type: other
diff --git a/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml b/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml
new file mode 100644
index 00000000000..124584c9bd4
--- /dev/null
+++ b/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml
@@ -0,0 +1,5 @@
+---
+title: Move project tags to separate line
+merge_request: 26797
+author:
+type: other
diff --git a/changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml b/changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml
new file mode 100644
index 00000000000..57c9a1aaa48
--- /dev/null
+++ b/changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml
@@ -0,0 +1,5 @@
+---
+title: Group transfer now properly redirects to edit on failure
+merge_request: 26837
+author:
+type: fixed
diff --git a/config.ru b/config.ru
index 5cd79870d54..6f6fb85d8fa 100644
--- a/config.ru
+++ b/config.ru
@@ -13,10 +13,6 @@ if defined?(Unicorn)
# Max memory size (RSS) per worker
use Unicorn::WorkerKiller::Oom, min, max
end
-
- # Monkey patch for fixing Rack 2.0.6 bug:
- # https://gitlab.com/gitlab-org/gitlab-ee/issues/8539
- Unicorn::StreamInput.send(:public, :eof?) # rubocop:disable GitlabSecurity/PublicSend
end
require ::File.expand_path('../config/environment', __FILE__)
diff --git a/config/helpers/is_ee_env.js b/config/helpers/is_ee_env.js
index 1fdbca591c0..3fe9bb891eb 100644
--- a/config/helpers/is_ee_env.js
+++ b/config/helpers/is_ee_env.js
@@ -4,6 +4,6 @@ const path = require('path');
const ROOT_PATH = path.resolve(__dirname, '../..');
module.exports =
- process.env.EE !== undefined
- ? JSON.parse(process.env.EE)
+ process.env.IS_GITLAB_EE !== undefined
+ ? JSON.parse(process.env.IS_GITLAB_EE)
: fs.existsSync(path.join(ROOT_PATH, 'ee'));
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 01ffcade931..3c426cdb969 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -126,6 +126,7 @@ Settings['issues_tracker'] ||= {}
# GitLab
#
Settings['gitlab'] ||= Settingslogic.new({})
+Settings.gitlab['default_project_creation'] ||= ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb
index e653556231d..f1bc289f1f0 100644
--- a/config/initializers/graphql.rb
+++ b/config/initializers/graphql.rb
@@ -1,4 +1,7 @@
# frozen_string_literal: true
+GraphQL::ObjectType.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize))
GraphQL::Field.accepts_definitions(authorize: GraphQL::Define.assign_metadata_key(:authorize))
+
+GraphQL::Schema::Object.accepts_definition(:authorize)
GraphQL::Schema::Field.accepts_definition(:authorize)
diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb
index cb00d3cfe95..87f8e67ef1c 100644
--- a/config/initializers/premailer.rb
+++ b/config/initializers/premailer.rb
@@ -4,5 +4,6 @@ Premailer::Rails.config.merge!(
preserve_styles: true,
remove_comments: true,
remove_ids: false,
- remove_scripts: false
+ remove_scripts: false,
+ output_encoding: 'US-ASCII'
)
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 9bdaf1575e9..884868c6336 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -259,3 +259,13 @@
label: Pod average
unit: "cores"
track: canary
+ - title: "Knative function invocations"
+ y_label: "Invocations"
+ required_metrics:
+ - istio_revision_request_count
+ weight: 1
+ queries:
+ - id: system_metrics_knative_function_invocation_count
+ query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))'
+ label: invocations / minute
+ unit: requests
diff --git a/config/routes/project.rb b/config/routes/project.rb
index d60a5cc9ae8..93d168fc595 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -219,6 +219,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :metrics
get :additional_metrics
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
+
+ get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy'
end
collection do
@@ -250,7 +252,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
namespace :serverless do
- get '/functions/:environment_id/:id', to: 'functions#show'
+ 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
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 9a37856a99e..19b48845305 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -322,7 +322,7 @@ module.exports = {
}),
new webpack.DefinePlugin({
- 'process.env.EE': JSON.stringify(IS_EE),
+ 'process.env.IS_GITLAB_EE': JSON.stringify(IS_EE),
}),
].filter(Boolean),
diff --git a/db/fixtures/development/02_application_settings.rb b/db/fixtures/development/02_application_settings.rb
new file mode 100644
index 00000000000..d604f0be3cd
--- /dev/null
+++ b/db/fixtures/development/02_application_settings.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+puts "Creating the default ApplicationSetting record.".color(:green)
+Gitlab::CurrentSettings.current_application_settings
+
+# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241
+puts "Enable hashed storage for every new projects.".color(:green)
+ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
+
+print '.'
diff --git a/db/fixtures/development/02_settings.rb b/db/fixtures/development/02_settings.rb
deleted file mode 100644
index 3a4a5d436bf..00000000000
--- a/db/fixtures/development/02_settings.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-# Enable hashed storage, in development mode, for all projects by default.
-Gitlab::Seeder.quiet do
- ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache
- ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
- print '.'
-end
diff --git a/db/fixtures/development/08_settings.rb b/db/fixtures/development/08_settings.rb
deleted file mode 100644
index 141465c06cf..00000000000
--- a/db/fixtures/development/08_settings.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# We want to enable hashed storage for every new project in development
-# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241
-Gitlab::Seeder.quiet do
- ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache
- ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
- print '.'
-end
diff --git a/db/fixtures/production/001_application_settings.rb b/db/fixtures/production/001_application_settings.rb
index ab15717e9a9..cf647650142 100644
--- a/db/fixtures/production/001_application_settings.rb
+++ b/db/fixtures/production/001_application_settings.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
puts "Creating the default ApplicationSetting record.".color(:green)
Gitlab::CurrentSettings.current_application_settings
diff --git a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb
new file mode 100644
index 00000000000..87427ad2930
--- /dev/null
+++ b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDefaultProjectCreationApplicationSetting < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:application_settings, :default_project_creation)
+ add_column(:application_settings, :default_project_creation, :integer, default: 2, null: false)
+ end
+ end
+
+ def down
+ if column_exists?(:application_settings, :default_project_creation)
+ remove_column(:application_settings, :default_project_creation)
+ end
+ end
+end
diff --git a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb
new file mode 100644
index 00000000000..159e0a95ace
--- /dev/null
+++ b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddProjectCreationLevelToNamespaces < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:namespaces, :project_creation_level)
+ add_column :namespaces, :project_creation_level, :integer
+ end
+ end
+
+ def down
+ unless column_exists?(:namespaces, :project_creation_level)
+ remove_column :namespaces, :project_creation_level, :integer
+ end
+ end
+end
diff --git a/db/migrate/20190325080727_truncate_user_fullname.rb b/db/migrate/20190325080727_truncate_user_fullname.rb
new file mode 100644
index 00000000000..e5f88671eef
--- /dev/null
+++ b/db/migrate/20190325080727_truncate_user_fullname.rb
@@ -0,0 +1,21 @@
+# rubocop:disable Migration/UpdateLargeTable
+class TruncateUserFullname < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ truncated_name = Arel.sql('SUBSTRING(name from 1 for 128)')
+ where_clause = Arel.sql("LENGTH(name) > 128")
+
+ update_column_in_batches(:users, :name, truncated_name) do |table, query|
+ query.where(where_clause)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20190326164045_import_common_metrics_knative.rb b/db/migrate/20190326164045_import_common_metrics_knative.rb
new file mode 100644
index 00000000000..340ec1e1f75
--- /dev/null
+++ b/db/migrate/20190326164045_import_common_metrics_knative.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ImportCommonMetricsKnative < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ require Rails.root.join('db/importers/common_metrics_importer.rb')
+
+ DOWNTIME = false
+
+ def up
+ Importers::CommonMetricsImporter.new.execute
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb b/db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb
new file mode 100644
index 00000000000..fcd63f42b0e
--- /dev/null
+++ b/db/post_migrate/20190313092516_clean_up_noteable_id_for_notes_on_commits.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CleanUpNoteableIdForNotesOnCommits < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ TEMP_INDEX_NAME = 'index_notes_on_commit_with_null_noteable_id'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name(:notes, TEMP_INDEX_NAME)
+
+ add_concurrent_index(:notes, :id, where: "noteable_type = 'Commit' AND noteable_id IS NOT NULL", name: TEMP_INDEX_NAME)
+
+ # rubocop:disable Migration/UpdateLargeTable
+ update_column_in_batches(:notes, :noteable_id, nil, batch_size: 300) do |table, query|
+ query.where(
+ table[:noteable_type].eq('Commit').and(table[:noteable_id].not_eq(nil))
+ )
+ end
+
+ remove_concurrent_index_by_name(:notes, TEMP_INDEX_NAME)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20190325111602_rename_v2_root_namespaces.rb b/db/post_migrate/20190325111602_rename_v2_root_namespaces.rb
new file mode 100644
index 00000000000..8571bb82fa0
--- /dev/null
+++ b/db/post_migrate/20190325111602_rename_v2_root_namespaces.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameV2RootNamespaces < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::Database::RenameReservedPathsMigration::V1
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # We're taking over the /v2 namespace as it necessary for Docker client to
+ # work with GitLab as Dependency proxy for containers.
+ def up
+ disable_statement_timeout do
+ rename_root_paths 'v2'
+ end
+ end
+
+ def down
+ disable_statement_timeout do
+ revert_renames
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b20fe4b3d39..ca5b04e810a 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: 20190325165127) do
+ActiveRecord::Schema.define(version: 20190326164045) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -177,6 +177,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do
t.string "runners_registration_token_encrypted"
t.integer "local_markdown_version", default: 0, null: false
t.integer "first_day_of_week", default: 0, null: false
+ t.integer "default_project_creation", default: 2, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -1391,6 +1392,7 @@ ActiveRecord::Schema.define(version: 20190325165127) do
t.integer "cached_markdown_version"
t.string "runners_token"
t.string "runners_token_encrypted"
+ t.integer "project_creation_level"
t.boolean "auto_devops_enabled"
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
diff --git a/doc/administration/auth/google_secure_ldap.md b/doc/administration/auth/google_secure_ldap.md
new file mode 100644
index 00000000000..65a51fc4aa0
--- /dev/null
+++ b/doc/administration/auth/google_secure_ldap.md
@@ -0,0 +1,207 @@
+# Google Secure LDAP **[CORE ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46391) in GitLab 11.9.
+
+[Google Cloud Identity](https://cloud.google.com/identity/) provides a Secure
+LDAP service that can be configured with GitLab for authentication and group sync.
+
+Secure LDAP requires a slightly different configuration than standard LDAP servers.
+The steps below cover:
+
+- Configuring the Secure LDAP Client in the Google Admin console.
+- Required GitLab configuration.
+
+## Configuring Google LDAP client
+
+1. Navigate to https://admin.google.com and sign in as a GSuite domain administrator.
+
+1. Go to **Apps > LDAP > Add Client**.
+
+1. Provide an `LDAP client name` and an optional `Description`. Any descriptive
+ values are acceptable. For example, the name could be 'GitLab' and the
+ description could be 'GitLab LDAP Client'. Click the **Continue** button.
+
+ ![Add LDAP Client Step 1](img/google_secure_ldap_add_step_1.png)
+
+1. Set **Access Permission** according to your needs. You must choose either
+ 'Entire domain (GitLab)' or 'Selected organizational units' for both 'Verify user
+ credentials' and 'Read user information'. Select 'Add LDAP Client'
+
+ TIP: **Tip:** If you plan to use GitLab [LDAP Group Sync](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)
+ , turn on 'Read group information'.
+
+ ![Add LDAP Client Step 2](img/google_secure_ldap_add_step_2.png)
+
+1. Download the generated certificate. This is required for GitLab to
+ communicate with the Google Secure LDAP service. Save the downloaded certificates
+ for later use. After downloading, click the **Continue to Client Details** button.
+
+1. Expand the **Service Status** section and turn the LDAP client 'ON for everyone'.
+ After selecting 'Save', click on the 'Service Status' bar again to collapse
+ and return to the rest of the settings.
+
+1. Expand the **Authentication** section and choose 'Generate New Credentials'.
+ Copy/note these credentials for later use. After selecting 'Close', click
+ on the 'Authentication' bar again to collapse and return to the rest of the settings.
+
+Now the Google Secure LDAP Client configuration is finished. The screenshot below
+shows an example of the final settings. Continue on to configure GitLab.
+
+![LDAP Client Settings](img/google_secure_ldap_client_settings.png)
+
+## Configuring GitLab
+
+Edit GitLab configuration, inserting the access credentials and certificate
+obtained earlier.
+
+The following are the configuration keys that need to be modified using the
+values obtained during the LDAP client configuration earlier:
+
+- `bind_dn`: The access credentials username
+- `password`: The access credentials password
+- `cert`: The `.crt` file text from the downloaded certificate bundle
+- `key`: The `.key` file text from the downloaded certificate bundle
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_enabled'] = true
+ gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
+ main: # 'main' is the GitLab 'provider ID' of this LDAP server
+ label: 'Google Secure LDAP'
+
+ host: 'ldap.google.com'
+ port: 636
+ uid: 'uid'
+ bind_dn: 'DizzyHorse'
+ password: 'd6V5H8nhMUW9AuDP25abXeLd'
+ encryption: 'simple_tls'
+ verify_certificates: true
+
+ tls_options:
+ cert: |
+ -----BEGIN CERTIFICATE-----
+ MIIDbDCCAlSgAwIBAgIGAWlzxiIfMA0GCSqGSIb3DQEBCwUAMHcxFDASBgNVBAoTC0dvb2dsZSBJ
+ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UE
+ CxMGR1N1aXRlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xOTAzMTIyMTE5
+ MThaFw0yMjAzMTEyMTE5MThaMHcxFDASBgNVBAoTC0dvb2dsZSBJbmMuMRYwFAYDVQQHEw1Nb3Vu
+ dGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UECxMGR1N1aXRlMQswCQYDVQQG
+ EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ ALOTy4aC38dyjESk6N8fRsKk8DN23ZX/GaNFL5OUmmA1KWzrvVC881OzNdtGm3vNOIxr9clteEG/
+ tQwsmsJvQT5U+GkBt+tGKF/zm7zueHUYqTP7Pg5pxAnAei90qkIRFi17ulObyRHPYv1BbCt8pxNB
+ 4fG/gAXkFbCNxwh1eiQXXRTfruasCZ4/mHfX7MVm8JmWU9uAVIOLW+DSWOFhrDQduJdGBXJOyC2r
+ Gqoeg9+tkBmNH/jjxpnEkFW8q7io9DdOUqqNgoidA1h9vpKTs3084sy2DOgUvKN9uXWx14uxIyYU
+ Y1DnDy0wczcsuRt7l+EgtCEgpsLiLJQbKW+JS1UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAf60J
+ yazhbHkDKIH2gFxfm7QLhhnqsmafvl4WP7JqZt0u0KdnvbDPfokdkM87yfbKJU1MTI86M36wEC+1
+ P6bzklKz7kXbzAD4GggksAzxsEE64OWHC+Y64Tkxq2NiZTw/76POkcg9StiIXjG0ZcebHub9+Ux/
+ rTncip92nDuvgEM7lbPFKRIS/YMhLCk09B/U0F6XLsf1yYjyf5miUTDikPkov23b/YGfpc8kh6hq
+ 1kqdi6a1cYPP34eAhtRhMqcZU9qezpJF6s9EeN/3YFfKzLODFSsVToBRAdZgGHzj//SAtLyQTD4n
+ KCSvK1UmaMxNaZyTHg8JnMf0ZuRpv26iSg==
+ -----END CERTIFICATE-----
+
+ key: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzk8uGgt/HcoxEpOjfH0bCpPAz
+ dt2V/xmjRS+TlJpgNSls671QvPNTszXbRpt7zTiMa/XJbXhBv7UMLJrCb0E+VPhpAbfrRihf85u8
+ 7nh1GKkz+z4OacQJwHovdKpCERYte7pTm8kRz2L9QWwrfKcTQeHxv4AF5BWwjccIdXokF10U367m
+ rAmeP5h31+zFZvCZllPbgFSDi1vg0ljhYaw0HbiXRgVyTsgtqxqqHoPfrZAZjR/448aZxJBVvKu4
+ qPQ3TlKqjYKInQNYfb6Sk7N9POLMtgzoFLyjfbl1sdeLsSMmFGNQ5w8tMHM3LLkbe5fhILQhIKbC
+ 4iyUGylviUtVAgMBAAECggEAIPb0CQy0RJoX+q/lGbRVmnyJpYDf+115WNnl+mrwjdGkeZyqw4v0
+ BPzkWYzUFP1esJRO6buBNFybQRFdFW0z5lvVv/zzRKq71aVUBPInxaMRyHuJ8D5lIL8nDtgVOwyE
+ 7DOGyDtURUMzMjdUwoTe7K+O6QBU4X/1pVPZYgmissYSMmt68LiP8k0p601F4+r5xOi/QEy44aVp
+ aOJZBUOisKB8BmUXZqmQ4Cy05vU9Xi1rLyzkn9s7fxnZ+JO6Sd1r0Thm1mE0yuPgxkDBh/b4f3/2
+ GsQNKKKCiij/6TfkjnBi8ZvWR44LnKpu760g/K7psVNrKwqJG6C/8RAcgISWQQKBgQDop7BaKGhK
+ 1QMJJ/vnlyYFTucfGLn6bM//pzTys5Gop0tpcfX/Hf6a6Dd+zBhmC3tBmhr80XOX/PiyAIbc0lOI
+ 31rafZuD/oVx5mlIySWX35EqS14LXmdVs/5vOhsInNgNiE+EPFf1L9YZgG/zA7OUBmqtTeYIPDVC
+ 7ViJcydItQKBgQDFmK0H0IA6W4opGQo+zQKhefooqZ+RDk9IIZMPOAtnvOM7y3rSVrfsSjzYVuMS
+ w/RP/vs7rwhaZejnCZ8/7uIqwg4sdUBRzZYR3PRNFeheW+BPZvb+2keRCGzOs7xkbF1mu54qtYTa
+ HZGZj1OsD83AoMwVLcdLDgO1kw32dkS8IQKBgFRdgoifAHqqVah7VFB9se7Y1tyi5cXWsXI+Wufr
+ j9U9nQ4GojK52LqpnH4hWnOelDqMvF6TQTyLIk/B+yWWK26Ft/dk9wDdSdystd8L+dLh4k0Y+Whb
+ +lLMq2YABw+PeJUnqdYE38xsZVHoDjBsVjFGRmbDybeQxauYT7PACy3FAoGBAK2+k9bdNQMbXp7I
+ j8OszHVkJdz/WXlY1cmdDAxDwXOUGVKIlxTAf7TbiijILZ5gg0Cb+hj+zR9/oI0WXtr+mAv02jWp
+ W8cSOLS4TnBBpTLjIpdu+BwbnvYeLF6MmEjNKEufCXKQbaLEgTQ/XNlchBSuzwSIXkbWqdhM1+gx
+ EjtBAoGARAdMIiDMPWIIZg3nNnFebbmtBP0qiBsYohQZ+6i/8s/vautEHBEN6Q0brIU/goo+nTHc
+ t9VaOkzjCmAJSLPUanuBC8pdYgLu5J20NXUZLD9AE/2bBT3OpezKcdYeI2jqoc1qlWHlNtVtdqQ2
+ AcZSFJQjdg5BTyvdEDhaYUKGdRw=
+ -----END PRIVATE KEY-----
+ EOS
+ ```
+
+1. Save the file and [reconfigure] GitLab for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ ldap:
+ enabled: true
+ servers:
+ main: # 'main' is the GitLab 'provider ID' of this LDAP server
+ label: 'Google Secure LDAP'
+
+ host: 'ldap.google.com'
+ port: 636
+ uid: 'uid'
+ bind_dn: 'DizzyHorse'
+ password: 'd6V5H8nhMUW9AuDP25abXeLd'
+ encryption: 'simple_tls'
+ verify_certificates: true
+
+ tls_options:
+ cert: |
+ -----BEGIN CERTIFICATE-----
+ MIIDbDCCAlSgAwIBAgIGAWlzxiIfMA0GCSqGSIb3DQEBCwUAMHcxFDASBgNVBAoTC0dvb2dsZSBJ
+ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UE
+ CxMGR1N1aXRlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xOTAzMTIyMTE5
+ MThaFw0yMjAzMTEyMTE5MThaMHcxFDASBgNVBAoTC0dvb2dsZSBJbmMuMRYwFAYDVQQHEw1Nb3Vu
+ dGFpbiBWaWV3MRQwEgYDVQQDEwtMREFQIENsaWVudDEPMA0GA1UECxMGR1N1aXRlMQswCQYDVQQG
+ EwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ ALOTy4aC38dyjESk6N8fRsKk8DN23ZX/GaNFL5OUmmA1KWzrvVC881OzNdtGm3vNOIxr9clteEG/
+ tQwsmsJvQT5U+GkBt+tGKF/zm7zueHUYqTP7Pg5pxAnAei90qkIRFi17ulObyRHPYv1BbCt8pxNB
+ 4fG/gAXkFbCNxwh1eiQXXRTfruasCZ4/mHfX7MVm8JmWU9uAVIOLW+DSWOFhrDQduJdGBXJOyC2r
+ Gqoeg9+tkBmNH/jjxpnEkFW8q7io9DdOUqqNgoidA1h9vpKTs3084sy2DOgUvKN9uXWx14uxIyYU
+ Y1DnDy0wczcsuRt7l+EgtCEgpsLiLJQbKW+JS1UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAf60J
+ yazhbHkDKIH2gFxfm7QLhhnqsmafvl4WP7JqZt0u0KdnvbDPfokdkM87yfbKJU1MTI86M36wEC+1
+ P6bzklKz7kXbzAD4GggksAzxsEE64OWHC+Y64Tkxq2NiZTw/76POkcg9StiIXjG0ZcebHub9+Ux/
+ rTncip92nDuvgEM7lbPFKRIS/YMhLCk09B/U0F6XLsf1yYjyf5miUTDikPkov23b/YGfpc8kh6hq
+ 1kqdi6a1cYPP34eAhtRhMqcZU9qezpJF6s9EeN/3YFfKzLODFSsVToBRAdZgGHzj//SAtLyQTD4n
+ KCSvK1UmaMxNaZyTHg8JnMf0ZuRpv26iSg==
+ -----END CERTIFICATE-----
+
+ key: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzk8uGgt/HcoxEpOjfH0bCpPAz
+ dt2V/xmjRS+TlJpgNSls671QvPNTszXbRpt7zTiMa/XJbXhBv7UMLJrCb0E+VPhpAbfrRihf85u8
+ 7nh1GKkz+z4OacQJwHovdKpCERYte7pTm8kRz2L9QWwrfKcTQeHxv4AF5BWwjccIdXokF10U367m
+ rAmeP5h31+zFZvCZllPbgFSDi1vg0ljhYaw0HbiXRgVyTsgtqxqqHoPfrZAZjR/448aZxJBVvKu4
+ qPQ3TlKqjYKInQNYfb6Sk7N9POLMtgzoFLyjfbl1sdeLsSMmFGNQ5w8tMHM3LLkbe5fhILQhIKbC
+ 4iyUGylviUtVAgMBAAECggEAIPb0CQy0RJoX+q/lGbRVmnyJpYDf+115WNnl+mrwjdGkeZyqw4v0
+ BPzkWYzUFP1esJRO6buBNFybQRFdFW0z5lvVv/zzRKq71aVUBPInxaMRyHuJ8D5lIL8nDtgVOwyE
+ 7DOGyDtURUMzMjdUwoTe7K+O6QBU4X/1pVPZYgmissYSMmt68LiP8k0p601F4+r5xOi/QEy44aVp
+ aOJZBUOisKB8BmUXZqmQ4Cy05vU9Xi1rLyzkn9s7fxnZ+JO6Sd1r0Thm1mE0yuPgxkDBh/b4f3/2
+ GsQNKKKCiij/6TfkjnBi8ZvWR44LnKpu760g/K7psVNrKwqJG6C/8RAcgISWQQKBgQDop7BaKGhK
+ 1QMJJ/vnlyYFTucfGLn6bM//pzTys5Gop0tpcfX/Hf6a6Dd+zBhmC3tBmhr80XOX/PiyAIbc0lOI
+ 31rafZuD/oVx5mlIySWX35EqS14LXmdVs/5vOhsInNgNiE+EPFf1L9YZgG/zA7OUBmqtTeYIPDVC
+ 7ViJcydItQKBgQDFmK0H0IA6W4opGQo+zQKhefooqZ+RDk9IIZMPOAtnvOM7y3rSVrfsSjzYVuMS
+ w/RP/vs7rwhaZejnCZ8/7uIqwg4sdUBRzZYR3PRNFeheW+BPZvb+2keRCGzOs7xkbF1mu54qtYTa
+ HZGZj1OsD83AoMwVLcdLDgO1kw32dkS8IQKBgFRdgoifAHqqVah7VFB9se7Y1tyi5cXWsXI+Wufr
+ j9U9nQ4GojK52LqpnH4hWnOelDqMvF6TQTyLIk/B+yWWK26Ft/dk9wDdSdystd8L+dLh4k0Y+Whb
+ +lLMq2YABw+PeJUnqdYE38xsZVHoDjBsVjFGRmbDybeQxauYT7PACy3FAoGBAK2+k9bdNQMbXp7I
+ j8OszHVkJdz/WXlY1cmdDAxDwXOUGVKIlxTAf7TbiijILZ5gg0Cb+hj+zR9/oI0WXtr+mAv02jWp
+ W8cSOLS4TnBBpTLjIpdu+BwbnvYeLF6MmEjNKEufCXKQbaLEgTQ/XNlchBSuzwSIXkbWqdhM1+gx
+ EjtBAoGARAdMIiDMPWIIZg3nNnFebbmtBP0qiBsYohQZ+6i/8s/vautEHBEN6Q0brIU/goo+nTHc
+ t9VaOkzjCmAJSLPUanuBC8pdYgLu5J20NXUZLD9AE/2bBT3OpezKcdYeI2jqoc1qlWHlNtVtdqQ2
+ AcZSFJQjdg5BTyvdEDhaYUKGdRw=
+ -----END PRIVATE KEY-----
+ ```
+
+1. Save the file and [restart] GitLab for the changes to take effect.
+
+
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/auth/img/google_secure_ldap_add_step_1.png b/doc/administration/auth/img/google_secure_ldap_add_step_1.png
new file mode 100644
index 00000000000..fd254443d75
--- /dev/null
+++ b/doc/administration/auth/img/google_secure_ldap_add_step_1.png
Binary files differ
diff --git a/doc/administration/auth/img/google_secure_ldap_add_step_2.png b/doc/administration/auth/img/google_secure_ldap_add_step_2.png
new file mode 100644
index 00000000000..611a21ae03c
--- /dev/null
+++ b/doc/administration/auth/img/google_secure_ldap_add_step_2.png
Binary files differ
diff --git a/doc/administration/auth/img/google_secure_ldap_client_settings.png b/doc/administration/auth/img/google_secure_ldap_client_settings.png
new file mode 100644
index 00000000000..3c0b3f3d4bd
--- /dev/null
+++ b/doc/administration/auth/img/google_secure_ldap_client_settings.png
Binary files differ
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 440c2b1285a..2d057dc7509 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -48,6 +48,14 @@ LDAP-enabled users can always authenticate with Git using their GitLab username
or email and LDAP password, even if password authentication for Git is disabled
in the application settings.
+## Google Secure LDAP **[CORE ONLY]**
+
+> Introduced in GitLab 11.9.
+
+[Google Cloud Identity](https://cloud.google.com/identity/) provides a Secure
+LDAP service that can be configured with GitLab for authentication and group sync.
+See [Google Secure LDAP](google_secure_ldap.md) for detailed configuration instructions.
+
## Configuration
NOTE: **Note**:
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index b21bfafc096..05beb724d4d 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -362,6 +362,10 @@ configuring a different storage driver. By default the GitLab Container Registry
is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path)
configuration.
+NOTE: **Note:** Enabling a storage driver other than `filesystem` would mean
+that your Docker client needs to be able to access the storage backend directly.
+In that case, you must use an address that resolves and is accessible outside GitLab server.
+
The different supported drivers are:
| Driver | Description |
@@ -369,20 +373,16 @@ The different supported drivers are:
| filesystem | Uses a path on the local filesystem |
| azure | Microsoft Azure Blob Storage |
| gcs | Google Cloud Storage |
-| s3 | Amazon Simple Storage Service |
+| s3 | Amazon Simple Storage Service. Be sure to configure your storage bucket with the correct [S3 Permission Scopes](https://docs.docker.com/registry/storage-drivers/s3/#s3-permission-scopes). |
| swift | OpenStack Swift Object Storage |
| oss | Aliyun OSS |
Read more about the individual driver's config options in the
[Docker Registry docs][storage-config].
-> **Warning** GitLab will not backup Docker images that are not stored on the
+CAUTION: **Warning:** GitLab will not backup Docker images that are not stored on the
filesystem. Remember to enable backups with your object storage provider if
desired.
->
-> **Important** Enabling storage driver other than `filesystem` would mean
-that your Docker client needs to be able to access the storage backend directly.
-So you must use an address that resolves and is accessible outside GitLab server.
---
diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md
index 345e8646f9b..1780e1babe9 100644
--- a/doc/administration/git_protocol.md
+++ b/doc/administration/git_protocol.md
@@ -31,7 +31,7 @@ From the client side, `git` `v2.18.0` or newer must be installed.
From the server side, if we want to configure SSH we need to set the `sshd`
server to accept the `GIT_PROTOCOL` environment.
-In installations using [GitLab Helm Charts](../install/kubernetes/gitlab_chart.md)
+In installations using [GitLab Helm Charts](https://docs.gitlab.com/charts/)
and [All-in-one docker image](https://docs.gitlab.com/omnibus/docker/), the SSH
service is already configured to accept the `GIT_PROTOCOL` environment and users
need not do anything more.
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 0b7ef46888c..46f7b1d2a25 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -4,6 +4,29 @@
[ce-2640]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2640
+## Registration and authentication tokens
+
+There are two tokens to take into account when connecting a Runner with GitLab.
+
+| Token | Description |
+| ----- | ----------- |
+| Registration token | Token used to [register the Runner](https://docs.gitlab.com/runner/register/). It can be [obtained through GitLab](../ci/runners/README.md). |
+| Authentication token | Token used to authenticate the Runner with the GitLab instance. It is obtained either automatically when [registering a Runner](https://docs.gitlab.com/runner/register/), or manually when [registering the Runner via the Runners API](#register-a-new-runner). |
+
+Here's an example of how the two tokens are used in Runner registration:
+
+1. You register the Runner via the GitLab API using a registration token, and an
+ authentication token is returned.
+1. You use that authentication token and add it to the
+ [Runner's configuration file](https://docs.gitlab.com/runner/commands/#configuration-file):
+
+ ```toml
+ [[runners]]
+ token = "<authentication_token>"
+ ```
+
+GitLab and Runner are then connected.
+
## List owned runners
Get a list of specific runners available to the user.
@@ -456,7 +479,7 @@ POST /runners
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `token` | string | yes | Registration token ([Read how to obtain a token](../ci/runners/README.md)) |
+| `token` | string | yes | [Registration token](#registration-and-authentication-tokens). |
| `description`| string | no | Runner's description|
| `info` | hash | no | Runner's metadata |
| `active` | boolean| no | Whether the Runner is active |
@@ -466,7 +489,7 @@ POST /runners
| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
```
-curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=ipzXrMhuyyJPifUt6ANz" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
+curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=<registration_token>" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
```
Response:
@@ -494,10 +517,10 @@ DELETE /runners
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `token` | string | yes | Runner's authentication token |
+| `token` | string | yes | Runner's [authentication token](#registration-and-authentication-tokens). |
```
-curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=ebb6fc00521627750c8bb750f2490e"
+curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=<authentication_token>"
```
Response:
@@ -516,10 +539,10 @@ POST /runners/verify
| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
-| `token` | string | yes | Runner's authentication token |
+| `token` | string | yes | Runner's [authentication token](#registration-and-authentication-tokens). |
```
-curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=ebb6fc00521627750c8bb750f2490e"
+curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=<authentication_token>"
```
Response:
diff --git a/doc/ci/merge_request_pipelines/img/merge_request.png b/doc/ci/merge_request_pipelines/img/merge_request.png
index cf9c628e9a0..d03fdc6a885 100644
--- a/doc/ci/merge_request_pipelines/img/merge_request.png
+++ b/doc/ci/merge_request_pipelines/img/merge_request.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png b/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png
new file mode 100644
index 00000000000..58d5581f628
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/merge_request_pipeline.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png b/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png
new file mode 100644
index 00000000000..0a84e61d284
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/merge_request_pipeline_config.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index e8953d235a7..4f61e97bd8a 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -2,14 +2,16 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6.
-Usually, when you create a new merge request, a pipeline runs on the
+Usually, when you create a new merge request, a pipeline runs with the
new change and checks if it's qualified to be merged into a target branch. This
-pipeline should contain only necessary jobs for checking the new changes.
+pipeline should contain only necessary jobs for validating the new changes.
For example, unit tests, lint checks, and [Review Apps](../review_apps/index.md)
are often used in this cycle.
With pipelines for merge requests, you can design a specific pipeline structure
-for merge requests.
+for when you are running a pipeline in a merge request. This
+could be either adding or removing steps in the pipeline, to make sure that
+your pipelines are as efficient as possible.
## Configuring pipelines for merge requests
@@ -30,9 +32,7 @@ build:
stage: build
script: ./build
only:
- - branches
- - tags
- - merge_requests
+ - master
test:
stage: test
@@ -43,6 +43,8 @@ test:
deploy:
stage: deploy
script: ./deploy
+ only:
+ - master
```
After the merge request is updated with new commits:
@@ -50,18 +52,58 @@ After the merge request is updated with new commits:
- GitLab detects that changes have occurred and creates a new pipeline for the merge request.
- The pipeline fetches the latest code from the source branch and run tests against it.
-In the above example, the pipeline contains only `build` and `test` jobs.
-Since the `deploy` job doesn't have the `only: merge_requests` parameter,
-deployment jobs will not happen in the merge request.
+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,
+they will not run in the merge request.
-Pipelines tagged with the **merge request** badge indicate that they were triggered
+Pipelines tagged with the **detached** badge indicate that they were triggered
when a merge request was created or updated. For example:
![Merge request page](img/merge_request.png)
-The same tag is shown on the pipeline's details:
+## Combined ref pipelines **[PREMIUM]**
+
+> [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
+
+It's possible for your source and target branches to diverge, which can result
+in the scenario that source branch's pipeline was green, the target's pipeline was green,
+but the combined output fails. By having your merge request pipeline automatically
+create a new ref that contains the merge result of the source and target branch
+(then running a pipeline on that ref), we can better test that the combined result
+is also valid.
+
+From GitLab 11.10, pipelines for merge requests run by default
+on this merged result. That is, where the source and target branches are combined into a
+new ref and a pipeline for this ref validates the result prior to merging.
+
+![Merge request pipeline as the head pipeline](img/merge_request_pipeline.png)
+
+There are some cases where creating a combined ref is not possible or not wanted.
+For example, a source branch that has conflicts with the target branch
+or a merge request that is still in WIP status. In this case, the merge request pipeline falls back to a "detached" state
+and runs on the source branch ref as if it was a regular pipeline.
+
+The detached state serves to warn you that you are working in a situation
+subjected to merge problems, and helps to highlight that you should
+get out of WIP status or resolve merge conflicts as soon as possible.
-![Pipeline's details](img/pipeline_detail.png)
+### Enabling combined ref pipelines
+
+This feature disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186). It can be enabled at the project level:
+
+1. Visit your project's **Settings > General** and expand **Merge requests**.
+1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
+1. Click **Save changes** button.
+
+![Merge request pipeline config](img/merge_request_pipeline_config.png)
+
+### Combined ref pipeline's limitations
+
+- This feature requires [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
+- This feature requires [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer.
+- After the merge request pipeline succeeds, if the target branch has moved forward, the result of the pipeline is stale and must be retried. In busy repos, this can become a problem as it is highly probable that the target branch will have moved ahead. Improvements are [planned](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186) for future versions of GitLab.
+- Forking/cross-repo workflows are not currently supported. To follow progress, see [#9713](https://gitlab.com/gitlab-org/gitlab-ee/issues/9713).
+- This feature is not available for [fast forward merges](../../user/project/merge_requests/fast_forward_merge.md) yet. To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab-ce/issues/58226).
## Excluding certain jobs
@@ -138,3 +180,12 @@ External users could steal secret variables from the parent project by modifying
We're discussing a secure solution of running pipelines for merge requests
that submitted from forked projects,
see [the issue about the permission extension](https://gitlab.com/gitlab-org/gitlab-ce/issues/23902).
+
+## Additional predefined variables
+
+By using pipelines for merge requests, GitLab exposes additional predefined variables to the pipeline jobs.
+Those variables contain information of the associated merge request, so that it's useful
+to integrate your job with [GitLab Merge Request API](../../api/merge_requests.md).
+
+You can find the list of avilable variables in [the reference sheet](../variables/predefined_variables.md).
+The variable names begin with the `CI_MERGE_REQUEST_` prefix.
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 9dbcf9d1155..53651a807c2 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -81,7 +81,7 @@ The process of adding Review Apps in your workflow is as follows:
1. Set up the infrastructure to host and deploy the Review Apps.
1. [Install](https://docs.gitlab.com/runner/install/) and [configure](https://docs.gitlab.com/runner/commands/) a Runner to do deployment.
-1. Set up a job in `.gitlab-ci.yml` that uses the predefined [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches.
+1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches.
1. Optionally, set a job that [manually stops](../environments.md#stopping-an-environment) the Review Apps.
After adding Review Apps to your workflow, you follow the branched Git flow. That is:
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 416627740d2..b429dc8c8be 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -23,6 +23,9 @@ future GitLab releases.**
| `CHAT_INPUT` | 10.6 | all | Additional arguments passed in the [ChatOps](../chatops/README.md) command |
| `CHAT_CHANNEL` | 10.6 | all | Source chat channel which triggered the [ChatOps](../chatops/README.md) command |
| `CI` | all | 0.4 | Mark that job is executed in CI environment |
+| `CI_BUILDS_DIR` | all | 11.10 | Top-level directory where builds are executed. |
+| `CI_CONCURRENT_ID` | all | 11.10 | Unique ID of build execution within a single executor. |
+| `CI_CONCURRENT_PROJECT_ID` | all | 11.10 | Unique ID of build execution within a single executor and project. |
| `CI_COMMIT_BEFORE_SHA` | 11.2 | all | The previous latest commit present on a branch before a push request. |
| `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
| `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. |
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index ceca4af1bee..1218ac0b071 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -15,26 +15,26 @@ There are two places defined variables can be used. On the:
### `.gitlab-ci.yml` file
-| Definition | Can be expanded? | Expansion place | Description |
-|--------------------------------------|-------------------|-----------------|--------------|
-| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>Supported: all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>Not supported: variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> |
-| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
-| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
-| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
-| `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
-| `services:[]:name` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
-| `cache:key` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
-| `artifacts:name` | yes | Runner | The variable expansion is made by GitLab Runner's shell environment |
-| `script`, `before_script`, `after_script` | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) |
-| `only:variables:[]`, `except:variables:[]` | no | n/a | The variable must be in the form of `$variable`.<br/>Not supported:<ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> |
+| Definition | Can be expanded? | Expansion place | Description |
+|:-------------------------------------------|:-----------------|:-----------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<br/><br/>Supported are all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules).<br/><br/>Not supported are variables defined in Runner's `config.toml` and variables created in job's `script`. |
+| `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion doesn't support the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). |
+| `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `services:[]` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `services:[]:name` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `cache:key` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
+| `artifacts:name` | yes | Runner | The variable expansion is made by GitLab Runner's shell environment |
+| `script`, `before_script`, `after_script` | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) |
+| `only:variables:[]`, `except:variables:[]` | no | n/a | The variable must be in the form of `$variable`. Not supported are the following:<br/><br/>- Variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`).<br/>- Any other variables related to environment (currently only `CI_ENVIRONMENT_URL`).<br/>- [Persisted variables](#persisted-variables). |
### `config.toml` file
NOTE: **Note:**
You can read more about `config.toml` in the [Runner's docs](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
-| Definition | Can be expanded? | Description |
-|--------------------------------------|------------------|-------------|
+| Definition | Can be expanded? | Description |
+|:-------------------------------------|:-----------------|:---------------------------------------------------------------------------------------------------------------------------------------------|
| `runners.environment` | yes | The variable expansion is made by the Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `runners.kubernetes.pod_labels` | yes | The Variable expansion is made by the Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
| `runners.kubernetes.pod_annotations` | yes | The Variable expansion is made by the Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 83a226d3577..d52312371cd 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -414,6 +414,27 @@ job:
only: ['branches', 'tags']
```
+### Supported `only`/`except` regexp syntax
+
+CAUTION: **Warning:**
+This is a breaking change that was introduced with GitLab 11.9.4.
+
+In GitLab 11.9.4, GitLab begun internally converting regexp used
+in `only` and `except` parameters to [RE2](https://github.com/google/re2/wiki/Syntax).
+
+This means that only subset of features provided by [Ruby Regexp](https://ruby-doc.org/core/Regexp.html)
+is supported. [RE2](https://github.com/google/re2/wiki/Syntax) limits the set of features
+provided due to computational complexity, which means some features became unavailable in GitLab 11.9.4.
+For example, negative lookaheads.
+
+For GitLab versions from 11.9.7 and up to GitLab 12.0, GitLab provides a feature flag that can be
+enabled by administrators that allows users to use unsafe regexp syntax. This brings compatibility
+with previously allowed syntax version and allows users to gracefully migrate to the new syntax.
+
+```ruby
+Feature.enable(:allow_unsafe_ruby_regexp)
+```
+
### `only`/`except` (advanced)
> - `refs` and `kubernetes` policies introduced in GitLab 10.0.
@@ -501,7 +522,7 @@ Learn more about [variables expressions](../variables/README.md#variables-expres
#### `only:changes`/`except:changes`
-Using the `changes` keyword with `only` or `except`, makes it possible to define if
+Using the `changes` keyword with `only` or `except` makes it possible to define if
a job should be created based on files modified by a git push event.
For example:
@@ -518,14 +539,38 @@ docker build:
```
In the scenario above, when pushing multiple commits to GitLab to an existing
-branch, GitLab creates and triggers `docker build` job, provided that one of the
-commits contains changes to either:
+branch, GitLab creates and triggers the `docker build` job, provided that one of the
+commits contains changes to any of the following:
- The `Dockerfile` file.
- Any of the files inside `docker/scripts/` directory.
- Any of the files and subdirectories inside the `dockerfiles` directory.
- Any of the files with `rb`, `py`, `sh` extensions inside the `more_scripts` directory.
+You can also use glob patterns to match multiple files in either the root directory of the repo, or in _any_ directory within the repo. For example:
+
+```yaml
+test:
+ script: npm run test
+ only:
+ changes:
+ - "*.json"
+ - "**/*.sql"
+```
+
+NOTE: **Note:**
+In the example above, the expressions are wrapped double quotes because they are glob patterns. GitLab will fail to parse `.gitlab-ci.yml` files with unwrapped glob patterns.
+
+The following example will skip the CI job if a change is detected in any file in the root directory of the repo with a `.md` extension:
+
+```yaml
+build:
+ script: npm run build
+ except:
+ changes:
+ - "*.md"
+```
+
CAUTION: **Warning:**
There are some caveats when using this feature with new branches and tags. See
the section below.
@@ -2317,8 +2362,35 @@ variables:
GIT_STRATEGY: clone
GIT_CHECKOUT: "false"
script:
- - git checkout master
- - git merge $CI_BUILD_REF_NAME
+ - git checkout -B master origin/master
+ - git merge $CI_COMMIT_SHA
+```
+
+#### Git clean flags
+
+> Introduced in GitLab Runner 11.10
+
+The `GIT_CLEAN_FLAGS` variable is used to control the default behavior of
+`git clean` after checking out the sources. You can set it globally or per-job in the
+[`variables`](#variables) section.
+
+`GIT_CLEAN_FLAGS` accepts all possible options of the [git clean](https://git-scm.com/docs/git-clean)
+command.
+
+`git clean` is disabled if `GIT_CHECKOUT: "false"` is specified.
+
+If `GIT_CLEAN_FLAGS` is:
+
+- Not specified, `git clean` flags default to `-ffdx`.
+- Given the value `none`, `git clean` is not executed.
+
+For example:
+
+```yaml
+variables:
+ GIT_CLEAN_FLAGS: -ffdx -e cache/
+script:
+ - ls -al cache/
```
#### Job stages attempts
@@ -2394,6 +2466,72 @@ CAUTION: **Deprecated:**
`type` is deprecated, and could be removed in one of the future releases.
Use [`stage`](#stage) instead.
+## Custom build directories
+
+> [Introduced][https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1267] in Gitlab Runner 11.10
+
+NOTE: **Note:**
+This can only be used when `custom_build_dir` is enabled in the [Runner's
+configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscustom_build_dir-section).
+This is the default configuration for `docker` and `kubernetes` executor.
+
+By default, GitLab Runner clones the repository in a unique subpath of the `$CI_BUILDS_DIR` directory.
+However, sometimes your project might require the code in a specific directory,
+but sometimes your project might require to have the code in a specific directory,
+like Go projects, for example. In that case, you can specify the `GIT_CLONE_PATH` variable
+to tell the Runner in which directory to clone the repository:
+
+```yml
+variables:
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/project-name
+
+test:
+ script:
+ - pwd
+```
+
+The `GIT_CLONE_PATH` has to always be within `$CI_BUILDS_DIR`. The directory set in `$CI_BUILDS_DIR`
+is dependent on executor and configuration of [runners.builds_dir](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section)
+setting.
+
+### Handling concurrency
+
+An executor using a concurrency greater than `1` might lead
+to failures because multiple jobs might be working on the same directory if the `builds_dir`
+is shared between jobs.
+GitLab Runner does not try to prevent this situation. It is up to the administrator
+and developers to comply with the requirements of Runner configuration.
+
+To avoid this scenario, you can use a unique path within `$CI_BUILDS_DIR`, because Runner
+exposes two additional variables that provide a unique `ID` of concurrency:
+
+- `$CI_CONCURRENT_ID`: Unique ID for all jobs running within the given executor.
+- `$CI_CONCURRENT_PROJECT_ID`: Unique ID for all jobs running within the given executor and project.
+
+The most stable configuration that should work well in any scenario and on any executor
+is to use `$CI_CONCURRENT_ID` in the `GIT_CLONE_PATH`. For example:
+
+```yml
+variables:
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/project-name
+
+test:
+ script:
+ - pwd
+```
+
+The `$CI_CONCURRENT_PROJECT_ID` should be used in conjunction with `$CI_PROJECT_PATH`
+as the `$CI_PROJECT_PATH` provides a path of a repository. That is, `group/subgroup/project`. For example:
+
+```yml
+variables:
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH
+
+test:
+ script:
+ - pwd
+```
+
## Special YAML features
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 501092ff2aa..8d2bfff3a5d 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -9,38 +9,6 @@ can be shared.
It is also possible to add a `private_token` to the querystring, or
add a `HTTP_PRIVATE_TOKEN` header.
-### Authorization
-
-Fields can be authorized using the same abilities used in the Rails
-app. This can be done by supplying the `authorize` option:
-
-```ruby
-module Types
- class QueryType < BaseObject
- graphql_name 'Query'
-
- field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :read_project
- end
-end
-```
-
-Fields can be authorized against multiple abilities, in which case all
-ability checks must pass. This requires explicitly passing a block to `field`:
-
-```ruby
-field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
- authorize [:read_project, :another_ability]
-end
-```
-
-The object found by the resolve call is used for authorization.
-
-TIP: **Tip:**
-When authorizing collections, try to load only what the currently
-authenticated user is allowed to view with our existing finders first.
-This minimizes database queries and unnecessary authorization checks of
-the loaded records.
-
## Types
When exposing a model through the GraphQL API, we do so by creating a
@@ -197,6 +165,114 @@ end
policies at once. The fields for these will all have be non-nullable
booleans with a default description.
+## Authorization
+
+Authorizations can be applied to both types and fields using the same
+abilities as in the Rails app.
+
+If the:
+
+- Currently authenticated user fails the authorization, the authorized
+resource will be returned as `null`.
+- Resource is part of a collection, the collection will be filtered to
+exclude the objects that the user's authorization checks failed against.
+
+TIP: **Tip:**
+Try to load only what the currently authenticated user is allowed to
+view with our existing finders first, without relying on authorization
+to filter the records. This minimizes database queries and unnecessary
+authorization checks of the loaded records.
+
+### Type authorization
+
+Authorize a type by passing an ability to the `authorize` method. All
+fields with the same type will be authorized by checking that the
+currently authenticated user has the required ability.
+
+For example, the following authorization ensures that the currently
+authenticated user can only see projects that they have the
+`read_project` ability for (so long as the project is returned in a
+field that uses `Types::ProjectType`):
+
+```ruby
+module Types
+ class ProjectType < BaseObject
+ authorize :read_project
+ end
+end
+```
+
+You can also authorize against multiple abilities, in which case all of
+the ability checks must pass.
+
+For example, the following authorization ensures that the currently
+authenticated user must have `read_project` and `another_ability`
+abilities to see a project:
+
+```ruby
+module Types
+ class ProjectType < BaseObject
+ authorize [:read_project, :another_ability]
+ end
+end
+```
+
+### Field authorization
+
+Fields can be authorized with the `authorize` option.
+
+For example, the following authorization ensures that the currently
+authenticated user must have the `owner_access` ability to see the
+project:
+
+```ruby
+module Types
+ class MyType < BaseObject
+ field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, authorize: :owner_access
+ end
+end
+```
+
+Fields can also be authorized against multiple abilities, in which case
+all of ability checks must pass. **Note:** This requires explicitly
+passing a block to `field`:
+
+```ruby
+module Types
+ class MyType < BaseObject
+ field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do
+ authorize [:owner_access, :another_ability]
+ end
+ end
+end
+```
+
+NOTE: **Note:** If the field's type already [has a particular
+authorization](#type-authorization) then there is no need to add that
+same authorization to the field.
+
+### Type and Field authorizations together
+
+Authorizations are cumulative, so where authorizations are defined on
+a field, and also on the field's type, then the currently authenticated
+user would need to pass all ability checks.
+
+In the following simplified example the currently authenticated user
+would need both `first_permission` and `second_permission` abilities in
+order to see the author of the issue.
+
+```ruby
+class UserType
+ authorize :first_permission
+end
+```
+
+```ruby
+class IssueType
+ field :author, UserType, authorize: :second_permission
+end
+```
+
## Resolvers
To find objects to display in a field, we can add resolvers to
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index c2e05b2d065..9452593c510 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -943,7 +943,7 @@ import mixin from 'ee_else_ce/path/mixin';
```html
- <ul v-if="renderIfEE">
+ <ul v-if="ifEE">
<li>One wrapped</li>
<li>element</li>
<li>that is rendered</li>
@@ -962,7 +962,7 @@ For regular JS files, the approach is similar.
```javascript
import { ifEE } from '~/lib/utils/common_utils'
-if (renderIfEE) {
+if (ifEE) {
$('.js-import-git-toggle-button').on('click', () => {
const $projectMirror = $('#project_mirror');
@@ -976,7 +976,7 @@ if (renderIfEE) {
To separate EE-specific styles in SCSS files, if a component you're adding styles for
is limited to only EE, it is better to have a separate SCSS file in appropriate directory
within `app/assets/stylesheets`.
-See [backporting changes](#backporting-changes-from-EE-to-CE) for instructions on how to merge changes safely.
+See [backporting changes](#backporting-changes-from-ee-to-ce) for instructions on how to merge changes safely.
In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill,
e.g. a text style of some component is different for EE. In such cases,
diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md
index 6f6b361f423..548d72bea93 100644
--- a/doc/development/fe_guide/style_guide_scss.md
+++ b/doc/development/fe_guide/style_guide_scss.md
@@ -12,7 +12,15 @@ led by the [GitLab UI WG](https://gitlab.com/gitlab-com/www-gitlab-com/merge_req
We have a few internal utility classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/framework/common.scss)
and we use [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/)
-New utility classes should be added to [`common.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/framework/common.scss).
+New utility classes should be added to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/stylesheets/utilities.scss). Existing classes include:
+
+**Background color**: `.bg-variant-shade` e.g. `.bg-warning-400`
+**Text color**: `.text-variant-shade` e.g. `.text-success-500`
+- variant is one of 'primary', 'secondary', 'success', 'warning', 'error'
+- shade is on of the shades listed on [colors](https://design.gitlab.com/foundations/colors/)
+
+**Font size**: `.text-size` e.g. `.text-2`
+- **size** is number from 1-6 from our [Type scale](https://design.gitlab.com/foundations/typography)
### Naming
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 71c9637e72c..f58a8dcbcdc 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -26,6 +26,10 @@ It is not yet a requirement to use Jest. You can view the
[epic](https://gitlab.com/groups/gitlab-org/-/epics/873) of issues
we need to solve before being able to use Jest for all our needs.
+### Debugging Jest tests
+
+Running `yarn jest-debug` will run Jest in debug mode, allowing you to debug/inspect as described in the [Jest docs](https://jestjs.io/docs/en/troubleshooting#tests-are-failing-and-you-don-t-know-why).
+
### Timeout error
The default timeout for Jest is set in
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index ffc71051377..55ca502f84a 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -130,9 +130,9 @@ secure note named **gitlab-{ce,ee} Review App's root password**.
1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`.
1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
-1. Replace `-c task-runner -- ls` with `-- /srv/gitlab/bin/rails c` from the
+1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
default command or
- - Run `kubectl exec --namespace review-apps-ce -it review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -- /srv/gitlab/bin/rails c`
+ - Run `kubectl exec --namespace review-apps-ce review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console`
and
- Replace `review-apps-ce` with `review-apps-ee` if the Review App
is running EE, and
diff --git a/doc/install/README.md b/doc/install/README.md
index 52011526768..53778f7f0d3 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -55,9 +55,9 @@ need to be aware of:
- It can be more expensive for smaller installations. The default installation
requires more resources than a single node Omnibus deployment, as most services
are deployed in a redundant fashion.
-- There are some feature [limitations to be aware of](kubernetes/gitlab_chart.md#limitations).
+- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations).
-[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](kubernetes/index.md)
+[**> Install GitLab on Kubernetes using the GitLab Helm charts.**](https://docs.gitlab.com/charts/)
## Installing GitLab with Docker
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 9db246b3eb3..43655767002 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,156 +1,5 @@
-# GitLab Helm Chart
+---
+redirect_to: https://docs.gitlab.com/charts/
+---
-This is the official way to install GitLab on a cloud native environment.
-
-NOTE: **Kubernetes experience required:**
-Our Helm charts are recommended for those who are familiar with Kubernetes.
-If you're not sure if Kubernetes is for you, our
-[Omnibus GitLab packages](../README.md#installing-gitlab-using-the-omnibus-gitlab-package-recommended)
-are mature, scalable, support [high availability](../../administration/high_availability/README.md)
-and are used today on GitLab.com.
-It is not necessary to have GitLab installed on Kubernetes in order to use [GitLab Kubernetes integration](https://docs.gitlab.com/ee/user/project/clusters/index.html).
-
-## Introduction
-
-The `gitlab` chart is the best way to operate GitLab on Kubernetes. This chart
-contains all the required components to get started, and can scale to large deployments.
-
-The default deployment includes:
-
-- Core GitLab components: Unicorn, Shell, Workhorse, Registry, Sidekiq, and Gitaly
-- Optional dependencies: Postgres, Redis, Minio
-- An auto-scaling, unprivileged [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor
-- Automatically provisioned SSL via [Let's Encrypt](https://letsencrypt.org/).
-
-## Limitations
-
-Some features of GitLab are not currently available:
-
-- [GitLab Pages](https://gitlab.com/charts/gitlab/issues/37)
-- [GitLab Geo](https://gitlab.com/charts/gitlab/issues/8)
-- [No in-cluster HA database](https://gitlab.com/charts/gitlab/issues/48)
-- MySQL will not be supported, as support is [deprecated within GitLab](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only)
-
-## Installing GitLab using the Helm Chart
-
-The `gitlab` chart includes all required dependencies, and takes a few minutes
-to deploy.
-
-TIP: **Tip:**
-For production deployments, we strongly recommend using the
-[detailed installation instructions](https://gitlab.com/charts/gitlab/blob/master/doc/installation/index.md)
-utilizing [external Postgres, Redis, and object storage](https://gitlab.com/charts/gitlab/tree/master/doc/advanced) services.
-
-### Requirements
-
-In order to deploy GitLab on Kubernetes, the following are required:
-
-1. `helm` and `kubectl` [installed on your computer](preparation/tools_installation.md).
-1. A Kubernetes cluster, version 1.8 or higher. 6vCPU and 16GB of RAM is recommended.
- - [Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html)
- - [Google GKE](https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-container-cluster)
- - [IBM IKS](https://console.bluemix.net/docs/tutorials/scalable-webapp-kubernetes.html#create_kube_cluster)
- - [Microsoft AKS](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal)
-1. A [wildcard DNS entry and external IP address](preparation/networking.md)
-1. [Authenticate and connect](preparation/connect.md) to the cluster
-1. Configure and initialize [Helm Tiller](preparation/tiller.md).
-
-### Deployment of GitLab to Kubernetes
-
-To deploy GitLab, the following three parameters are required:
-
-- `global.hosts.domain`: the [base domain](preparation/networking.md) of the
- wildcard host entry. For example, `example.com` if the wild card entry is
- `*.example.com`.
-- `global.hosts.externalIP`: the [external IP](preparation/networking.md) which
- the wildcard DNS resolves to.
-- `certmanager-issuer.email`: the email address to use when requesting new SSL
- certificates from Let's Encrypt.
-
-NOTE: **Note:**
-For deployments to Amazon EKS, there are
-[additional configuration requirements](preparation/eks.md). A full list of
-configuration options is [also available](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md).
-
-Once you have all of your configuration options collected, you can get any
-dependencies and run helm. In this example, the helm release is named "gitlab":
-
-```sh
-helm repo add gitlab https://charts.gitlab.io/
-helm repo update
-helm upgrade --install gitlab gitlab/gitlab \
- --timeout 600 \
- --set global.hosts.domain=example.com \
- --set global.hosts.externalIP=10.10.10.10 \
- --set certmanager-issuer.email=email@example.com
-```
-
-### Monitoring the Deployment
-
-This will output the list of resources installed once the deployment finishes,
-which may take 5-10 minutes.
-
-The status of the deployment can be checked by running `helm status gitlab`
-which can also be done while the deployment is taking place if you run the
-command in another terminal.
-
-### Initial login
-
-You can access the GitLab instance by visiting the domain name beginning with
-`gitlab.` followed by the domain specified during installation. From the example
-above, the URL would be `https://gitlab.example.com`.
-
-If you manually created the secret for initial root password, you
-can use that to sign in as `root` user. If not, GitLab automatically
-created a random password for `root` user. This can be extracted by the
-following command (replace `<name>` by name of the release - which is `gitlab`
-if you used the command above):
-
-```sh
-kubectl get secret <name>-gitlab-initial-root-password -ojsonpath={.data.password} | base64 --decode ; echo
-```
-
-### Outgoing email
-
-By default outgoing email is disabled. To enable it, provide details for your SMTP server
-using the `global.smtp` and `global.email` settings. You can find details for these settings in the
-[command line options](https://gitlab.com/charts/gitlab/blob/master/doc/installation/command-line-options.md#email-configuration).
-
-If your SMTP server requires authentication make sure to read the section on providing
-your password in the [secrets documentation](https://gitlab.com/charts/gitlab/blob/master/doc/installation/secrets.md#smtp-password).
-You can disable authentication settings with `--set global.smtp.authentication=""`.
-
-If your Kubernetes cluster is on GKE, be aware that SMTP port [25 is blocked](https://cloud.google.com/compute/docs/tutorials/sending-mail/#using_standard_email_ports).
-
-### Deploying the Community Edition
-
-To deploy the Community Edition, include these options in your `helm install` command:
-
-```sh
---set gitlab.migrations.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-rails-ce
---set gitlab.sidekiq.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-sidekiq-ce
---set gitlab.unicorn.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-unicorn-ce
---set gitlab.unicorn.workhorse.image=registry.gitlab.com/gitlab-org/build/cng/gitlab-workhorse-ce
---set gitlab.task-runner.image.repository=registry.gitlab.com/gitlab-org/build/cng/gitlab-task-runner-ce
-```
-
-## Updating GitLab using the Helm Chart
-
-Once your GitLab Chart is installed, configuration changes and chart updates
-should be done using `helm upgrade`:
-
-```sh
-helm repo update
-helm upgrade --reuse-values gitlab gitlab/gitlab
-```
-
-## Uninstalling GitLab using the Helm Chart
-
-To uninstall the GitLab Chart, run the following:
-
-```sh
-helm delete gitlab
-```
-
-[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
-[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
+This document was moved to [another location](https://docs.gitlab.com/charts/).
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index c0cb7694e91..43655767002 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -1,246 +1,5 @@
-# GitLab-Omnibus Helm Chart
+---
+redirect_to: https://docs.gitlab.com/charts/
+---
-CAUTION: **Caution:**
-This chart is **deprecated**. We recommend using the [`gitlab` chart](gitlab_chart.md)
-instead. A comparison of the two charts is available in [this video](https://youtu.be/Z6jWR8Z8dv8).
-
-For more information on available GitLab Helm Charts, see [Installing GitLab on Kubernetes](index.md).
-
-- This GitLab-Omnibus chart has been tested on Google Kubernetes Engine and Azure Container Service.
-- This work is based partially on: <https://github.com/lwolf/kubernetes-gitlab/>. GitLab would like to thank Sergey Nuzhdin for his work.
-
-## Introduction
-
-This chart provides an easy way to get started with GitLab, provisioning an
-installation with nearly all functionality enabled. SSL is automatically
-provisioned via [Let's Encrypt](https://letsencrypt.org/).
-
-This Helm chart is suited for small to medium deployments and is **deprecated**
-and replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
-Due to the significant architectural changes, migrating will require backing up
-data out of this instance and importing it into the new deployment.
-
-The deployment includes:
-
-- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus
-- An auto-scaling [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor
-- [Redis](https://github.com/kubernetes/charts/tree/master/stable/redis)
-- [PostgreSQL](https://github.com/kubernetes/charts/tree/master/stable/postgresql)
-- [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress)
-- Persistent Volume Claims for Data, Registry, Postgres, and Redis
-
-## Limitations
-
-[High Availability](../../administration/high_availability/README.md) and
-[Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) are not supported.
-
-## Requirements
-
-- _At least_ 4 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required.
-- Kubernetes 1.4+ with Beta APIs enabled
-- [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure
-- A [wildcard DNS entry](#networking-requirements), which resolves to the external IP address
-- The `kubectl` CLI installed locally and authenticated for the cluster
-- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine
-
-### Networking requirements
-
-This chart configures a GitLab server and Kubernetes cluster which can support
-dynamic [Review Apps](../../ci/review_apps/index.md), as well as services like
-the integrated [Container Registry](../../user/project/container_registry.md)
-and [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/).
-
-To support the GitLab services and dynamic environments, a wildcard DNS entry
-is required which resolves to the [load balancer](#load-balancer-ip) or
-[external IP](#external-ip-recommended). Configuration of the DNS entry will depend upon
-the DNS service being used.
-
-#### External IP (recommended)
-
-To provision an external IP on GCP and Azure, simply request a new address from
-the Networking section. Ensure that the region matches the region your container
-cluster is created in. It is important that the IP is not assigned at this point
-in time. It will be automatically assigned once the Helm chart is installed,
-and assigned to the Load Balancer.
-
-Now that an external IP address has been allocated, ensure that the wildcard
-DNS entry you would like to use resolves to this IP. Please consult the
-documentation for your DNS service for more information on creating DNS records.
-
-Finally, set the `baseIP` setting to this IP address when
-[deploying GitLab](#configuring-and-installing-gitlab).
-
-#### Load Balancer IP
-
-If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or
-Ingress. You can retrieve this IP by running the following command *after* deploying GitLab:
-
-```sh
-kubectl get svc -w --namespace nginx-ingress nginx
-```
-
-The IP address will be displayed in the `EXTERNAL-IP` field, and should be used
-to configure the Wildcard DNS entry. For more information on creating a wildcard
-DNS entry, consult the documentation for the DNS server you are using.
-
-For production deployments of GitLab, we strongly recommend using a
-[external IP](#external-ip-recommended).
-
-## Configuring and Installing GitLab
-
-For most installations, two parameters are required:
-
-- `baseDomain`: the [base domain](#networking-requirements) of the wildcard host entry. For example, `mycompany.io` if the wild card entry is `*.mycompany.io`.
-- `legoEmail`: Email address to use when requesting new SSL certificates from Let's Encrypt.
-
-Other common configuration options:
-
-- `baseIP`: the desired [external IP address](#external-ip-recommended)
-- `gitlab`: Choose the [desired edition](https://about.gitlab.com/pricing), either `ee` or `ce`. `ce` is the default.
-- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart
-- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
-
-For additional configuration options, consult the
-[`values.yaml`](https://gitlab.com/charts/gitlab-omnibus/blob/master/values.yaml).
-
-### Choosing a different GitLab release version
-
-The version of GitLab installed is based on the `gitlab` setting (see [section](#configuring-and-installing-gitLab) above), and
-the value of the corresponding helm setting: `gitlabCEImage` or `gitabEEImage`.
-
-```yaml
-gitlab: CE
-gitlabCEImage: gitlab/gitlab-ce:9.5.2-ce.0
-gitlabEEImage: gitlab/gitlab-ee:9.5.2-ee.0
-```
-
-The different images can be found in the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce/tags/) and [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee/tags/)
-repositories on Docker Hub.
-
-### Persistent storage
-
-NOTE: **Note:**
-If you are using a machine type with support for less than 4 attached disks,
-like an Azure trial, you should disable dedicated storage for Postgres and Redis.
-
-By default, persistent storage is enabled for GitLab and the charts it depends
-on (Redis and PostgreSQL). Components can have their claim size set from your
-`values.yaml`, along with whether to provision separate storage for Postgres and Redis.
-
-Basic configuration:
-
-```yaml
-redisImage: redis:3.2.10
-redisDedicatedStorage: true
-redisStorageSize: 5Gi
-postgresImage: postgres:9.6.3
-# If you disable postgresDedicatedStorage, you should consider bumping up gitlabRailsStorageSize
-postgresDedicatedStorage: true
-postgresStorageSize: 30Gi
-gitlabRailsStorageSize: 30Gi
-gitlabRegistryStorageSize: 30Gi
-gitlabConfigStorageSize: 1Gi
-```
-
-### Routing and SSL
-
-Ingress routing and SSL are automatically configured within this Chart. An NGINX
-ingress is provisioned and configured, and will route traffic to any service.
-SSL certificates are automatically created and configured by
-[kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego).
-
-NOTE: **Note:**
-Let's Encrypt limits a single TLD to five certificate requests within a single
-week. This means that common DNS wildcard services like [nip.io](http://nip.io)
-and [xip.io](http://xip.io) are unlikely to work.
-
-## Installing GitLab using the Helm Chart
-
-NOTE: **Note:**
-You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound`
-while storage provisions. Once the storage provisions, the pods will automatically start.
-This may take a couple minutes depending on your cloud provider. If the error persists,
-please review the [requirements sections](#requirements) to ensure you have enough RAM, CPU, and storage.
-
-Add the GitLab Helm repository and initialize Helm:
-
-```bash
-helm init
-helm repo add gitlab https://charts.gitlab.io
-```
-
-Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab),
-you can install the chart. We recommending saving your configuration options in a
-`values.yaml` file for easier upgrades in the future:
-
-```bash
-helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus
-```
-
-Or you can pass them on the command line:
-
-```bash
-helm install --name gitlab --set baseDomain=gitlab.io,baseIP=192.0.2.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus
-```
-
-## Updating GitLab using the Helm Chart
-
-If you are upgrading from a previous version to 0.1.35 or above, you will need to
-change the access mode values for GitLab's storage. To do this, set the following
-in `values.yaml` or on the CLI:
-
-```sh
-gitlabDataAccessMode=ReadWriteMany
-gitlabRegistryAccessMode=ReadWriteMany
-gitlabConfigAccessMode=ReadWriteMany
-```
-
-Once your GitLab Chart is installed, configuration changes and chart updates
-should be done using `helm upgrade`:
-
-```sh
-helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus
-```
-
-## Upgrading from CE to EE using the Helm Chart
-
-If you have installed the Community Edition using this chart, upgrading to
-Enterprise Edition is easy.
-
-If you are using a `values.yaml` file to specify the configuration options, edit
-the file and set `gitlab=ee`. If you would like to run a specific version of
-GitLab EE, set `gitlabEEImage` to be the desired GitLab
-[docker image](https://hub.docker.com/r/gitlab/gitlab-ee/tags/). Then you can
-use `helm upgrade` to update your GitLab instance to EE:
-
-```bash
-helm upgrade -f values.yaml gitlab gitlab/gitlab-omnibus
-```
-
-You can also upgrade and specify these options via the command line:
-
-```bash
-helm upgrade gitlab --set gitlab=ee,gitlabEEImage=gitlab/gitlab-ee:9.5.5-ee.0 gitlab/gitlab-omnibus
-```
-
-## Uninstalling GitLab using the Helm Chart
-
-To uninstall the GitLab Chart, run the following:
-
-```bash
-helm delete --purge gitlab
-```
-
-## Troubleshooting
-
-### Storage errors when updating `gitlab-omnibus` versions prior to 0.1.35
-
-Users upgrading `gitlab-omnibus` from a version prior to 0.1.35, may see an error
-like: `Error: UPGRADE FAILED: PersistentVolumeClaim "gitlab-gitlab-config-storage" is invalid: spec: Forbidden: field is immutable after creation`.
-
-This is due to a change in the access mode for GitLab storage in version 0.1.35.
-To successfully upgrade, the access mode flags must be set to `ReadWriteMany`
-as detailed in the [update section](#updating-gitlab-using-the-helm-chart).
-
-[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
-[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
+This document was moved to [another location](https://docs.gitlab.com/charts/).
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 68b2a146115..08ccf2cf9ad 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,269 +1,5 @@
-# GitLab Runner Helm Chart
-> **Note:**
-These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/gitlab-org/gitlab-runner/issues).
+---
+redirect_to: https://docs.gitlab.com/runner/install/kubernetes.html
+---
-The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
-Kubernetes cluster.
-
-This chart configures the Runner to:
-
-- Run using the GitLab Runner [Kubernetes executor](https://docs.gitlab.com/runner/install/kubernetes.html)
-- For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a
- new pod within the specified namespace to run it.
-
-For more information on available GitLab Helm Charts, please see our [overview](index.md).
-
-## Prerequisites
-
-- Your GitLab Server's API is reachable from the cluster
-- Kubernetes 1.4+ with Beta APIs enabled
-- The `kubectl` CLI installed locally and authenticated for the cluster
-- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine
-
-## Configuring GitLab Runner using the Helm Chart
-
-Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md)
-for information on how your values file will override the defaults.
-
-The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository.
-
-### Required configuration
-
-In order for GitLab Runner to function, your config file **must** specify the following:
-
- - `gitlabUrl` - the GitLab Server URL (with protocol) to register the runner against
- - `runnerRegistrationToken` - The Registration Token for adding new Runners to the GitLab Server. This must be
- retrieved from your GitLab Instance. See the [GitLab Runner Documentation](../../ci/runners/README.md) for more information.
-
-Unless you need to specify additional configuration, you are [ready to install](#installing-gitlab-runner-using-the-helm-chart).
-
-### Other configuration
-
-The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository.
-
-Here is a snippet of the important settings:
-
-```yaml
-## The GitLab Server URL (with protocol) that want to register the runner against
-## ref: https://docs.gitlab.com/runner/commands/README.html#gitlab-runner-register
-##
-gitlabUrl: http://gitlab.your-domain.com/
-
-## The Registration Token for adding new Runners to the GitLab Server. This must
-## be retrieved from your GitLab Instance.
-## ref: https://docs.gitlab.com/ee/ci/runners/README.html
-##
-runnerRegistrationToken: ""
-
-## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use
-## Provide resource name for a Kubernetes Secret Object in the same namespace,
-## this is used to populate the /etc/gitlab-runner/certs directory
-## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates
-##
-#certsSecretName:
-
-## Configure the maximum number of concurrent jobs
-## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
-##
-concurrent: 10
-
-## Defines in seconds how often to check GitLab for a new builds
-## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
-##
-checkInterval: 30
-
-## For RBAC support:
-rbac:
- create: false
-
- ## Run the gitlab-bastion container with the ability to deploy/manage containers of jobs
- ## cluster-wide or only within namespace
- clusterWideAccess: false
-
- ## Use the following Kubernetes Service Account name if RBAC is disabled in this Helm chart (see rbac.create)
- ##
- # serviceAccountName: default
-
-## Configuration for the Pods that the runner launches for each new job
-##
-runners:
- ## Default container image to use for builds when none is specified
- ##
- image: ubuntu:16.04
-
- ## Run all containers with the privileged flag enabled
- ## This will allow the docker:stable-dind image to run if you need to run Docker
- ## commands. Please read the docs before turning this on:
- ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
- ##
- privileged: false
-
- ## Namespace to run Kubernetes jobs in (defaults to 'default')
- ##
- # namespace:
-
- ## Build Container specific configuration
- ##
- builds:
- # cpuLimit: 200m
- # memoryLimit: 256Mi
- cpuRequests: 100m
- memoryRequests: 128Mi
-
- ## Service Container specific configuration
- ##
- services:
- # cpuLimit: 200m
- # memoryLimit: 256Mi
- cpuRequests: 100m
- memoryRequests: 128Mi
-
- ## Helper Container specific configuration
- ##
- helpers:
- # cpuLimit: 200m
- # memoryLimit: 256Mi
- cpuRequests: 100m
- memoryRequests: 128Mi
-
-```
-
-### Enabling RBAC support
-
-If your cluster has RBAC enabled, you can choose to either have the chart create its own service account or provide one.
-
-To have the chart create the service account for you, set `rbac.create` to true.
-
-### Controlling maximum Runner concurrency
-
-A single GitLab Runner deployed on Kubernetes is able to execute multiple jobs in parallel by automatically starting additional Runner pods. The [`concurrent` setting](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) controls the maximum number of pods allowed at a single time, and defaults to `10`.
-
-```yaml
-## Configure the maximum number of concurrent jobs
-## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
-##
-concurrent: 10
-```
-
-### Running Docker-in-Docker containers with GitLab Runners
-
-See [Running Privileged Containers for the Runners](#running-privileged-containers-for-the-runners) for how to enable it,
-and the [GitLab CI Runner documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-in-your-builds) on running dind.
-
-### Running privileged containers for the Runners
-
-You can tell the GitLab Runner to run using privileged containers. You may need
-this enabled if you need to use the Docker executable within your GitLab CI jobs.
-
-This comes with several risks that you can read about in the
-[GitLab CI Runner documentation](https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-in-your-builds).
-
-If you are okay with the risks, and your GitLab CI Runner instance is registered
-against a specific project in GitLab that you trust the CI jobs of, you can
-enable privileged mode in `values.yaml`:
-
-```yaml
-runners:
- ## Run all containers with the privileged flag enabled
- ## This will allow the docker:stable-dind image to run if you need to run Docker
- ## commands. Please read the docs before turning this on:
- ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
- ##
- privileged: true
-```
-
-### Providing a custom certificate for accessing GitLab
-
-You can provide a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret/)
-to the GitLab Runner Helm Chart, which will be used to populate the container's
-`/etc/gitlab-runner/certs` directory.
-
-Each key name in the Secret will be used as a filename in the directory, with the
-file content being the value associated with the key.
-
-More information on how GitLab Runner uses these certificates can be found in the
-[Runner Documentation](https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates).
-
- - The key/file name used should be in the format `<gitlab-hostname>.crt`. For example: `gitlab.your-domain.com.crt`.
- - Any intermediate certificates need to be concatenated to your server certificate in the same file.
- - The hostname used should be the one the certificate is registered for.
-
-The GitLab Runner Helm Chart does not create a secret for you. In order to create
-the secret, you can prepare your certificate on you local machine, and then run
-the `kubectl create secret` command from the directory with the certificate
-
-```bash
-kubectl
- --namespace <NAMESPACE>
- create secret generic <SECRET_NAME>
- --from-file=<CERTFICATE_FILENAME>
-```
-
-- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
-- `<SECRET_NAME>` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert`
-- `<CERTFICATE_FILENAME>` is the filename for the certificate in your current directory that will be imported into the secret
-
-You then need to provide the secret's name to the GitLab Runner chart.
-
-Add the following to your `values.yaml`
-
-```yaml
-## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use
-## Provide resource name for a Kubernetes Secret Object in the same namespace,
-## this is used to populate the /etc/gitlab-runner/certs directory
-## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates
-##
-certsSecretName: <SECRET NAME>
-```
-
-- `<SECRET_NAME>` is the Kubernetes Secret resource name. For example: `gitlab-domain-cert`
-
-## Installing GitLab Runner using the Helm Chart
-
-Add the GitLab Helm repository and initialize Helm:
-
-```bash
-helm repo add gitlab https://charts.gitlab.io
-helm init
-```
-
-Once you [have configured](#configuring-gitlab-runner-using-the-helm-chart) GitLab Runner in your `values.yml` file,
-run the following:
-
-```bash
-helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
-```
-
-- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
-- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom configuration. See the
- [Configuring GitLab Runner using the Helm Chart](#configuring-gitlab-runner-using-the-helm-chart) section to create it.
-
-## Updating GitLab Runner using the Helm Chart
-
-Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
-
-```bash
-helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
-```
-
-Where:
-
-- `<NAMESPACE>` is the Kubernetes namespace where GitLab Runner is installed
-- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom configuration. See the
- [Configuring GitLab Runner using the Helm Chart](#configuring-gitlab-runner-using-the-helm-chart) section to create it.
-- `<RELEASE-NAME>` is the name you gave the chart when installing it.
- In the [Installing GitLab Runner using the Helm Chart](#installing-gitlab-runner-using-the-helm-chart) section, we called it `gitlab-runner`.
-
-## Uninstalling GitLab Runner using the Helm Chart
-
-To uninstall the GitLab Runner Chart, run the following:
-
-```bash
-helm delete --namespace <NAMESPACE> <RELEASE-NAME>
-```
-
-where:
-
-- `<NAMESPACE>` is the Kubernetes namespace where GitLab Runner is installed
-- `<RELEASE-NAME>` is the name you gave the chart when installing it.
- In the [Installing GitLab Runner using the Helm Chart](#installing-gitlab-runner-using-the-helm-chart) section, we called it `gitlab-runner`.
+This document was moved to [another location](https://docs.gitlab.com/runner/install/kubernetes.html).
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index ecc956d04e9..7312bf2d4f7 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -21,16 +21,15 @@ of the application including how it should be deployed, upgraded, and configured
## GitLab Chart
This chart contains all the required components to get started, and can scale to
-large deployments. It offers a number of benefits:
+large deployments. It offers a number of benefits, among others:
-- Horizontal scaling of individual components
-- No requirement for shared storage to scale
-- Containers do not need `root` permissions
-- Automatic SSL with Let's Encrypt
-- An unprivileged GitLab Runner
-- and plenty more.
+- Horizontal scaling of individual components.
+- No requirement for shared storage to scale.
+- Containers do not need `root` permissions.
+- Automatic SSL with Let's Encrypt.
+- An unprivileged GitLab Runner.
-Learn more about the [GitLab chart](gitlab_chart.md).
+Learn more about the [GitLab chart](https://docs.gitlab.com/charts/).
## GitLab Runner Chart
@@ -39,4 +38,4 @@ and you'd like to leverage the Runner's
[Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html),
it can be deployed with the GitLab Runner chart.
-Learn more about [gitlab-runner chart](gitlab_runner_chart.md).
+Learn more about the [GitLab Runner chart](https://docs.gitlab.com/runner/install/kubernetes.html).
diff --git a/doc/install/kubernetes/preparation/connect.md b/doc/install/kubernetes/preparation/connect.md
index a3a0cba4bf2..db55e03d3d4 100644
--- a/doc/install/kubernetes/preparation/connect.md
+++ b/doc/install/kubernetes/preparation/connect.md
@@ -1,27 +1,5 @@
-# Connecting your computer to a cluster
+---
+redirect_to: https://docs.gitlab.com/charts/installation/cloud/
+---
-In order to deploy software and settings to a cluster, you must connect and authenticate to it.
-
-## Connect to GKE cluster
-
-The command for connection to the cluster can be obtained from the
-[Google Cloud Platform Console](https://console.cloud.google.com/kubernetes/list)
-by the individual cluster.
-
-Look for the **Connect** button in the clusters list page or use the command below,
-filling in your cluster's information:
-
-```
-gcloud container clusters get-credentials <cluster-name> --zone <zone> --project <project-id>
-```
-
-## Connect to EKS cluster
-
-For the most up to date instructions, follow the Amazon EKS documentation on
-[connecting to a cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#eks-configure-kubectl).
-
-## Connect to local minikube cluster
-
-If you are doing local development, you can use `minikube` as your
-local cluster. If `kubectl cluster-info` is not showing `minikube` as the current
-cluster, use `kubectl config set-cluster minikube` to set the active cluster.
+This document was moved to [another location](https://docs.gitlab.com/charts/installation/cloud/).
diff --git a/doc/install/kubernetes/preparation/eks.md b/doc/install/kubernetes/preparation/eks.md
index ea3b075dd82..975d35c11c6 100644
--- a/doc/install/kubernetes/preparation/eks.md
+++ b/doc/install/kubernetes/preparation/eks.md
@@ -1,45 +1,5 @@
-# Running GitLab on EKS
+---
+redirect_to: https://docs.gitlab.com/charts/installation/cloud/eks.html
+---
-There are a few nuances to Amazon EKS which are important to be aware of, when deploying GitLab.
-
-## Persistent volume management
-
-There are two methods to manage volume claims on Kubernetes:
-
-1. Manually creating each persistent volume (recommended on EKS)
-1. Utilizing dynamic provisioning to automatically create the persistent volumes
-
-### Manual provisioning of volumes (Recommended)
-
-Manually creating the volumes allows you to control the zone of each volume, as well as all other details supported by the underlying storage.
-
-Follow our documentation on [manually creating persistent volumes](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#manually-creating-static-volumes).
-
-### Dynamic provisioning of volumes
-
-Dynamic provisioning utilizes a Kubernetes provisioner, like `aws-ebs`, to automatically create persistent volumes to fulfill each claim.
-
-With EKS, there are a few important details to keep in mind:
-
-1. Clusters are required to span multiple AZ's
-1. Kubernetes volume provisioners create volumes across zones without regard to which pod they belong to. This leads to scenarios where a pod with multiple volumes being unable to start due to the volumes being in different zones.
-1. There is no default Storage Class.
-
-The easiest way to solve this and still utilize dynamic provisioning is to utilize, or create, a Storage Class that is locked to a specific zone.
-
-> **Note**: Restricting volumes to specific zone will cause GitLab and any other application using this Storage Class to only reside in that zone. For multiple zone support, utilize [manually provisioned volumes](#manual-provisioning-of-volumes-recommended).
-
-To create the storage class, download and edit Amazon EKS's [sample Storage Class](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) and add the following parameter:
-
-```yaml
-parameters:
- zone: <desired-zone>
-```
-
-Then [specify the Storage Class](https://gitlab.com/charts/gitlab/blob/master/doc/installation/storage.md#using-a-custom-storage-class) name when deploying GitLab.
-
-## External access to GitLab
-
-By default, GitLab will an deploy an ingress which will create an associated Elastic Load Balancer. Since the DNS names of ELB's cannot be known ahead of time, it is difficult to utilize Let's Encrypt to automatically provision HTTPS certificates.
-
-We recommend [using your own certificates](https://gitlab.com/charts/gitlab/blob/master/doc/installation/tls.md#option-2-use-your-own-wildcard-certificate), and then mapping your desired DNS name to the created ELB using a CNAME record.
+This document was moved to [another location](https://docs.gitlab.com/charts/installation/cloud/eks.html).
diff --git a/doc/install/kubernetes/preparation/networking.md b/doc/install/kubernetes/preparation/networking.md
index b9fb9a7399f..2af16a752dc 100644
--- a/doc/install/kubernetes/preparation/networking.md
+++ b/doc/install/kubernetes/preparation/networking.md
@@ -1,38 +1,5 @@
-# Networking Prerequisites
+---
+redirect_to: https://docs.gitlab.com/charts/installation/deployment.html#networking-and-dns
+---
-NOTE: **Note:**
-Amazon EKS utilizes Elastic Load Balancers, which are addressed by DNS name and
-cannot be known ahead of time. If you're using EKS, you can skip this section.
-
-The `gitlab` chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html).
-
-To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the external IP.
-
-## External IP
-
-To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, to the Load Balancer.
-
-Set `global.hosts.externalIP` to this IP address when [deploying GitLab](../gitlab_chart.md#installing-gitlab-using-the-helm-chart).
-
-Then, create a [wildcard DNS record](#wildcard-dns-entry) which resolves to this IP address.
-
-### Creating an external IP on GCP
-
-When creating the external IP, it is critical to create it in the same region as your cluster. Otherwise, the IP address will fail to bind to the Load Balancer.
-
-1. Open the [web console](https://console.cloud.google.com)
-1. In the sidebar, browse to `VPC Network > External IP addresses`
-1. Click `Reserve static address`
-1. Choose `Regional` and select the region of your cluster
-1. Leave `Attached to` blank, as it will be automatically assigned during deployment
-
-## Wildcard DNS entry
-
-Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Typically this would be an `A record` for `*`, resolving to the external IP above.
-
-Please consult the documentation for your DNS service for more information on creating DNS records:
-
-- [Google Domains](https://support.google.com/domains/answer/3290350?hl=en)
-- [GoDaddy](https://www.godaddy.com/help/add-an-a-record-19238)
-
-Set `global.hosts.domain` to this DNS name when [deploying GitLab](../gitlab_chart.md#installing-gitlab-using-the-helm-chart).
+This document was moved to [another location](https://docs.gitlab.com/charts/installation/deployment.html#networking-and-dns).
diff --git a/doc/install/kubernetes/preparation/rbac.md b/doc/install/kubernetes/preparation/rbac.md
index c5f8d7a7e9e..f94e7c24cdc 100644
--- a/doc/install/kubernetes/preparation/rbac.md
+++ b/doc/install/kubernetes/preparation/rbac.md
@@ -1,20 +1,5 @@
-# Role Based Access Control
+---
+redirect_to: https://docs.gitlab.com/charts/installation/deployment.html#rbac
+---
-Until Kubernetes 1.7, there were no permissions within a cluster. With the launch
-of 1.7, there is now a [role based access control system (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/)
-which determines what services can perform actions within a cluster.
-
-RBAC affects a few different aspects of GitLab:
-
-- [Installation of GitLab using Helm](tiller.md#preparing-for-helm-with-rbac)
-- Prometheus monitoring
-- GitLab Runner
-
-## Checking that RBAC is enabled
-
-Try listing the current cluster roles, if it fails then `RBAC` is disabled.
-The following command will output `false` if `RBAC` is disabled and `true` otherwise:
-
-```sh
-kubectl get clusterroles > /dev/null 2>&1 && echo true || echo false
-```
+This document was moved to [another location](https://docs.gitlab.com/charts/installation/deployment.html#rbac).
diff --git a/doc/install/kubernetes/preparation/tiller.md b/doc/install/kubernetes/preparation/tiller.md
index 684df14ac2c..66d6c8faece 100644
--- a/doc/install/kubernetes/preparation/tiller.md
+++ b/doc/install/kubernetes/preparation/tiller.md
@@ -1,109 +1,5 @@
-# Configuring and initializing Helm Tiller
-
-To make use of Helm, you must have a [Kubernetes][k8s-io] cluster. Ensure you can
-access your cluster using `kubectl`.
-
-Helm consists of two parts, the `helm` client and a `tiller` server inside Kubernetes.
-
-NOTE: **Note:**
-If you are not able to run Tiller in your cluster, for example on OpenShift, it
-is possible to use [Tiller locally](https://docs.gitlab.com/charts/installation/tools.html#local-tiller)
-and avoid deploying it into the cluster. This should only be used when Tiller
-cannot be normally deployed.
-
-## Initialize Helm and Tiller
-
-Tiller is deployed into the cluster and interacts with the Kubernetes API to deploy your applications. If role based access control (RBAC) is enabled, Tiller will need to be [granted permissions](#preparing-for-helm-with-rbac) to allow it to talk to the Kubernetes API.
-
-If RBAC is not enabled, skip to [initializing Helm](#initialize-helm).
-
-If you are not sure whether RBAC is enabled in your cluster, or to learn more, read through our [RBAC documentation](rbac.md).
-
-## Preparing for Helm with RBAC
-
-Helm's Tiller will need to be granted permissions to perform operations. These instructions grant cluster wide permissions, however for more advanced deployments [permissions can be restricted to a single namespace](https://docs.helm.sh/using_helm/#example-deploy-tiller-in-a-namespace-restricted-to-deploying-resources-only-in-that-namespace). To grant access to the cluster, we will create a new `tiller` service account and bind it to the `cluster-admin` role.
-
-Create a file `rbac-config.yaml` with the following contents:
-
-```yaml
-apiVersion: v1
-kind: ServiceAccount
-metadata:
- name: tiller
- namespace: kube-system
---
-apiVersion: rbac.authorization.k8s.io/v1beta1
-kind: ClusterRoleBinding
-metadata:
- name: tiller
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: ClusterRole
- name: cluster-admin
-subjects:
- - kind: ServiceAccount
- name: tiller
- namespace: kube-system
-```
-
-Next we need to connect to the cluster and upload the RBAC config.
-
-### Upload the RBAC config
-
-Some clusters require authentication to use `kubectl` to create the Tiller roles.
-
-#### Upload the RBAC config as an admin user (GKE)
-
-For GKE, you need to obtain the admin credentials. This command will output the admin password:
-
-```
-gcloud container clusters describe <cluster-name> --zone <zone> --project <project-id> --format='value(masterAuth.password)'
-```
-
-Use the admin password to set the admin credentials. Replace the password value below with the output value from the above step:
-
-```
-kubectl config set-credentials admin --username=admin --password=xxxxxxxxxxxxxx
-```
-
-Once credentials have been set, create the role:
-
-```
-kubectl --user=admin create -f rbac-config.yaml
-```
-
-#### Upload the RBAC config (Non-GKE clusters)
-
-For other clusters like Amazon EKS, you can directly upload the RBAC configuration.
-
-```
-kubectl create -f rbac-config.yaml
-```
-
-## Initialize Helm
-
-Deploy Helm Tiller with a service account:
-
-```
-helm init --service-account tiller
-```
-
-If your cluster previously had Helm/Tiller installed,
-run the following to ensure that the deployed version of Tiller matches the local Helm version:
-
-```
-helm init --upgrade --service-account tiller
-```
-
-### Patching Helm Tiller for Amazon EKS
-
-Helm Tiller requires a flag to be enabled to work properly on Amazon EKS:
-
-```
-kubectl -n kube-system patch deployment tiller-deploy -p '{"spec": {"template": {"spec": {"automountServiceAccountToken": true}}}}'
-```
+redirect_to: https://docs.gitlab.com/charts/installation/tools.html
+---
-[helm]: https://helm.sh
-[helm-using]: https://docs.helm.sh/using_helm
-[k8s-io]: https://kubernetes.io/
-[gcp-k8s]: https://console.cloud.google.com/kubernetes/list
+This document was moved to [another location](https://docs.gitlab.com/charts/installation/tools.html).
diff --git a/doc/install/kubernetes/preparation/tools_installation.md b/doc/install/kubernetes/preparation/tools_installation.md
index d2f7a69a0af..66d6c8faece 100644
--- a/doc/install/kubernetes/preparation/tools_installation.md
+++ b/doc/install/kubernetes/preparation/tools_installation.md
@@ -1,19 +1,5 @@
-# Installing kubectl and Helm on your computer
+---
+redirect_to: https://docs.gitlab.com/charts/installation/tools.html
+---
-In order to work with the GitLab Helm charts, `kubectl` and `helm` must be installed and configured on your computer.
-
-## Installing `kubectl`
-
-`kubectl` is the Kubernetes command line tool, which can be used to deploy settings to the cluster.
-
-Follow the [official documentation](https://kubernetes.io/docs/tasks/tools/install-kubectl/) for the most up to date instructions.
-
-## Installing `helm`
-
-Helm is a package management tool for Kubernetes, and is used to deploy charts.
-
-You can get Helm from the project's [releases page](https://github.com/kubernetes/helm/releases), or follow other options under the official documentation of [Installing Helm](https://docs.helm.sh/using_helm/#installing-helm).
-
-# Next steps
-
-Once installed, proceed to the next [installation step](../gitlab_chart.md#installing-gitlab-using-the-helm-chart).
+This document was moved to [another location](https://docs.gitlab.com/charts/installation/tools.html).
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index 350072186ee..b7f7d71689d 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -1,31 +1,58 @@
---
-last_updated: 2018-02-07
+last_updated: 2019-03-27
---
# Migrating from MySQL to PostgreSQL
-> **Note:** This guide assumes you have a working GitLab instance with
-> MySQL and want to migrate to bundled PostgreSQL database.
+This guide documents how to take a working GitLab instance that uses MySQL and
+migrate it to a PostgreSQL database.
-## Omnibus installation
+## Requirements
-### Prerequisites
+[pgloader](http://pgloader.io) 3.4.1+ is required.
-First, we'll need to enable the bundled PostgreSQL database with up-to-date
-schema. Next, we'll use [pgloader](http://pgloader.io) to migrate the data
-from the old MySQL database to the new PostgreSQL one.
+You can install it directly from your distribution, for example in
+Debian/Ubuntu:
-Here's what you'll need to have installed:
+1. Search for the version:
-- pgloader 3.4.1+
-- Omnibus GitLab
-- MySQL
+ ```bash
+ apt-cache madison pgloader
+ ```
-### Enable bundled PostgreSQL database
+1. If the version is 3.4.1+, install it with:
+
+ ```bash
+ sudo apt-get install pgloader
+ ```
+
+ If your distribution's version is too old, use PostgreSQL's repository:
+
+ ```bash
+ # Add repository
+ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
+
+ # Add key
+ sudo apt-get install wget ca-certificates
+ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
+
+ # Install package
+ sudo apt-get update
+ sudo apt-get install pgloader
+ ```
+
+For other distributions, follow the instructions in PostrgreSQL's
+[download page](https://www.postgresql.org/download/) to add their repository
+and then install `pgloader`.
+
+## Omnibus GitLab installations
+
+For [Omnibus GitLab packages](https://about.gitlab.com/install/), you'll first
+need to enable the bundled PostgreSQL:
1. Stop GitLab:
- ``` bash
+ ```bash
sudo gitlab-ctl stop
```
@@ -40,39 +67,34 @@ Here's what you'll need to have installed:
and alike. You could just comment all of them out so that we'll just use
the defaults.
-1. [Reconfigure GitLab] for the changes to take effect:
-
- ``` bash
- sudo gitlab-ctl reconfigure
- ```
-
+1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
+ for the changes to take effect.
1. Start Unicorn and PostgreSQL so that we can prepare the schema:
- ``` bash
+ ```bash
sudo gitlab-ctl start unicorn
sudo gitlab-ctl start postgresql
```
1. Run the following commands to prepare the schema:
- ``` bash
+ ```bash
sudo gitlab-rake db:create db:migrate
```
1. Stop Unicorn to prevent other database access from interfering with the loading of data:
- ``` bash
+ ```bash
sudo gitlab-ctl stop unicorn
```
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
-### Migrate data from MySQL to PostgreSQL
-
-Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
+Next, we'll use `pgloader` to migrate the data from the old MySQL database to the
+new PostgreSQL one:
1. Save the following snippet in a `commands.load` file, and edit with your
- database `username`, `password` and `host`:
+ MySQL database `username`, `password` and `host`:
```
LOAD DATABASE
@@ -90,7 +112,7 @@ Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
1. Start the migration:
- ``` bash
+ ```bash
sudo -u gitlab-psql pgloader commands.load
```
@@ -117,170 +139,140 @@ Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
Total import time 1894 1894 0 12.497s
```
- If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See
- the [troubleshooting guide](#Troubleshooting) for more details.
+ If there is no output for more than 30 minutes, it's possible `pgloader` encountered an error. See
+ the [troubleshooting guide](#troubleshooting) for more details.
1. Start GitLab:
- ``` bash
+ ```bash
sudo gitlab-ctl start
```
-Now, you can verify that everything worked by visiting GitLab.
-
-### Troubleshooting
-
-#### Permissions
-
-Note that the PostgreSQL user that you use for the above MUST have **superuser** privileges. Otherwise, you may see
-a similar message to the following:
-
-```
-debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread
- #<THREAD "lparallel" RUNNING {10078A3513}>:
- Database error 42501: permission denied: "RI_ConstraintTrigger_a_20937" is a system trigger
- QUERY: ALTER TABLE ci_builds DISABLE TRIGGER ALL;
- 2017-08-23T00:36:56.782000Z ERROR Database error 42501: permission denied: "RI_ConstraintTrigger_c_20864" is a system trigger
- QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL;
-```
-
-#### Experiencing 500 errors after the migration
-
-If you experience 500 errors after the migration, try to clear the cache:
-
-``` bash
-sudo gitlab-rake cache:clear
-```
-
-[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-
-## Source installation
+You can now verify that everything works as expected by visiting GitLab.
-### Prerequisites
+## Source installations
-#### Install PostgreSQL and create database
+For installations from source that use MySQL, you'll first need to
+[install PostgreSQL and create a database](../install/installation.md#6-database).
-See [installation guide](../install/installation.md#6-database).
-
-#### Install [pgloader](http://pgloader.io) 3.4.1+
-
-Install directly from your distro:
-``` bash
-sudo apt-get install pgloader
-```
-
-If this version is too old, use PostgreSQL's repository:
-``` bash
-# add repository
-sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
-
-# add key
-sudo apt-get install wget ca-certificates
-wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
-
-# install package
-sudo apt-get update
-sudo apt-get install pgloader
-```
-
-### Enable bundled PostgreSQL database
+After the database is created, go on with the following steps:
1. Stop GitLab:
- ``` bash
- sudo service gitlab stop
- ```
+ ```bash
+ sudo service gitlab stop
+ ```
1. Switch database from MySQL to PostgreSQL
- ``` bash
- cd /home/git/gitlab
- sudo -u git mv config/database.yml config/database.yml.bak
- sudo -u git cp config/database.yml.postgresql config/database.yml
- sudo -u git -H chmod o-rwx config/database.yml
- ```
+ ```bash
+ cd /home/git/gitlab
+ sudo -u git mv config/database.yml config/database.yml.bak
+ sudo -u git cp config/database.yml.postgresql config/database.yml
+ sudo -u git -H chmod o-rwx config/database.yml
+ ```
+
1. Install Gems related to Postgresql
- ``` bash
- sudo -u git -H rm .bundle/config
- sudo -u git -H bundle install --deployment --without development test mysql aws kerberos
- ```
+ ```bash
+ sudo -u git -H rm .bundle/config
+ sudo -u git -H bundle install --deployment --without development test mysql aws kerberos
+ ```
1. Run the following commands to prepare the schema:
- ``` bash
- sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production
- ```
+ ```bash
+ sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production
+ ```
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema.
-### Migrate data from MySQL to PostgreSQL
-
-Now, you can use pgloader to migrate the data from MySQL to PostgreSQL:
+Next, we'll use `pgloader` to migrate the data from the old MySQL database to the
+new PostgreSQL one:
1. Save the following snippet in a `commands.load` file, and edit with your
MySQL `username`, `password` and `host`:
- ```
- LOAD DATABASE
- FROM mysql://username:password@host/gitlabhq_production
- INTO postgresql://postgres@unix://var/run/postgresql:/gitlabhq_production
+ ```
+ LOAD DATABASE
+ FROM mysql://username:password@host/gitlabhq_production
+ INTO postgresql://postgres@unix://var/run/postgresql:/gitlabhq_production
- WITH include no drop, truncate, disable triggers, create no tables,
- create no indexes, preserve index names, no foreign keys,
- data only
+ WITH include no drop, truncate, disable triggers, create no tables,
+ create no indexes, preserve index names, no foreign keys,
+ data only
- ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
+ ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
- ;
- ```
+ ;
+ ```
1. Start the migration:
- ``` bash
- sudo -u postgres pgloader commands.load
- ```
+ ```bash
+ sudo -u postgres pgloader commands.load
+ ```
1. Once the migration finishes, you should see a summary table that looks like
the following:
- ```
- table name read imported errors total time
- ----------------------------------------------- --------- --------- --------- --------------
- fetch meta data 119 119 0 0.388s
- Truncate 119 119 0 1.134s
- ----------------------------------------------- --------- --------- --------- --------------
- public.abuse_reports 0 0 0 0.490s
- public.appearances 0 0 0 0.488s
- .
- .
- .
- public.web_hook_logs 0 0 0 1.080s
- ----------------------------------------------- --------- --------- --------- --------------
- COPY Threads Completion 4 4 0 2.008s
- Reset Sequences 113 113 0 0.304s
- Install Comments 0 0 0 0.000s
- ----------------------------------------------- --------- --------- --------- --------------
- Total import time 1894 1894 0 12.497s
- ```
-
- If there is no output for more than 30 minutes, it's possible pgloader encountered an error. See
- the [troubleshooting guide](#Troubleshooting) for more details.
+ ```
+ table name read imported errors total time
+ ----------------------------------------------- --------- --------- --------- --------------
+ fetch meta data 119 119 0 0.388s
+ Truncate 119 119 0 1.134s
+ ----------------------------------------------- --------- --------- --------- --------------
+ public.abuse_reports 0 0 0 0.490s
+ public.appearances 0 0 0 0.488s
+ .
+ .
+ .
+ public.web_hook_logs 0 0 0 1.080s
+ ----------------------------------------------- --------- --------- --------- --------------
+ COPY Threads Completion 4 4 0 2.008s
+ Reset Sequences 113 113 0 0.304s
+ Install Comments 0 0 0 0.000s
+ ----------------------------------------------- --------- --------- --------- --------------
+ Total import time 1894 1894 0 12.497s
+ ```
+
+ If there is no output for more than 30 minutes, it's possible `pgloader` encountered an error. See
+ the [troubleshooting guide](#troubleshooting) for more details.
1. Start GitLab:
- ``` bash
- sudo service gitlab start
- ```
+ ```bash
+ sudo service gitlab start
+ ```
+
+You can now verify that everything works as expected by visiting GitLab.
+
+## Troubleshooting
+
+Sometimes, you might encounter some errors during or after the migration.
-Now, you can verify that everything worked by visiting GitLab.
+### Database error permission denied
-### Troubleshooting
+The PostgreSQL user that you use for the migration MUST have **superuser** privileges.
+Otherwise, you may see a similar message to the following:
-#### Experiencing 500 errors after the migration
+```
+debugger invoked on a CL-POSTGRES-ERROR:INSUFFICIENT-PRIVILEGE in thread
+ #<THREAD "lparallel" RUNNING {10078A3513}>:
+ Database error 42501: permission denied: "RI_ConstraintTrigger_a_20937" is a system trigger
+ QUERY: ALTER TABLE ci_builds DISABLE TRIGGER ALL;
+ 2017-08-23T00:36:56.782000Z ERROR Database error 42501: permission denied: "RI_ConstraintTrigger_c_20864" is a system trigger
+ QUERY: ALTER TABLE approver_groups DISABLE TRIGGER ALL;
+```
+
+### Experiencing 500 errors after the migration
If you experience 500 errors after the migration, try to clear the cache:
-``` bash
+```bash
+# Omnibus GitLab
+sudo gitlab-rake cache:clear
+
+# Installations from source
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
```
diff --git a/doc/user/discussions/img/multi-line-suggestion-preview.png b/doc/user/discussions/img/multi-line-suggestion-preview.png
new file mode 100644
index 00000000000..4288d0ba034
--- /dev/null
+++ b/doc/user/discussions/img/multi-line-suggestion-preview.png
Binary files differ
diff --git a/doc/user/discussions/img/multi-line-suggestion-syntax.png b/doc/user/discussions/img/multi-line-suggestion-syntax.png
new file mode 100644
index 00000000000..df0c99b84ef
--- /dev/null
+++ b/doc/user/discussions/img/multi-line-suggestion-syntax.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 23b9604a456..bf41fdecd10 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -344,6 +344,24 @@ and push the suggested change directly into the codebase in the merge request's
Custom commit messages will be introduced by
[#54404](https://gitlab.com/gitlab-org/gitlab-ce/issues/54404).
+### Multi-line suggestions
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310) in GitLab 11.10.
+
+Reviewers can also suggest changes to
+multiple lines with a single suggestion within Merge Request diff discussions.
+
+![Multi-line suggestion syntax](img/multi-line-suggestion-syntax.png)
+
+In the example above, the suggestion covers three lines above and four lines below the commented diff line.
+It'd change from 3 lines _above_ to 4 lines _below_ the commented Diff line.
+
+![Multi-line suggestion preview](img/multi-line-suggestion-preview.png)
+
+NOTE: **Note:**
+Suggestions covering multiple lines are limited to 100 lines _above_ and 100 lines _below_
+the commented diff line, allowing up to 200 changed lines per suggestion.
+
## Start a discussion by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30299) in GitLab 11.9
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 3bcfd30079d..2f1cadb2bbc 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -151,6 +151,17 @@ There are two different ways to add a new project to a group:
![Select group](img/select_group_dropdown.png)
+### Default project creation level
+
+Group owners or administrators can allow users with the
+Developer role to create projects under groups.
+
+By default, [Developers and Maintainers](../permissions.md##group-members-permissions) can create projects under agroup, but this can be changed either within the group settings for a group, or
+be set globally by a GitLab administrator in the Admin area
+at **Settings > General > Visibility and access controls**.
+
+Available settings are `No one`, `Maintainers`, or `Developers + Maintainers`.
+
## Transfer projects into groups
Learn how to [transfer a project into a group](../project/settings/index.md#transferring-an-existing-project-into-another-namespace).
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 69dfad829b4..df413a11af0 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -162,8 +162,7 @@ a new set of recovery codes with SSH.
1. Run `ssh git@gitlab.example.com 2fa_recovery_codes`.
1. You are prompted to confirm that you want to generate new codes. Continuing this process invalidates previously saved codes.
- ```
- bash
+ ```sh
$ ssh git@gitlab.example.com 2fa_recovery_codes
Are you sure you want to generate new two-factor recovery codes?
Any existing recovery codes you saved will be invalidated. (yes/no)
@@ -208,17 +207,17 @@ Sign in and re-enable two-factor authentication as soon as possible.
- You need to take special care to that 2FA keeps working after
[restoring a GitLab backup](../../../raketasks/backup_restore.md).
- To ensure 2FA authorizes correctly with TOTP server, you may want to ensure
- your GitLab server's time is synchronized via a service like NTP. Otherwise,
+ your GitLab server's time is synchronized via a service like NTP. Otherwise,
you may have cases where authorization always fails because of time differences.
- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from
multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at
the time of registration, and cannot be used for other hostnames/FQDNs.
- For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
+ For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
- - The user logs in via `first.host.xyz` and registers their U2F key.
- - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication succeeds.
- - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
+ - The user logs in via `first.host.xyz` and registers their U2F key.
+ - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication succeeds.
+ - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
the U2F key has only been registered on `first.host.xyz`.
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index c94a3f4d3b5..878d30dddaa 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -570,7 +570,7 @@ deployment jobs, immediately before the jobs starts.
However, sometimes GitLab can not create them. In such instances, your job will fail with the message:
```text
-The job failed to complete prerequisite tasks
+This job failed because the necessary resources were not successfully created.
```
To find the cause of this error when creating a namespace and service account, check the [logs](../../../administration/logs.md#sidekiqlog).
diff --git a/doc/user/project/clusters/serverless/img/function-details-loaded.png b/doc/user/project/clusters/serverless/img/function-details-loaded.png
new file mode 100644
index 00000000000..34465c5c087
--- /dev/null
+++ b/doc/user/project/clusters/serverless/img/function-details-loaded.png
Binary files differ
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 96bf455116c..5b7e9ef906f 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -11,13 +11,12 @@ Run serverless workloads on Kubernetes using [Knative](https://cloud.google.com/
Knative extends Kubernetes to provide a set of middleware components that are useful to build modern, source-centric, container-based applications. Knative brings some significant benefits out of the box through its main components:
-- [Build](https://github.com/knative/build): Source-to-container build orchestration.
-- [Eventing](https://github.com/knative/eventing): Management and delivery of events.
- [Serving](https://github.com/knative/serving): Request-driven compute that can scale to zero.
+- [Eventing](https://github.com/knative/eventing): Management and delivery of events.
For more information on Knative, visit the [Knative docs repo](https://github.com/knative/docs).
-With GitLab serverless, you can deploy both functions-as-a-service (FaaS) and serverless applications.
+With GitLab Serverless, you can deploy both functions-as-a-service (FaaS) and serverless applications.
## Prerequisites
@@ -41,13 +40,16 @@ To run Knative on Gitlab, you will need:
wildcard domain where your applications will be served. Configure your DNS server to use the
external IP address or hostname for that domain.
1. **`.gitlab-ci.yml`:** GitLab uses [Kaniko](https://github.com/GoogleContainerTools/kaniko)
- to build the application and the [TriggerMesh CLI](https://github.com/triggermesh/tm) to simplify the
- deployment of knative services and functions.
+ to build the application. We also use [gitlabktl](https://gitlab.com/gitlab-org/gitlabktl)
+ and [TriggerMesh CLI](https://github.com/triggermesh/tm) CLIs to simplify the
+ deployment of services and functions to Knative.
1. **`serverless.yml`** (for [functions only](#deploying-functions)): When using serverless to deploy functions, the `serverless.yml` file
will contain the information for all the functions being hosted in the repository as well as a reference to the
runtime being used.
-1. **`Dockerfile`** (for [applications only](#deploying-serverless-applications): Knative requires a `Dockerfile` in order to build your application. It should be included
- at the root of your project's repo and expose port `8080`.
+1. **`Dockerfile`** (for [applications only](#deploying-serverless-applications): Knative requires a
+ `Dockerfile` in order to build your applications. It should be included at the root of your
+ project's repo and expose port `8080`. `Dockerfile` is not require if you plan to build serverless functions
+ using our [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes).
1. **Prometheus** (optional): Installing Prometheus allows you to monitor the scale and traffic of your serverless function/application.
See [Installing Applications](../index.md#installing-applications) for more information.
@@ -89,10 +91,11 @@ Using functions is useful for dealing with independent
events without needing to maintain a complex unified infrastructure. This allows
you to focus on a single task that can be executed/scaled automatically and independently.
-Currently the following [runtimes](https://gitlab.com/triggermesh/runtimes) are offered:
+Currently the following [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes) are offered:
+- ruby
- node.js
-- kaniko
+- Dockerfile
You can find and import all the files referenced in this doc in the **[functions example project](https://gitlab.com/knative-examples/functions)**.
@@ -111,13 +114,17 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ
include:
template: Serverless.gitlab-ci.yml
- functions:
+ functions:build:
+ extends: .serverless:build:functions
+ environment: production
+
+ functions:deploy:
extends: .serverless:deploy:functions
environment: production
```
- This `.gitlab-ci.yml` creates a `functions` job that invokes some
- predefined commands to deploy your functions to your cluster.
+ This `.gitlab-ci.yml` creates jobs that invoke some predefined commands to
+ build and deploy your functions to your cluster.
`Serverless.gitlab-ci.yml` is a template that allows customization.
You can either import it with `include` parameter and use `extends` to
@@ -135,29 +142,40 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ
You can find the relevant files for this project in the [functions example project](https://gitlab.com/knative-examples/functions).
```yaml
- service: my-functions
- description: "Deploying functions from GitLab using Knative"
+ service: functions
+ description: "GitLab Serverless functions using Knative"
provider:
name: triggermesh
- registry-secret: gitlab-registry
environment:
- FOO: BAR
+ FOO: value
functions:
- echo:
- handler: echo
- runtime: https://gitlab.com/triggermesh/runtimes/raw/master/nodejs.yaml
- description: "echo function using node.js runtime"
- buildargs:
- - DIRECTORY=echo
+ echo-js:
+ handler: echo-js
+ source: ./echo-js
+ runtime: https://gitlab.com/gitlab-org/serverless/runtimes/nodejs
+ description: "node.js runtime function"
+ environment:
+ MY_FUNCTION: echo-js
+
+ echo-rb:
+ handler: MyEcho.my_function
+ source: ./echo-rb
+ runtime: https://gitlab.com/gitlab-org/serverless/runtimes/ruby
+ description: "Ruby runtime function"
+ environment:
+ MY_FUNCTION: echo-rb
+
+ echo-docker:
+ handler: echo-docker
+ source: ./echo-docker
+ description: "Dockerfile runtime function"
environment:
- FUNCTION: echo
+ MY_FUNCTION: echo-docker
```
-The `serverless.yml` file references both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`),
-which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it
-contains three sections with distinct parameters:
+Explanation of the fields used above:
### `service`
@@ -171,7 +189,6 @@ contains three sections with distinct parameters:
| Parameter | Description |
|-----------|-------------|
| `name` | Indicates which provider is used to execute the `serverless.yml` file. In this case, the TriggerMesh `tm` CLI. |
-| `registry-secret` | Indicates which registry will be used to store docker images. The sample function is using the GitLab Registry (`gitlab-registry`). A different registry host may be specified using `registry` key in the `provider` object. If changing the default, update the permission and the secret value on the `gitlab-ci.yml` file |
| `environment` | Includes the environment variables to be passed as part of function execution for **all** functions in the file, where `FOO` is the variable name and `BAR` are he variable contents. You may replace this with you own variables. |
### `functions`
@@ -180,10 +197,10 @@ In the `serverless.yml` example above, the function name is `echo` and the subse
| Parameter | Description |
|-----------|-------------|
-| `handler` | The function's file name. In the example above, both the function name and the handler are the same. |
+| `handler` | The function's name. |
+| `source` | Directory with sources of a functions. |
| `runtime` | The runtime to be used to execute the function. |
| `description` | A short description of the function. |
-| `buildargs` | Pointer to the function file in the repo. In the sample the function is located in the `echo` directory. |
| `environment` | Sets an environment variable for the specific function only. |
After the `gitlab-ci.yml` template has been added and the `serverless.yml` file has been
@@ -284,3 +301,23 @@ The second to last line, labeled **Service domain** contains the URL for the dep
browser to see the app live.
![knative app](img/knative-app.png)
+
+## Function details
+
+Go to the **Operations > Serverless** page and click on one of the function
+rows to bring up the function details page.
+
+![function_details](img/function-details-loaded.png)
+
+The pod count will give you the number of pods running the serverless function instances on a given cluster.
+
+### Prometheus support
+
+For the Knative function invocations to appear,
+[Prometheus must be installed](../index.md#installing-applications).
+
+Once Prometheus is installed, a message may appear indicating that the metrics data _is
+loading or is not available at this time._ It will appear upon the first access of the
+page, but should go away after a few seconds. If the message does not disappear, then it
+is possible that GitLab is unable to connect to the Prometheus instance running on the
+cluster.
diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png
index 2a2101719ba..ea3aff59aa1 100644
--- a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png
+++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png
Binary files differ
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 f8af71ab46b..1477e35dca8 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -35,11 +35,11 @@ You need to have jobs configured to enable this feature.
You can prevent merge requests from being merged if their pipeline did not succeed
or if there are discussions to be resolved.
-Navigate to your project's settings page, select the
-**Only allow merge requests to be merged if the pipeline succeeds** check box and
-hit **Save** for the changes to take effect.
+Navigate to your project's settings page and expand the **Merge requests** section.
+In the **Merge checks** subsection, select the **Pipelines must succeed** check
+box and hit **Save** for the changes to take effect.
-![Only allow merge if pipeline succeeds settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
+![Pipelines must succeed settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
From now on, every time the pipeline fails you will not be able to merge the
merge request from the UI, until you make all relevant jobs pass.
diff --git a/doc/user/project/repository/img/download_source_code.png b/doc/user/project/repository/img/download_source_code.png
new file mode 100644
index 00000000000..17f2cb4b3e8
--- /dev/null
+++ b/doc/user/project/repository/img/download_source_code.png
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 22d912cd9d1..718566a539f 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -241,4 +241,24 @@ Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be clon
in Xcode using the new **Open in Xcode** button, located next to the Git URL
used for cloning your project. The button is only shown on macOS.
+## Download Source Code
+
+Source code stored in the repository can be downloaded.
+
+By clicking the download icon, a dropdown will open with links to download the following:
+
+![Download source code](img/download_source_code.png)
+
+- **Source Code:**
+ This allows users to download the source code on branch they're currently
+ viewing. Available zip, tar, tar.gz and tar.bz2.
+- **Directory:**
+ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24704) in GitLab 11.10
+
+ Only shows up when viewing a sub-directory. This allows users to download
+ the specific directory they're currently viewing. Also available in zip, tar,
+ tar.gz and tar.bz2.
+- **Artifacts:**
+ This allows users to download the artifacts of the latest CI build.
+
[jupyter]: https://jupyter.org
diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md
index 9aa81e33fc0..9e8475d8294 100644
--- a/doc/user/reserved_names.md
+++ b/doc/user/reserved_names.md
@@ -83,6 +83,7 @@ Currently the following names are reserved as top level groups:
- unsubscribes
- uploads
- users
+- v2
These group names are unavailable as subgroup names:
diff --git a/jest.config.js b/jest.config.js
index c7518be9e96..fdbbe977f0b 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -16,10 +16,13 @@ module.exports = {
testMatch: ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js'],
moduleFileExtensions: ['js', 'json', 'vue'],
moduleNameMapper: {
- '^~(.*)$': '<rootDir>/app/assets/javascripts$1',
- '^ee(.*)$': '<rootDir>/ee/app/assets/javascripts$1',
- '^helpers(.*)$': '<rootDir>/spec/frontend/helpers$1',
- '^vendor(.*)$': '<rootDir>/vendor/assets/javascripts$1',
+ '^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
+ '^ee(/.*)$': '<rootDir>/ee/app/assets/javascripts$1',
+ '^ee_else_ce(/.*)$': IS_EE
+ ? '<rootDir>/ee/app/assets/javascripts$1'
+ : '<rootDir>/app/assets/javascripts$1',
+ '^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
+ '^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
'\\.(jpg|jpeg|png|svg)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index cc62b5a3661..2dd3120d3fc 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -690,6 +690,10 @@ module API
# Deprecated
expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
+ expose :reference do |merge_request, options|
+ merge_request.to_reference(options[:project])
+ end
+
expose :web_url do |merge_request|
Gitlab::UrlBuilder.build(merge_request)
end
@@ -726,6 +730,8 @@ module API
merge_request.metrics&.pipeline
end
+ expose :head_pipeline, using: 'API::Entities::Pipeline'
+
expose :diff_refs, using: Entities::DiffRefs
# Allow the status of a rebase to be determined
@@ -1267,6 +1273,9 @@ module API
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
expose :duration
expose :coverage
+ expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
+ pipeline.detailed_status(options[:current_user])
+ end
end
class PipelineSchedule < Grape::Entity
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 9fcf476f537..ad16f26f5cc 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -26,6 +26,7 @@ module API
optional :ldap_cn, type: String, desc: 'LDAP Common Name'
optional :ldap_access, type: Integer, desc: 'A valid access level'
optional :shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Pipeline minutes quota for this group'
+ optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this group'
all_or_none_of :ldap_cn, :ldap_access
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index cb9aa849eeb..9c7b9146c8f 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -87,7 +87,8 @@ module API
gl_id: Gitlab::GlId.gl_id(user),
gl_username: user&.username,
git_config_options: [],
- gitaly: gitaly_payload(params[:action])
+ gitaly: gitaly_payload(params[:action]),
+ gl_console_messages: check_result.console_messages
}
# Custom option for git-receive-pack command
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index fae20e45bf9..999a9cb5a82 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -307,10 +307,10 @@ module API
merge_requests = ::Issues::ReferencedMergeRequestsService.new(user_project, current_user)
.execute(issue)
- .flatten
+ .first
present paginate(::Kaminari.paginate_array(merge_requests)),
- with: Entities::MergeRequestBasic,
+ with: Entities::MergeRequest,
current_user: current_user,
project: user_project
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 98dcc388f44..e4b21b7d1c4 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -111,6 +111,7 @@ module API
desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
+ optional :source_project_id, type: Integer, desc: 'Return merge requests with the given source project id'
optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
optional :search, type: String, desc: 'Search merge requests for text present in the title, description, or any combination of these'
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index d742c6c97c1..d96cdc31212 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -40,7 +40,8 @@ module API
end
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
- optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master'
+ optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
+ optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 776329622e2..2f23e33bd4a 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -54,6 +54,7 @@ module API
if Gitlab.ee?
optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user'
+ optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this user'
end
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 5f8aca104aa..44b151d01e7 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -195,15 +195,21 @@ module Banzai
content = link_content || object_link_text(object, matches)
- %(<a href="#{url}" #{data}
- title="#{escape_once(title)}"
- class="#{klass}">#{content}</a>)
+ link = %(<a href="#{url}" #{data}
+ title="#{escape_once(title)}"
+ class="#{klass}">#{content}</a>)
+
+ wrap_link(link, object)
else
match
end
end
end
+ def wrap_link(link, object)
+ link
+ end
+
def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
object_parent_type = parent.is_a?(Group) ? :group : :project
diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb
index ad367cc5efe..8f5ad9981e5 100644
--- a/lib/banzai/filter/blockquote_fence_filter.rb
+++ b/lib/banzai/filter/blockquote_fence_filter.rb
@@ -42,7 +42,9 @@ module Banzai
def call
@text.gsub(REGEX) do
if $~[:quote]
- $~[:quote].gsub(/^/, "> ").gsub(/^> $/, ">")
+ # keep the same number of source lines/positions by replacing the
+ # fence lines with newlines
+ "\n" + $~[:quote].gsub(/^/, "> ").gsub(/^> $/, ">") + "\n"
else
$~[0]
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index f90a35952e5..77e4c438bd0 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -91,7 +91,11 @@ module Banzai
label_suffix = " <i>in #{reference}</i>" if reference.present?
end
- LabelsHelper.render_colored_label(object, label_suffix)
+ LabelsHelper.render_colored_label(object, label_suffix: label_suffix, title: tooltip_title(object))
+ end
+
+ def tooltip_title(label)
+ nil
end
def full_path_ref?(matches)
diff --git a/lib/banzai/suggestions_parser.rb b/lib/banzai/suggestions_parser.rb
deleted file mode 100644
index 0d7f751bfc1..00000000000
--- a/lib/banzai/suggestions_parser.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: Delete when https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26107
-# exchange this parser by `Gitlab::Diff::SuggestionsParser`.
-module Banzai
- module SuggestionsParser
- # Returns the content of each suggestion code block.
- #
- def self.parse(text)
- html = Banzai.render(text, project: nil, no_original_data: true)
- doc = Nokogiri::HTML(html)
-
- doc.search('pre.suggestion').map { |node| node.text }
- end
- end
-end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 6c8ca8f219c..6eb08f674c2 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -24,6 +24,11 @@ module Gitlab
PROTECTION_FULL = 2
PROTECTION_DEV_CAN_MERGE = 3
+ # Default project creation level
+ NO_ONE_PROJECT_ACCESS = 0
+ MAINTAINER_PROJECT_ACCESS = 1
+ DEVELOPER_MAINTAINER_PROJECT_ACCESS = 2
+
class << self
delegate :values, to: :options
@@ -85,6 +90,22 @@ module Gitlab
def human_access_with_none(access)
options_with_none.key(access)
end
+
+ def project_creation_options
+ {
+ s_('ProjectCreationLevel|No one') => NO_ONE_PROJECT_ACCESS,
+ s_('ProjectCreationLevel|Maintainers') => MAINTAINER_PROJECT_ACCESS,
+ s_('ProjectCreationLevel|Developers + Maintainers') => DEVELOPER_MAINTAINER_PROJECT_ACCESS
+ }
+ end
+
+ def project_creation_values
+ project_creation_options.values
+ end
+
+ def project_creation_level_name(name)
+ project_creation_options.key(name)
+ end
end
def human_access
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index 360424bec11..c3005303fd8 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -35,7 +35,7 @@ module Gitlab
# patterns can be matched only when branch or tag is used
# the pattern matching does not work for merge requests pipelines
if pipeline.branch? || pipeline.tag?
- if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern)
+ if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern, fallback: true)
regexp.match?(pipeline.ref)
else
pattern == pipeline.ref
diff --git a/lib/gitlab/ci/config/entry/includes.rb b/lib/gitlab/ci/config/entry/includes.rb
index 82b2b1ccf4b..43e74dfd628 100644
--- a/lib/gitlab/ci/config/entry/includes.rb
+++ b/lib/gitlab/ci/config/entry/includes.rb
@@ -11,7 +11,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, type: Array
+ validates :config, array_or_string: true
end
def self.aspects
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index adc3660d950..7b14218d3ea 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -17,7 +17,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, array_of_strings_or_regexps: true
+ validates :config, array_of_strings_or_regexps_with_fallback: true
end
def value
@@ -38,7 +38,7 @@ module Gitlab
validate :variables_expressions_syntax
with_options allow_nil: true do
- validates :refs, array_of_strings_or_regexps: true
+ validates :refs, array_of_strings_or_regexps_with_fallback: true
validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true
validates :changes, array_of_strings: true
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index f7d0715e617..96d05842838 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -16,7 +16,8 @@ module Gitlab
Status::Build::Skipped],
[Status::Build::Cancelable,
Status::Build::Retryable],
- [Status::Build::Failed],
+ [Status::Build::FailedUnmetPrerequisites,
+ Status::Build::Failed],
[Status::Build::FailedAllowed,
Status::Build::Unschedule,
Status::Build::Play,
diff --git a/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb
new file mode 100644
index 00000000000..eaad3969a4c
--- /dev/null
+++ b/lib/gitlab/ci/status/build/failed_unmet_prerequisites.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class FailedUnmetPrerequisites < Status::Extended
+ def illustration
+ {
+ image: 'illustrations/pipelines_failed.svg',
+ size: 'svg-430',
+ title: _('Failed to create resources'),
+ content: _('Retry this job in order to create the necessary resources.')
+ }
+ end
+
+ def self.matches?(build, _)
+ build.unmet_prerequisites?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index f786022beb0..b3eec2b6210 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -78,8 +78,14 @@ include:
- template: Jobs/Code-Quality.gitlab-ci.yml
- template: Jobs/Deploy.gitlab-ci.yml
- template: Jobs/Browser-Performance-Testing.gitlab-ci.yml
- - template: Jobs/DAST.gitlab-ci.yml
+ - template: Security/DAST.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
- template: Security/License-Management.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
+
+# Override DAST job to exclude master branch
+dast:
+ except:
+ refs:
+ - master \ No newline at end of file
diff --git a/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml
deleted file mode 100644
index aedbbb21674..00000000000
--- a/lib/gitlab/ci/templates/Jobs/DAST.gitlab-ci.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-dast:
- stage: dast
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}
- - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
- - |
- if ! docker info &>/dev/null; then
- if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
- export DOCKER_HOST='tcp://localhost:2375'
- fi
- fi
- - |
- function dast_run() {
- docker run \
- --env DAST_TARGET_AVAILABILITY_TIMEOUT \
- --volume "$PWD:/output" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- -w /output \
- "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \
- /analyze -t $DAST_WEBSITE \
- "$@"
- }
- - |
- if [ -n "$DAST_AUTH_URL" ]
- then
- dast_run \
- --auth-url $DAST_AUTH_URL \
- --auth-username $DAST_USERNAME \
- --auth-password $DAST_PASSWORD \
- --auth-username-field $DAST_USERNAME_FIELD \
- --auth-password-field $DAST_PASSWORD_FIELD
- else
- dast_run
- fi
- artifacts:
- reports:
- dast: gl-dast-report.json
- only:
- refs:
- - branches
- - tags
- variables:
- - $GITLAB_FEATURES =~ /\bdast\b/
- except:
- refs:
- - master
- variables:
- - $DAST_DISABLED
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 770340de16f..2a90cc9a06c 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -4,12 +4,6 @@
# List of the variables: https://gitlab.com/gitlab-org/security-products/dast#settings
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
-include:
- - template: Jobs/DAST.gitlab-ci.yml
-
-variables:
- DAST_WEBSITE: http://example.com # Please edit to be your website to scan for vulnerabilities
-
stages:
- build
- test
@@ -17,10 +11,53 @@ stages:
- dast
dast:
+ stage: dast
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}
+ - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
+ - |
+ if ! docker info &>/dev/null; then
+ if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
+ export DOCKER_HOST='tcp://localhost:2375'
+ fi
+ fi
+ - |
+ function dast_run() {
+ docker run \
+ --env DAST_TARGET_AVAILABILITY_TIMEOUT \
+ --volume "$PWD:/output" \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ -w /output \
+ "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \
+ /analyze -t $DAST_WEBSITE \
+ "$@"
+ }
+ - |
+ if [ -n "$DAST_AUTH_URL" ]
+ then
+ dast_run \
+ --auth-url $DAST_AUTH_URL \
+ --auth-username $DAST_USERNAME \
+ --auth-password $DAST_PASSWORD \
+ --auth-username-field $DAST_USERNAME_FIELD \
+ --auth-password-field $DAST_PASSWORD_FIELD
+ else
+ dast_run
+ fi
+ artifacts:
+ reports:
+ dast: gl-dast-report.json
only:
refs:
- branches
+ variables:
+ - $GITLAB_FEATURES =~ /\bdast\b/
except:
- refs: [] # Override default from template
variables:
- $DAST_DISABLED
diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
index 4f3d08d98fe..4c92dcb7941 100644
--- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
@@ -31,11 +31,14 @@ stages:
- echo "$CI_REGISTRY_IMAGE"
- tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait
+.serverless:build:functions:
+ stage: build
+ environment: development
+ image: registry.gitlab.com/gitlab-org/gitlabktl:latest
+ script: /usr/bin/gitlabktl serverless build
+
.serverless:deploy:functions:
stage: deploy
environment: development
- image: gcr.io/triggermesh/tm:v0.0.9
- script:
- - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push
- - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull
- - tm -n "$KUBE_NAMESPACE" deploy --wait
+ image: registry.gitlab.com/gitlab-org/gitlabktl:latest
+ script: /usr/bin/gitlabktl serverless deploy
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index d0ee94370ba..df34d254c65 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -54,6 +54,14 @@ module Gitlab
end
end
+ class ArrayOrStringValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array) || value.is_a?(String)
+ record.errors.add(attribute, 'should be an array or a string')
+ end
+ end
+ end
+
class BooleanValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
@@ -129,6 +137,12 @@ module Gitlab
end
end
+ protected
+
+ def fallback
+ false
+ end
+
private
def matches_syntax?(value)
@@ -137,7 +151,7 @@ module Gitlab
def validate_regexp(value)
matches_syntax?(value) &&
- Gitlab::UntrustedRegexp::RubySyntax.valid?(value)
+ Gitlab::UntrustedRegexp::RubySyntax.valid?(value, fallback: fallback)
end
end
@@ -162,6 +176,14 @@ module Gitlab
end
end
+ class ArrayOfStringsOrRegexpsWithFallbackValidator < ArrayOfStringsOrRegexpsValidator
+ protected
+
+ def fallback
+ true
+ end
+ end
+
class ArrayOfStringsOrStringValidator < RegexpValidator
def validate_each(record, attribute, value)
unless validate_array_of_strings_or_string(value)
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index ac65cf74808..d347f3c13a4 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -7,6 +7,7 @@ require_relative 'teammate'
module Gitlab
module Danger
module Helper
+ RELEASE_TOOLS_BOT = 'gitlab-release-tools-bot'
ROULETTE_DATA_URL = URI.parse('https://about.gitlab.com/roulette.json').freeze
# Returns a list of all files that have been added, modified or renamed.
@@ -40,6 +41,10 @@ module Gitlab
ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md')
end
+ def release_automation?
+ gitlab.mr_author == RELEASE_TOOLS_BOT
+ end
+
def project_name
ee? ? 'gitlab-ee' : 'gitlab-ce'
end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 76c8b4ec5c2..fa06fb935f7 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -47,7 +47,7 @@ module Gitlab
user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: {
- filename: build.artifacts_file.filename,
+ filename: build.artifacts_file&.filename,
size: build.artifacts_size
}
}
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 3abd0600e9d..7f5eb1188fc 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -282,6 +282,7 @@ module Gitlab
# Updates the value of a column in batches.
#
# This method updates the table in batches of 5% of the total row count.
+ # A `batch_size` option can also be passed to set this to a fixed number.
# This method will continue updating rows until no rows remain.
#
# When given a block this method will yield two values to the block:
@@ -320,7 +321,7 @@ module Gitlab
# make things _more_ complex).
#
# rubocop: disable Metrics/AbcSize
- def update_column_in_batches(table, column, value)
+ def update_column_in_batches(table, column, value, batch_size: nil)
if transaction_open?
raise 'update_column_in_batches can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -336,14 +337,16 @@ module Gitlab
return if total == 0
- # Update in batches of 5% until we run out of any rows to update.
- batch_size = ((total / 100.0) * 5.0).ceil
- max_size = 1000
+ if batch_size.nil?
+ # Update in batches of 5% until we run out of any rows to update.
+ batch_size = ((total / 100.0) * 5.0).ceil
+ max_size = 1000
- # The upper limit is 1000 to ensure we don't lock too many rows. For
- # example, for "merge_requests" even 1% of the table is around 35 000
- # rows for GitLab.com.
- batch_size = max_size if batch_size > max_size
+ # The upper limit is 1000 to ensure we don't lock too many rows. For
+ # example, for "merge_requests" even 1% of the table is around 35 000
+ # rows for GitLab.com.
+ batch_size = max_size if batch_size > max_size
+ end
start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
start_arel = yield table, start_arel if block_given?
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 0891f79198d..17fbecbd097 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -15,50 +15,51 @@ module Gitlab
new environments].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
+ RESERVED_WORDS_PREFIX = %Q(^(?!.*\/(#{RESERVED_WORDS_REGEX})\/).*)
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
+ %r(#{RESERVED_WORDS_PREFIX}/noteable/issue/\d+/notes\z),
'issue_notes'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/realtime_changes\z),
+ %r(#{RESERVED_WORDS_PREFIX}/issues/\d+/realtime_changes\z),
'issue_title'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/commit/\S+/pipelines\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/commit/\S+/pipelines\.json\z),
'commit_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/new\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/merge_requests/new\.json\z),
'new_merge_request_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/\d+/pipelines\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/merge_requests/\d+/pipelines\.json\z),
'merge_request_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/pipelines\.json\z),
'project_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines/\d+\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/pipelines/\d+\.json\z),
'project_pipeline'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/builds/\d+\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/builds/\d+\.json\z),
'project_build'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/environments\.json\z),
'environments'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/github/realtime_changes\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/import/github/realtime_changes\.json\z),
'realtime_changes_import_github'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/gitea/realtime_changes\.json\z),
+ %r(#{RESERVED_WORDS_PREFIX}/import/gitea/realtime_changes\.json\z),
'realtime_changes_import_gitea'
)
].freeze
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 7d6851a4b8d..c33d243330d 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -231,12 +231,12 @@ module Gitlab
end
end
- def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:)
+ def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil)
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
- prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha)
+ prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path)
{
'ArchivePrefix' => prefix,
@@ -248,13 +248,14 @@ module Gitlab
# This is both the filename of the archive (missing the extension) and the
# name of the top-level member of the archive under which all files go
- def archive_prefix(ref, sha, project_path, append_sha:)
+ def archive_prefix(ref, sha, project_path, append_sha:, path:)
append_sha = (ref != sha) if append_sha.nil?
formatted_ref = ref.tr('/', '-')
prefix_segments = [project_path, formatted_ref]
prefix_segments << sha if append_sha
+ prefix_segments << path.tr('/', '-').gsub(%r{^/|/$}, '') if path
prefix_segments.join('-')
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index c43331bed60..a0dd4a24363 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -86,9 +86,9 @@ module Gitlab
end
end
- def pages(limit: 0)
+ def pages(limit: 0, sort: nil, direction_desc: false)
wrapped_gitaly_errors do
- gitaly_get_all_pages(limit: limit)
+ gitaly_get_all_pages(limit: limit, sort: sort, direction_desc: direction_desc)
end
end
@@ -168,8 +168,10 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file)
end
- def gitaly_get_all_pages(limit: 0)
- gitaly_wiki_client.get_all_pages(limit: limit).map do |wiki_page, version|
+ def gitaly_get_all_pages(limit: 0, sort: nil, direction_desc: false)
+ gitaly_wiki_client.get_all_pages(
+ limit: limit, sort: sort, direction_desc: direction_desc
+ ).map do |wiki_page, version|
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 010bd0e520c..cb80ed64eff 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -85,7 +85,7 @@ module Gitlab
check_push_access!
end
- ::Gitlab::GitAccessResult::Success.new
+ ::Gitlab::GitAccessResult::Success.new(console_messages: check_for_console_messages(cmd))
end
def guest_can_download_code?
@@ -116,6 +116,10 @@ module Gitlab
nil
end
+ def check_for_console_messages(cmd)
+ []
+ end
+
def check_valid_actor!
return unless actor.is_a?(Key)
diff --git a/lib/gitlab/git_access_result/success.rb b/lib/gitlab/git_access_result/success.rb
index 7bb9f24cb0e..e950d727e2e 100644
--- a/lib/gitlab/git_access_result/success.rb
+++ b/lib/gitlab/git_access_result/success.rb
@@ -3,6 +3,11 @@
module Gitlab
module GitAccessResult
class Success
+ attr_reader :console_messages
+
+ def initialize(console_messages: [])
+ @console_messages = console_messages
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 15c9463e2f2..e036cdcd800 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -87,8 +87,13 @@ module Gitlab
wiki_page_from_iterator(response)
end
- def get_all_pages(limit: 0)
- request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo, limit: limit)
+ def get_all_pages(limit: 0, sort: nil, direction_desc: false)
+ sort_value = Gitaly::WikiGetAllPagesRequest::SortBy.resolve(sort.to_s.upcase.to_sym)
+
+ params = { repository: @gitaly_repo, limit: limit, direction_desc: direction_desc }
+ params[:sort] = sort_value if sort_value
+
+ request = Gitaly::WikiGetAllPagesRequest.new(params)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout)
pages = []
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index c2be7f3d63a..a56ca1e39e7 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -35,5 +35,9 @@ module Gitlab
[project, type]
end
+
+ def self.default_type
+ PROJECT
+ end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
new file mode 100644
index 00000000000..8deff79fc84
--- /dev/null
+++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Authorize
+ class AuthorizeFieldService
+ def initialize(field)
+ @field = field
+ @old_resolve_proc = @field.resolve_proc
+ end
+
+ def authorizations?
+ authorizations.present?
+ end
+
+ def authorized_resolve
+ proc do |parent_typed_object, args, ctx|
+ resolved_obj = @old_resolve_proc.call(parent_typed_object, args, ctx)
+ authorizing_obj = authorize_against(parent_typed_object)
+ checker = build_checker(ctx[:current_user], authorizing_obj)
+
+ if resolved_obj.respond_to?(:then)
+ resolved_obj.then(&checker)
+ else
+ checker.call(resolved_obj)
+ end
+ end
+ end
+
+ private
+
+ def authorizations
+ @authorizations ||= (type_authorizations + field_authorizations).uniq
+ end
+
+ # Returns any authorize metadata from the return type of @field
+ def type_authorizations
+ type = @field.type
+
+ # When the return type of @field is a collection, find the singular type
+ if type.get_field('edges')
+ type = node_type_for_relay_connection(type)
+ elsif type.list?
+ type = node_type_for_basic_connection(type)
+ end
+
+ Array.wrap(type.metadata[:authorize])
+ end
+
+ # Returns any authorize metadata from @field
+ def field_authorizations
+ Array.wrap(@field.metadata[:authorize])
+ end
+
+ # If it's a built-in/scalar type, authorize using its parent object.
+ # nil means authorize using the resolved object
+ def authorize_against(parent_typed_object)
+ parent_typed_object.object if built_in_type? && parent_typed_object.respond_to?(:object)
+ end
+
+ def build_checker(current_user, authorizing_obj)
+ lambda do |resolved_obj|
+ # Load the elements if they were not loaded by BatchLoader yet
+ resolved_obj = resolved_obj.sync if resolved_obj.respond_to?(:sync)
+
+ check = lambda do |object|
+ authorizations.all? do |ability|
+ Ability.allowed?(current_user, ability, authorizing_obj || object)
+ end
+ end
+
+ case resolved_obj
+ when Array, ActiveRecord::Relation
+ resolved_obj.select(&check)
+ else
+ resolved_obj if check.call(resolved_obj)
+ end
+ end
+ end
+
+ # Returns the singular type for relay connections.
+ # This will be the type class of edges.node
+ def node_type_for_relay_connection(type)
+ type = type.get_field('edges').type.unwrap.get_field('node')&.type
+
+ if type.nil?
+ raise Gitlab::Graphql::Errors::ConnectionDefinitionError,
+ 'Connection Type must conform to the Relay Cursor Connections Specification'
+ end
+
+ type
+ end
+
+ # Returns the singular type for basic connections, for example `[Types::ProjectType]`
+ def node_type_for_basic_connection(type)
+ type.unwrap
+ end
+
+ def built_in_type?
+ GraphQL::Schema::BUILT_IN_TYPES.has_value?(node_type_for_basic_connection(@field.type))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb
index 593da8471dd..15ecc3b04f0 100644
--- a/lib/gitlab/graphql/authorize/instrumentation.rb
+++ b/lib/gitlab/graphql/authorize/instrumentation.rb
@@ -7,46 +7,12 @@ module Gitlab
# Replace the resolver for the field with one that will only return the
# resolved object if the permissions check is successful.
def instrument(_type, field)
- required_permissions = Array.wrap(field.metadata[:authorize])
- return field if required_permissions.empty?
+ service = AuthorizeFieldService.new(field)
- old_resolver = field.resolve_proc
-
- new_resolver = -> (obj, args, ctx) do
- resolved_obj = old_resolver.call(obj, args, ctx)
- checker = build_checker(ctx[:current_user], required_permissions)
-
- if resolved_obj.respond_to?(:then)
- resolved_obj.then(&checker)
- else
- checker.call(resolved_obj)
- end
- end
-
- field.redefine do
- resolve(new_resolver)
- end
- end
-
- private
-
- def build_checker(current_user, abilities)
- lambda do |value|
- # Load the elements if they weren't loaded by BatchLoader yet
- value = value.sync if value.respond_to?(:sync)
-
- check = lambda do |object|
- abilities.all? do |ability|
- Ability.allowed?(current_user, ability, object)
- end
- end
-
- case value
- when Array
- value.select(&check)
- else
- value if check.call(value)
- end
+ if service.authorizations?
+ field.redefine { resolve(service.authorized_resolve) }
+ else
+ field
end
end
end
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
index fe74549e322..bcbba72e017 100644
--- a/lib/gitlab/graphql/errors.rb
+++ b/lib/gitlab/graphql/errors.rb
@@ -6,6 +6,7 @@ module Gitlab
BaseError = Class.new(GraphQL::ExecutionError)
ArgumentError = Class.new(BaseError)
ResourceNotAvailable = Class.new(BaseError)
+ ConnectionDefinitionError = Class.new(BaseError)
end
end
end
diff --git a/lib/gitlab/graphql/query_analyzers/log_query_complexity.rb b/lib/gitlab/graphql/query_analyzers/log_query_complexity.rb
new file mode 100644
index 00000000000..95db130dbfc
--- /dev/null
+++ b/lib/gitlab/graphql/query_analyzers/log_query_complexity.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module QueryAnalyzers
+ class LogQueryComplexity
+ class << self
+ def analyzer
+ GraphQL::Analysis::QueryComplexity.new do |query, complexity|
+ # temporary until https://gitlab.com/gitlab-org/gitlab-ce/issues/59587
+ Rails.logger.info("[GraphQL Query Complexity] #{complexity} | admin? #{query.context[:current_user]&.admin?}")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/tracing.rb b/lib/gitlab/graphql/tracing.rb
new file mode 100644
index 00000000000..6b505e4262b
--- /dev/null
+++ b/lib/gitlab/graphql/tracing.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ class Tracing < GraphQL::Tracing::PlatformTracing
+ self.platform_keys = {
+ 'lex' => 'graphql.lex',
+ 'parse' => 'graphql.parse',
+ 'validate' => 'graphql.validate',
+ 'analyze_query' => 'graphql.analyze',
+ 'analyze_multiplex' => 'graphql.analyze',
+ 'execute_multiplex' => 'graphql.execute',
+ 'execute_query' => 'graphql.execute',
+ 'execute_query_lazy' => 'graphql.execute',
+ 'execute_field' => 'graphql.execute',
+ 'execute_field_lazy' => 'graphql.execute'
+ }
+
+ def platform_field_key(type, field)
+ "#{type.name}.#{field.name}"
+ end
+
+ def platform_trace(platform_key, key, data, &block)
+ start = Gitlab::Metrics::System.monotonic_time
+
+ yield
+ ensure
+ duration = Gitlab::Metrics::System.monotonic_time - start
+
+ graphql_duration_seconds.observe({ platform_key: platform_key, key: key }, duration)
+ end
+
+ private
+
+ def graphql_duration_seconds
+ @graphql_duration_seconds ||= Gitlab::Metrics.histogram(
+ :graphql_duration_seconds,
+ 'GraphQL execution time'
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index e91803ecd62..d7986685c65 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -8,6 +8,8 @@ module Gitlab
# base labels shared among all transactions
BASE_LABELS = { controller: nil, action: nil }.freeze
+ # labels that potentially contain sensitive information and will be filtered
+ FILTERED_LABELS = [:branch, :path].freeze
THREAD_KEY = :_gitlab_metrics_transaction
@@ -64,7 +66,7 @@ module Gitlab
end
def add_metric(series, values, tags = {})
- @metrics << Metric.new("#{::Gitlab::Metrics.series_prefix}#{series}", values, tags)
+ @metrics << Metric.new("#{::Gitlab::Metrics.series_prefix}#{series}", values, filter_tags(tags))
end
# Tracks a business level event
@@ -75,8 +77,9 @@ module Gitlab
# event_name - The name of the event (e.g. "git_push").
# tags - A set of tags to attach to the event.
def add_event(event_name, tags = {})
- self.class.transaction_metric(event_name, :counter, prefix: 'event_', use_feature_flag: true, tags: tags).increment(tags.merge(labels))
- @metrics << Metric.new(EVENT_SERIES, { count: 1 }, tags.merge(event: event_name), :event)
+ filtered_tags = filter_tags(tags)
+ self.class.transaction_metric(event_name, :counter, prefix: 'event_', use_feature_flag: true, tags: filtered_tags).increment(filtered_tags.merge(labels))
+ @metrics << Metric.new(EVENT_SERIES, { count: 1 }, filtered_tags.merge(event: event_name), :event)
end
# Returns a MethodCall object for the given name.
@@ -164,6 +167,12 @@ module Gitlab
end
end
end
+
+ private
+
+ def filter_tags(tags)
+ tags.without(*FILTERED_LABELS)
+ end
end
end
end
diff --git a/lib/gitlab/object_hierarchy.rb b/lib/gitlab/object_hierarchy.rb
index f2772c733c7..38b32770e90 100644
--- a/lib/gitlab/object_hierarchy.rb
+++ b/lib/gitlab/object_hierarchy.rb
@@ -5,6 +5,8 @@ module Gitlab
#
# This class uses recursive CTEs and as a result will only work on PostgreSQL.
class ObjectHierarchy
+ DEPTH_COLUMN = :depth
+
attr_reader :ancestors_base, :descendants_base, :model
# ancestors_base - An instance of ActiveRecord::Relation for which to
@@ -27,6 +29,17 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
+ # Returns the maximum depth starting from the base
+ # A base object with no children has a maximum depth of `1`
+ def max_descendants_depth
+ unless hierarchy_supported?
+ # This makes the return value consistent with the case where hierarchy is supported
+ return descendants_base.exists? ? 1 : nil
+ end
+
+ base_and_descendants(with_depth: true).maximum(DEPTH_COLUMN)
+ end
+
# Returns the set of ancestors of a given relation, but excluding the given
# relation
#
@@ -64,10 +77,15 @@ module Gitlab
# Returns a relation that includes the descendants_base set of objects
# and all their descendants (recursively).
- def base_and_descendants
- return descendants_base unless hierarchy_supported?
-
- read_only(base_and_descendants_cte.apply_to(model.all))
+ #
+ # When `with_depth` is `true`, a `depth` column is included where it starts with `1` for the base objects
+ # and incremented as we go down the descendant tree
+ def base_and_descendants(with_depth: false)
+ unless hierarchy_supported?
+ return with_depth ? descendants_base.select("1 as #{DEPTH_COLUMN}", objects_table[Arel.star]) : descendants_base
+ end
+
+ read_only(base_and_descendants_cte(with_depth: with_depth).apply_to(model.all))
end
# Returns a relation that includes the base objects, their ancestors,
@@ -124,10 +142,9 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def base_and_ancestors_cte(stop_id = nil, hierarchy_order = nil)
cte = SQL::RecursiveCTE.new(:base_and_ancestors)
- depth_column = :depth
base_query = ancestors_base.except(:order)
- base_query = base_query.select("1 as #{depth_column}", objects_table[Arel.star]) if hierarchy_order
+ base_query = base_query.select("1 as #{DEPTH_COLUMN}", "ARRAY[id] AS tree_path", "false AS tree_cycle", objects_table[Arel.star]) if hierarchy_order
cte << base_query
@@ -137,7 +154,17 @@ module Gitlab
.where(objects_table[:id].eq(cte.table[:parent_id]))
.except(:order)
- parent_query = parent_query.select(cte.table[depth_column] + 1, objects_table[Arel.star]) if hierarchy_order
+ if hierarchy_order
+ quoted_objects_table_name = model.connection.quote_table_name(objects_table.name)
+
+ parent_query = parent_query.select(
+ cte.table[DEPTH_COLUMN] + 1,
+ "tree_path || #{quoted_objects_table_name}.id",
+ "#{quoted_objects_table_name}.id = ANY(tree_path)",
+ objects_table[Arel.star]
+ ).where(cte.table[:tree_cycle].eq(false))
+ end
+
parent_query = parent_query.where(cte.table[:parent_id].not_eq(stop_id)) if stop_id
cte << parent_query
@@ -146,17 +173,32 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def base_and_descendants_cte
+ def base_and_descendants_cte(with_depth: false)
cte = SQL::RecursiveCTE.new(:base_and_descendants)
- cte << descendants_base.except(:order)
+ base_query = descendants_base.except(:order)
+ base_query = base_query.select("1 AS #{DEPTH_COLUMN}", "ARRAY[id] AS tree_path", "false AS tree_cycle", objects_table[Arel.star]) if with_depth
+
+ cte << base_query
# Recursively get all the descendants of the base set.
- cte << model
+ descendants_query = model
.from([objects_table, cte.table])
.where(objects_table[:parent_id].eq(cte.table[:id]))
.except(:order)
+ if with_depth
+ quoted_objects_table_name = model.connection.quote_table_name(objects_table.name)
+
+ descendants_query = descendants_query.select(
+ cte.table[DEPTH_COLUMN] + 1,
+ "tree_path || #{quoted_objects_table_name}.id",
+ "#{quoted_objects_table_name}.id = ANY(tree_path)",
+ objects_table[Arel.star]
+ ).where(cte.table[:tree_cycle].eq(false))
+ end
+
+ cte << descendants_query
cte
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 3c888be0710..a07b1246bee 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -57,6 +57,7 @@ module Gitlab
unsubscribes
uploads
users
+ v2
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
diff --git a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
index 34b705138ba..c49877ddf9d 100644
--- a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
+++ b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
@@ -7,12 +7,16 @@ module Gitlab
include QueryAdditionalMetrics
# rubocop: disable CodeReuse/ActiveRecord
- def query(environment_id)
+ def query(environment_id, timeframe_start = 8.hours.ago, timeframe_end = Time.now)
::Environment.find_by(id: environment_id).try do |environment|
query_metrics(
environment.project,
environment,
- common_query_context(environment, timeframe_start: 8.hours.ago.to_f, timeframe_end: Time.now.to_f)
+ common_query_context(
+ environment,
+ timeframe_start: timeframe_start.to_f,
+ timeframe_end: timeframe_end.to_f
+ )
)
end
end
diff --git a/lib/gitlab/prometheus/queries/knative_invocation_query.rb b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
new file mode 100644
index 00000000000..2691abe46d6
--- /dev/null
+++ b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Prometheus
+ module Queries
+ class KnativeInvocationQuery < BaseQuery
+ include QueryAdditionalMetrics
+
+ def query(serverless_function_id)
+ PrometheusMetric
+ .find_by_identifier(:system_metrics_knative_function_invocation_count)
+ .to_query_metric.tap do |q|
+ q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
+ end
+ end
+
+ protected
+
+ def context(function_id)
+ function = Serverless::Function.find_by_id(function_id)
+ {
+ function_name: function.name,
+ kube_namespace: function.namespace
+ }
+ end
+
+ def run_query(query, context)
+ query %= context
+ client_query_range(query, start: 8.hours.ago.to_f, stop: Time.now.to_f)
+ end
+
+ def self.transform_reactive_result(result)
+ result[:metrics] = result.delete :data
+ result
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb
index 45828c77a33..f13156f898e 100644
--- a/lib/gitlab/prometheus_client.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -6,6 +6,14 @@ module Gitlab
Error = Class.new(StandardError)
QueryError = Class.new(Gitlab::PrometheusClient::Error)
+ # Target number of data points for `query_range`.
+ # Please don't exceed the limit of 11000 data points
+ # See https://github.com/prometheus/prometheus/blob/91306bdf24f5395e2601773316945a478b4b263d/web/api/v1/api.go#L347
+ QUERY_RANGE_DATA_POINTS = 600
+
+ # Minimal value of the `step` parameter for `query_range` in seconds.
+ QUERY_RANGE_MIN_STEP = 60
+
attr_reader :rest_client, :headers
def initialize(rest_client)
@@ -16,6 +24,19 @@ module Gitlab
json_api_get('query', query: '1')
end
+ def proxy(type, args)
+ path = api_path(type)
+ get(path, args)
+ rescue RestClient::ExceptionWithResponse => ex
+ if ex.response
+ ex.response
+ else
+ raise PrometheusClient::Error, "Network connection error"
+ end
+ rescue RestClient::Exception
+ raise PrometheusClient::Error, "Network connection error"
+ end
+
def query(query, time: Time.now)
get_result('vector') do
json_api_get('query', query: query, time: time.to_f)
@@ -23,12 +44,18 @@ module Gitlab
end
def query_range(query, start: 8.hours.ago, stop: Time.now)
+ start = start.to_f
+ stop = stop.to_f
+ step = self.class.compute_step(start, stop)
+
get_result('matrix') do
- json_api_get('query_range',
- query: query,
- start: start.to_f,
- end: stop.to_f,
- step: 1.minute.to_i)
+ json_api_get(
+ 'query_range',
+ query: query,
+ start: start,
+ end: stop,
+ step: step
+ )
end
end
@@ -40,24 +67,24 @@ module Gitlab
json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f)
end
+ def self.compute_step(start, stop)
+ diff = stop - start
+
+ step = (diff / QUERY_RANGE_DATA_POINTS).ceil
+
+ [QUERY_RANGE_MIN_STEP, step].max
+ end
+
private
- def json_api_get(type, args = {})
- path = ['api', 'v1', type].join('/')
- get(path, args)
- rescue JSON::ParserError
- raise PrometheusClient::Error, 'Parsing response failed'
- rescue Errno::ECONNREFUSED
- raise PrometheusClient::Error, 'Connection refused'
+ def api_path(type)
+ ['api', 'v1', type].join('/')
end
- def get(path, args)
- response = rest_client[path].get(params: args)
+ def json_api_get(type, args = {})
+ path = api_path(type)
+ response = get(path, args)
handle_response(response)
- rescue SocketError
- raise PrometheusClient::Error, "Can't connect to #{rest_client.url}"
- rescue OpenSSL::SSL::SSLError
- raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data"
rescue RestClient::ExceptionWithResponse => ex
if ex.response
handle_exception_response(ex.response)
@@ -68,8 +95,18 @@ module Gitlab
raise PrometheusClient::Error, "Network connection error"
end
+ def get(path, args)
+ rest_client[path].get(params: args)
+ rescue SocketError
+ raise PrometheusClient::Error, "Can't connect to #{rest_client.url}"
+ rescue OpenSSL::SSL::SSLError
+ raise PrometheusClient::Error, "#{rest_client.url} contains invalid SSL data"
+ rescue Errno::ECONNREFUSED
+ raise PrometheusClient::Error, 'Connection refused'
+ end
+
def handle_response(response)
- json_data = JSON.parse(response.body)
+ json_data = parse_json(response.body)
if response.code == 200 && json_data['status'] == 'success'
json_data['data'] || {}
else
@@ -81,7 +118,7 @@ module Gitlab
if response.code == 200 && response['status'] == 'success'
response['data'] || {}
elsif response.code == 400
- json_data = JSON.parse(response.body)
+ json_data = parse_json(response.body)
raise PrometheusClient::QueryError, json_data['error'] || 'Bad data received'
else
raise PrometheusClient::Error, "#{response.code} - #{response.body}"
@@ -92,5 +129,11 @@ module Gitlab
data = yield
data['result'] if data['resultType'] == expected_type
end
+
+ def parse_json(response_body)
+ JSON.parse(response_body)
+ rescue JSON::ParserError
+ raise PrometheusClient::Error, 'Parsing response failed'
+ end
end
end
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 207a80b7db2..b4f41b9cd9a 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -24,7 +24,10 @@ module Gitlab
return [project, type, redirected_path] if project
end
- nil
+ # When a project did not exist, the parsed repo_type would be empty.
+ # In that case, we want to continue with a regular project repository. As we
+ # could create the project if the user pushing is allowed to do so.
+ [nil, Gitlab::GlRepository.default_type, nil]
end
def self.find_project(project_path)
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index 14126b6ec06..c237f4a7404 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -47,6 +47,19 @@ module Gitlab
self.source == other.source
end
+ # Handles regular expressions with the preferred RE2 library where possible
+ # via UntustedRegex. Falls back to Ruby's built-in regular expression library
+ # when the syntax would be invalid in RE2.
+ #
+ # One difference between these is `(?m)` multi-line mode. Ruby regex enables
+ # this by default, but also handles `^` and `$` differently.
+ # See: https://www.regular-expressions.info/modifiers.html
+ def self.with_fallback(pattern, multiline: false)
+ UntrustedRegexp.new(pattern, multiline: multiline)
+ rescue RegexpError
+ Regexp.new(pattern)
+ end
+
private
attr_reader :regexp
diff --git a/lib/gitlab/untrusted_regexp/ruby_syntax.rb b/lib/gitlab/untrusted_regexp/ruby_syntax.rb
index 91f300f97d0..6adf119aa75 100644
--- a/lib/gitlab/untrusted_regexp/ruby_syntax.rb
+++ b/lib/gitlab/untrusted_regexp/ruby_syntax.rb
@@ -6,7 +6,7 @@ module Gitlab
# and converts that to RE2 representation:
# /<regexp>/<flags>
class RubySyntax
- PATTERN = %r{^/(?<regexp>.+)/(?<flags>[ismU]*)$}.freeze
+ PATTERN = %r{^/(?<regexp>.*)/(?<flags>[ismU]*)$}.freeze
# Checks if pattern matches a regexp pattern
# but does not enforce it's validity
@@ -16,28 +16,47 @@ module Gitlab
# The regexp can match the pattern `/.../`, but may not be fabricatable:
# it can be invalid or incomplete: `/match ( string/`
- def self.valid?(pattern)
- !!self.fabricate(pattern)
+ def self.valid?(pattern, fallback: false)
+ !!self.fabricate(pattern, fallback: fallback)
end
- def self.fabricate(pattern)
- self.fabricate!(pattern)
+ def self.fabricate(pattern, fallback: false)
+ self.fabricate!(pattern, fallback: fallback)
rescue RegexpError
nil
end
- def self.fabricate!(pattern)
+ def self.fabricate!(pattern, fallback: false)
raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String)
matches = pattern.match(PATTERN)
raise RegexpError, 'Invalid regular expression!' if matches.nil?
- expression = matches[:regexp]
- flags = matches[:flags]
- expression.prepend("(?#{flags})") if flags.present?
+ begin
+ create_untrusted_regexp(matches[:regexp], matches[:flags])
+ rescue RegexpError
+ raise unless fallback &&
+ Feature.enabled?(:allow_unsafe_ruby_regexp, default_enabled: false)
- UntrustedRegexp.new(expression, multiline: false)
+ create_ruby_regexp(matches[:regexp], matches[:flags])
+ end
end
+
+ def self.create_untrusted_regexp(pattern, flags)
+ pattern.prepend("(?#{flags})") if flags.present?
+
+ UntrustedRegexp.new(pattern, multiline: false)
+ end
+ private_class_method :create_untrusted_regexp
+
+ def self.create_ruby_regexp(pattern, flags)
+ options = 0
+ options += Regexp::IGNORECASE if flags&.include?('i')
+ options += Regexp::MULTILINE if flags&.include?('m')
+
+ Regexp.new(pattern, options)
+ end
+ private_class_method :create_ruby_regexp
end
end
end
diff --git a/lib/gitlab/url_helpers.rb b/lib/gitlab/url_helpers.rb
new file mode 100644
index 00000000000..883585c196f
--- /dev/null
+++ b/lib/gitlab/url_helpers.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class UrlHelpers
+ WSS_PROTOCOL = "wss".freeze
+ def self.as_wss(url)
+ return unless url.present?
+
+ URI.parse(url).tap do |uri|
+ uri.scheme = WSS_PROTOCOL
+ end.to_s
+ rescue URI::InvalidURIError
+ nil
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 5d5a867c9ab..533757d2237 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -63,13 +63,26 @@ module Gitlab
]
end
- def send_git_archive(repository, ref:, format:, append_sha:)
+ def send_git_archive(repository, ref:, format:, append_sha:, path: nil)
format ||= 'tar.gz'
format = format.downcase
- params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha)
- raise "Repository or ref not found" if params.empty?
+ metadata = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha, path: path)
- params['GitalyServer'] = gitaly_server_hash(repository)
+ raise "Repository or ref not found" if metadata.empty?
+
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => encode_binary(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: archive_format(format),
+ path: path.presence || ""
+ ).to_proto
+ )
+ }
# If present DisableCache must be a Boolean. Otherwise workhorse ignores it.
params['DisableCache'] = true if git_archive_cache_disabled?
@@ -149,16 +162,16 @@ module Gitlab
]
end
- def terminal_websocket(terminal)
+ def channel_websocket(channel)
details = {
- 'Terminal' => {
- 'Subprotocols' => terminal[:subprotocols],
- 'Url' => terminal[:url],
- 'Header' => terminal[:headers],
- 'MaxSessionTime' => terminal[:max_session_time]
+ 'Channel' => {
+ 'Subprotocols' => channel[:subprotocols],
+ 'Url' => channel[:url],
+ 'Header' => channel[:headers],
+ 'MaxSessionTime' => channel[:max_session_time]
}
}
- details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.key?(:ca_pem)
+ details['Channel']['CAPem'] = channel[:ca_pem] if channel.key?(:ca_pem)
details
end
@@ -220,6 +233,10 @@ module Gitlab
Base64.urlsafe_encode64(JSON.dump(hash))
end
+ def encode_binary(binary)
+ Base64.urlsafe_encode64(binary)
+ end
+
def gitaly_server_hash(repository)
{
address: Gitlab::GitalyClient.address(repository.project.repository_storage),
@@ -238,6 +255,19 @@ module Gitlab
def git_archive_cache_disabled?
ENV['WORKHORSE_ARCHIVE_CACHE_DISABLED'].present? || Feature.enabled?(:workhorse_archive_cache_disabled)
end
+
+ def archive_format(format)
+ case format
+ when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
+ Gitaly::GetArchiveRequest::Format::TAR_BZ2
+ when "tar"
+ Gitaly::GetArchiveRequest::Format::TAR
+ when "zip"
+ Gitaly::GetArchiveRequest::Format::ZIP
+ else
+ Gitaly::GetArchiveRequest::Format::TAR_GZ
+ end
+ end
end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index 7872e5b08c0..8fadadccce9 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -14,6 +14,12 @@ namespace :gitlab do
rake_version = run_and_match(%w(rake --version), /[\d\.]+/).try(:to_s)
# check redis version
redis_version = run_and_match(%w(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
+
+ # check for system defined proxies
+ if Gitlab.ee?
+ proxies = Gitlab::Proxy.detect_proxy.map {|k, v| "#{k}: #{v}"}.join("\n\t\t")
+ end
+
# check Git version
git_version = run_and_match([Gitlab.config.git.bin_path, '--version'], /git version ([\d\.]+)/).to_a
# check Go version
@@ -22,6 +28,11 @@ namespace :gitlab do
puts ""
puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".color(:red)}"
+
+ if Gitlab.ee?
+ puts "Proxy:\t\t#{proxies.present? ? proxies.color(:green) : "no"}"
+ end
+
puts "Current User:\t#{run_command(%w(whoami))}"
puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
@@ -39,6 +50,15 @@ namespace :gitlab do
http_clone_url = project.http_url_to_repo
ssh_clone_url = project.ssh_url_to_repo
+ if Gitlab.ee?
+ geo_node_type =
+ if Gitlab::Geo.current_node
+ Gitlab::Geo.current_node.primary ? 'Primary' : 'Secondary'
+ else
+ 'Undefined'.color(:red)
+ end
+ end
+
omniauth_providers = Gitlab.config.omniauth.providers.map { |provider| provider['name'] }
puts ""
@@ -51,6 +71,13 @@ namespace :gitlab do
puts "URL:\t\t#{Gitlab.config.gitlab.url}"
puts "HTTP Clone URL:\t#{http_clone_url}"
puts "SSH Clone URL:\t#{ssh_clone_url}"
+
+ if Gitlab.ee?
+ puts "Elasticsearch:\t#{Gitlab::CurrentSettings.current_application_settings.elasticsearch_indexing? ? "yes".color(:green) : "no"}"
+ puts "Geo:\t\t#{Gitlab::Geo.enabled? ? "yes".color(:green) : "no"}"
+ puts "Geo node:\t#{geo_node_type}" if Gitlab::Geo.enabled?
+ end
+
puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}"
puts "Using Omniauth:\t#{Gitlab::Auth.omniauth_enabled? ? "yes".color(:green) : "no"}"
puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab::Auth.omniauth_enabled?
diff --git a/locale/ar_SA/gitlab.po b/locale/ar_SA/gitlab.po
index af98becc19e..32dfea864fb 100644
--- a/locale/ar_SA/gitlab.po
+++ b/locale/ar_SA/gitlab.po
@@ -534,9 +534,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index cf0ef977e47..746301e5c2c 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Ðабор от графики отноÑно непрекъÑнатата интеграциÑ"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/ca_ES/gitlab.po b/locale/ca_ES/gitlab.po
index e70b8c43f9e..3bdf99cfe57 100644
--- a/locale/ca_ES/gitlab.po
+++ b/locale/ca_ES/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/cs_CZ/gitlab.po b/locale/cs_CZ/gitlab.po
index 97c0e8046a1..e7e75019fe7 100644
--- a/locale/cs_CZ/gitlab.po
+++ b/locale/cs_CZ/gitlab.po
@@ -464,9 +464,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/cy_GB/gitlab.po b/locale/cy_GB/gitlab.po
index 1eabb89a94c..8cd9e9d2594 100644
--- a/locale/cy_GB/gitlab.po
+++ b/locale/cy_GB/gitlab.po
@@ -534,9 +534,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/da_DK/gitlab.po b/locale/da_DK/gitlab.po
index 231c3569bb2..6f75ce3cb3a 100644
--- a/locale/da_DK/gitlab.po
+++ b/locale/da_DK/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index cc544400449..35c59b619df 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "Ein Default-Branch kann nicht für ein leeres Projekt ausgewählt werden."
diff --git a/locale/el_GR/gitlab.po b/locale/el_GR/gitlab.po
index e8c50215d7e..de854b2baa0 100644
--- a/locale/el_GR/gitlab.po
+++ b/locale/el_GR/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index 5e14e94e6fc..151fbffeb02 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -52,9 +52,6 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "About auto deploy"
msgstr ""
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 8704cc351fc..34f46e6f249 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Aro da diagramoj pri la seninterrompa integrado"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index 7ac4b7c7eb1..da0a2a25adb 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Una colección de gráficos sobre Integración Continua"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/et_EE/gitlab.po b/locale/et_EE/gitlab.po
index 16c4951d8a7..e32a2158987 100644
--- a/locale/et_EE/gitlab.po
+++ b/locale/et_EE/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/fil_PH/gitlab.po b/locale/fil_PH/gitlab.po
index 271ba2587b6..242c3c770f7 100644
--- a/locale/fil_PH/gitlab.po
+++ b/locale/fil_PH/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 81d25244a74..6b4b35f4562 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Un ensemble de graphiques concernant l’intégration continue (CI)"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "Une branche par défaut ne peut pas être choisie pour un projet vide."
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bb145a0984f..3bc48e8c5d2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19,6 +19,9 @@ msgstr ""
msgid " Status"
msgstr ""
+msgid " Try to %{action} this file again."
+msgstr ""
+
msgid " You need to do this before %{grace_period_deadline}."
msgstr ""
@@ -126,6 +129,9 @@ msgstr ""
msgid "%{label_for_message} unavailable"
msgstr ""
+msgid "%{level_name} visibility has been restricted by the administrator."
+msgstr ""
+
msgid "%{link_start}Read more%{link_end} about role permissions"
msgstr ""
@@ -135,6 +141,9 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{mrText}, this issue will be closed automatically."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -232,6 +241,9 @@ msgid_plural "%d closed merge requests"
msgstr[0] ""
msgstr[1] ""
+msgid "1 day"
+msgstr ""
+
msgid "1 merged merge request"
msgid_plural "%d merged merge requests"
msgstr[0] ""
@@ -252,6 +264,9 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1 week"
+msgstr ""
+
msgid "1st contribution!"
msgstr ""
@@ -261,6 +276,15 @@ msgstr ""
msgid "2FA enabled"
msgstr ""
+msgid "3 days"
+msgstr ""
+
+msgid "3 hours"
+msgstr ""
+
+msgid "30 minutes"
+msgstr ""
+
msgid "403|Please contact your GitLab administrator to get permission."
msgstr ""
@@ -276,6 +300,9 @@ msgstr ""
msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr ""
+msgid "8 hours"
+msgstr ""
+
msgid "<code>\"johnsmith@example.com\": \"@johnsmith\"</code> will add \"By <a href=\"#\">@johnsmith</a>\" to all issues and comments originally created by johnsmith@example.com, and will set <a href=\"#\">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com."
msgstr ""
@@ -315,9 +342,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -351,6 +375,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
+msgid "API Help"
+msgstr ""
+
msgid "About GitLab"
msgstr ""
@@ -447,6 +474,9 @@ msgstr ""
msgid "Add license"
msgstr ""
+msgid "Add list"
+msgstr ""
+
msgid "Add new application"
msgstr ""
@@ -480,6 +510,9 @@ msgstr ""
msgid "Admin Overview"
msgstr ""
+msgid "Admin Section"
+msgstr ""
+
msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
@@ -1506,9 +1539,6 @@ msgstr ""
msgid "Choose <strong>Next</strong> at the bottom of the page."
msgstr ""
-msgid "Choose File ..."
-msgstr ""
-
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
@@ -1530,7 +1560,7 @@ msgstr ""
msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
msgstr ""
-msgid "Choose file..."
+msgid "Choose file…"
msgstr ""
msgid "Choose the top-level group for your repository imports."
@@ -1620,6 +1650,9 @@ msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
+msgid "CiVariables|This variable will not be masked"
+msgstr ""
+
msgid "CiVariable|* (All environments)"
msgstr ""
@@ -1629,9 +1662,15 @@ msgstr ""
msgid "CiVariable|Error occurred while saving variables"
msgstr ""
+msgid "CiVariable|Masked"
+msgstr ""
+
msgid "CiVariable|Protected"
msgstr ""
+msgid "CiVariable|Toggle masked"
+msgstr ""
+
msgid "CiVariable|Toggle protected"
msgstr ""
@@ -1710,6 +1749,9 @@ msgstr ""
msgid "ClusterIntegration|API URL"
msgstr ""
+msgid "ClusterIntegration|API URL should be a valid http/https url."
+msgstr ""
+
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
@@ -1770,6 +1812,9 @@ msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr ""
+msgid "ClusterIntegration|Cluster name is required."
+msgstr ""
+
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
msgstr ""
@@ -1791,7 +1836,7 @@ msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
-msgid "ClusterIntegration|Copy Token"
+msgid "ClusterIntegration|Copy Service Token"
msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
@@ -1983,6 +2028,9 @@ msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Number of nodes must be a numerical value."
+msgstr ""
+
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
msgstr ""
@@ -1995,9 +2043,6 @@ msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
-msgid "ClusterIntegration|Project namespace"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -2064,7 +2109,10 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
-msgid "ClusterIntegration|Service token"
+msgid "ClusterIntegration|Service Token"
+msgstr ""
+
+msgid "ClusterIntegration|Service token is required."
msgstr ""
msgid "ClusterIntegration|Show"
@@ -2097,9 +2145,6 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Token"
-msgstr ""
-
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
@@ -2501,9 +2546,6 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
-msgid "Create a new branch and merge request"
-msgstr ""
-
msgid "Create a new issue"
msgstr ""
@@ -3004,19 +3046,10 @@ msgstr ""
msgid "Download asset"
msgstr ""
-msgid "Download tar"
+msgid "Download source code"
msgstr ""
-msgid "Download tar.bz2"
-msgstr ""
-
-msgid "Download tar.gz"
-msgstr ""
-
-msgid "Download zip"
-msgstr ""
-
-msgid "DownloadArtifacts|Download"
+msgid "Download this directory"
msgstr ""
msgid "DownloadCommit|Email Patches"
@@ -3166,6 +3199,9 @@ msgstr ""
msgid "Ends at (UTC)"
msgstr ""
+msgid "Enter at least three characters to search"
+msgstr ""
+
msgid "Enter in your Bitbucket Server URL and personal access token below"
msgstr ""
@@ -3181,10 +3217,7 @@ msgstr ""
msgid "Enter the merge request title"
msgstr ""
-msgid "Environment variables"
-msgstr ""
-
-msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want."
+msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they will be masked by default so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
msgstr ""
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default"
@@ -3568,6 +3601,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
+msgid "Failed to create resources"
+msgstr ""
+
msgid "Failed to deploy to"
msgstr ""
@@ -3595,6 +3631,9 @@ msgstr ""
msgid "Failed to remove user key."
msgstr ""
+msgid "Failed to save new settings"
+msgstr ""
+
msgid "Failed to update issues, please try again."
msgstr ""
@@ -4529,6 +4568,9 @@ msgstr ""
msgid "Job"
msgstr ""
+msgid "Job Failed #%{build_id}"
+msgstr ""
+
msgid "Job ID"
msgstr ""
@@ -4589,6 +4631,9 @@ msgstr ""
msgid "Job|The artifacts will be removed"
msgstr ""
+msgid "Job|This job failed because the necessary resources were not successfully created."
+msgstr ""
+
msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it."
msgstr ""
@@ -4774,6 +4819,9 @@ msgstr ""
msgid "Leave"
msgstr ""
+msgid "Leave edit mode? All unsaved changes will be lost."
+msgstr ""
+
msgid "Leave group"
msgstr ""
@@ -4923,6 +4971,9 @@ msgstr ""
msgid "Markdown"
msgstr ""
+msgid "Markdown Help"
+msgstr ""
+
msgid "Markdown enabled"
msgstr ""
@@ -5079,6 +5130,12 @@ msgstr ""
msgid "Metrics|No deployed environments"
msgstr ""
+msgid "Metrics|Not enough data to display"
+msgstr ""
+
+msgid "Metrics|Show last"
+msgstr ""
+
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr ""
@@ -5393,6 +5450,9 @@ msgstr ""
msgid "No files found."
msgstr ""
+msgid "No job trace"
+msgstr ""
+
msgid "No labels with such name or description"
msgstr ""
@@ -5459,6 +5519,9 @@ msgstr ""
msgid "Not now"
msgstr ""
+msgid "Not ready yet. Try again later."
+msgstr ""
+
msgid "Not started"
msgstr ""
@@ -5617,6 +5680,9 @@ msgstr ""
msgid "Open in Xcode"
msgstr ""
+msgid "Open raw"
+msgstr ""
+
msgid "Open sidebar"
msgstr ""
@@ -5740,6 +5806,9 @@ msgstr ""
msgid "Permissions"
msgstr ""
+msgid "Permissions Help"
+msgstr ""
+
msgid "Permissions, LFS, 2FA"
msgstr ""
@@ -5764,6 +5833,9 @@ msgstr ""
msgid "Pipeline triggers"
msgstr ""
+msgid "Pipeline: %{status}"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -5881,12 +5953,6 @@ msgstr ""
msgid "Pipeline|Coverage"
msgstr ""
-msgid "Pipeline|Create for"
-msgstr ""
-
-msgid "Pipeline|Create pipeline"
-msgstr ""
-
msgid "Pipeline|Duration"
msgstr ""
@@ -5899,6 +5965,9 @@ msgstr ""
msgid "Pipeline|Run Pipeline"
msgstr ""
+msgid "Pipeline|Run for"
+msgstr ""
+
msgid "Pipeline|Search branches"
msgstr ""
@@ -6016,9 +6085,15 @@ msgstr ""
msgid "Preview"
msgstr ""
+msgid "Preview changes"
+msgstr ""
+
msgid "Preview payload"
msgstr ""
+msgid "Previous Artifacts"
+msgstr ""
+
msgid "Prioritize"
msgstr ""
@@ -6334,9 +6409,6 @@ msgstr ""
msgid "Project avatar"
msgstr ""
-msgid "Project avatar in repository: %{link}"
-msgstr ""
-
msgid "Project details"
msgstr ""
@@ -6373,6 +6445,21 @@ msgstr ""
msgid "ProjectActivityRSS|Subscribe"
msgstr ""
+msgid "ProjectCreationLevel|Allowed to create projects"
+msgstr ""
+
+msgid "ProjectCreationLevel|Default project creation protection"
+msgstr ""
+
+msgid "ProjectCreationLevel|Developers + Maintainers"
+msgstr ""
+
+msgid "ProjectCreationLevel|Maintainers"
+msgstr ""
+
+msgid "ProjectCreationLevel|No one"
+msgstr ""
+
msgid "ProjectFileTree|Name"
msgstr ""
@@ -6544,6 +6631,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
+msgid "Public Access Help"
+msgstr ""
+
msgid "Public deploy keys (%{deploy_keys_count})"
msgstr ""
@@ -6574,6 +6664,9 @@ msgstr ""
msgid "README"
msgstr ""
+msgid "Rake Tasks Help"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -6669,6 +6762,12 @@ msgstr ""
msgid "Remove project"
msgstr ""
+msgid "Remove this label? Are you sure?"
+msgstr ""
+
+msgid "Remove this label? This will affect all projects within the group. Are you sure?"
+msgstr ""
+
msgid "Removed group can not be restored!"
msgstr ""
@@ -6687,6 +6786,9 @@ msgstr ""
msgid "Reopen milestone"
msgstr ""
+msgid "Replace"
+msgstr ""
+
msgid "Reply to comment"
msgstr ""
@@ -6822,6 +6924,9 @@ msgstr ""
msgid "Retry this job"
msgstr ""
+msgid "Retry this job in order to create the necessary resources."
+msgstr ""
+
msgid "Retry verification"
msgstr ""
@@ -6911,6 +7016,9 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "SSH Keys Help"
+msgstr ""
+
msgid "SSH host keys"
msgstr ""
@@ -6965,6 +7073,9 @@ msgstr ""
msgid "Scope not supported with disabled 'users_search' feature!"
msgstr ""
+msgid "Scoped label"
+msgstr ""
+
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
msgstr ""
@@ -7013,6 +7124,9 @@ msgstr ""
msgid "Search users"
msgstr ""
+msgid "Search your projects"
+msgstr ""
+
msgid "SearchAutocomplete|All GitLab"
msgstr ""
@@ -7037,6 +7151,9 @@ msgstr ""
msgid "SearchAutocomplete|in this project"
msgstr ""
+msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\""
+msgstr ""
+
msgid "Secret"
msgstr ""
@@ -7127,9 +7244,24 @@ msgstr ""
msgid "Serverless"
msgstr ""
+msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first."
+msgstr ""
+
+msgid "ServerlessDetails|Install Prometheus"
+msgstr ""
+
+msgid "ServerlessDetails|Invocation metrics loading or not available at this time."
+msgstr ""
+
+msgid "ServerlessDetails|Invocations"
+msgstr ""
+
msgid "ServerlessDetails|Kubernetes Pods"
msgstr ""
+msgid "ServerlessDetails|More information"
+msgstr ""
+
msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity."
msgstr ""
@@ -7145,9 +7277,6 @@ msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
msgstr ""
-msgid "Serverless|An error occurred while retrieving serverless components"
-msgstr ""
-
msgid "Serverless|Getting started with serverless"
msgstr ""
@@ -7387,6 +7516,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching related merge requests."
+msgstr ""
+
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""
@@ -7405,15 +7537,24 @@ msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
+msgid "Something went wrong, unable to search projects"
+msgstr ""
+
msgid "Something went wrong. Please try again."
msgstr ""
+msgid "Sorry, no projects matched your search"
+msgstr ""
+
msgid "Sorry, your filter produced no results"
msgstr ""
msgid "Sort by"
msgstr ""
+msgid "Sort direction"
+msgstr ""
+
msgid "SortOptions|Access level, ascending"
msgstr ""
@@ -7597,6 +7738,9 @@ msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
+msgid "Start a new merge request"
+msgstr ""
+
msgid "Start and due date"
msgstr ""
@@ -7732,6 +7876,9 @@ msgstr ""
msgid "System Hooks"
msgstr ""
+msgid "System Hooks Help"
+msgstr ""
+
msgid "System Info"
msgstr ""
@@ -7864,6 +8011,11 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "The %{type} contains the following error:"
+msgid_plural "The %{type} contains the following errors:"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr ""
@@ -7906,6 +8058,15 @@ msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr ""
+msgid "The group and any internal projects can be viewed by any logged in user."
+msgstr ""
+
+msgid "The group and any public projects can be viewed without any authentication."
+msgstr ""
+
+msgid "The group and its projects can only be viewed by members."
+msgstr ""
+
msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}."
msgstr ""
@@ -7984,6 +8145,18 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
+msgid "The snippet can be accessed without any authentication."
+msgstr ""
+
+msgid "The snippet is visible only to me."
+msgstr ""
+
+msgid "The snippet is visible only to project members."
+msgstr ""
+
+msgid "The snippet is visible to any logged in user."
+msgstr ""
+
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
@@ -8233,9 +8406,6 @@ msgstr ""
msgid "This option is disabled as you don't have write permissions for the current branch"
msgstr ""
-msgid "This option is disabled while you still have unstaged changes"
-msgstr ""
-
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
@@ -8350,9 +8520,6 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
-msgid "Timeago|%s seconds ago"
-msgstr ""
-
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -8837,6 +9004,9 @@ msgstr ""
msgid "User map"
msgstr ""
+msgid "User settings"
+msgstr ""
+
msgid "User was successfully created."
msgstr ""
@@ -8945,6 +9115,9 @@ msgstr ""
msgid "Value"
msgstr ""
+msgid "Variables"
+msgstr ""
+
msgid "Various container registry settings."
msgstr ""
@@ -8987,6 +9160,9 @@ msgstr ""
msgid "View it on GitLab"
msgstr ""
+msgid "View job trace"
+msgstr ""
+
msgid "View jobs"
msgstr ""
@@ -9062,12 +9238,20 @@ msgstr ""
msgid "Web terminal"
msgstr ""
+msgid "Webhooks Help"
+msgstr ""
+
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
+msgid "When this merge request is accepted"
+msgid_plural "When these merge requests are accepted"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "When:"
msgstr ""
@@ -9194,6 +9378,9 @@ msgstr ""
msgid "Wiki|Create page"
msgstr ""
+msgid "Wiki|Created date"
+msgstr ""
+
msgid "Wiki|Edit Page"
msgstr ""
@@ -9212,6 +9399,9 @@ msgstr ""
msgid "Wiki|Pages"
msgstr ""
+msgid "Wiki|Title"
+msgstr ""
+
msgid "Wiki|Wiki Pages"
msgstr ""
@@ -9221,6 +9411,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
+msgid "Workflow Help"
+msgstr ""
+
msgid "Write"
msgstr ""
@@ -9425,6 +9618,12 @@ msgstr ""
msgid "You'll need to use different branch names to get a valid comparison."
msgstr ""
+msgid "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
+msgstr ""
+
+msgid "You're not allowed to make changes to this project directly. A fork of this project is being created that you can make changes in, so you can submit a merge request."
+msgstr ""
+
msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options."
msgstr ""
@@ -9927,6 +10126,9 @@ msgstr ""
msgid "private"
msgstr ""
+msgid "processing"
+msgstr ""
+
msgid "project"
msgstr ""
diff --git a/locale/gl_ES/gitlab.po b/locale/gl_ES/gitlab.po
index 962e96995b6..9cd4eb0048a 100644
--- a/locale/gl_ES/gitlab.po
+++ b/locale/gl_ES/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/he_IL/gitlab.po b/locale/he_IL/gitlab.po
index 0819ef9afee..31b4e682982 100644
--- a/locale/he_IL/gitlab.po
+++ b/locale/he_IL/gitlab.po
@@ -464,9 +464,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/hi_IN/gitlab.po b/locale/hi_IN/gitlab.po
index 779c2496e2e..fda92703ce6 100644
--- a/locale/hi_IN/gitlab.po
+++ b/locale/hi_IN/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/hr_HR/gitlab.po b/locale/hr_HR/gitlab.po
index 912929a31c4..f1886066b54 100644
--- a/locale/hr_HR/gitlab.po
+++ b/locale/hr_HR/gitlab.po
@@ -429,9 +429,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/hu_HU/gitlab.po b/locale/hu_HU/gitlab.po
index 5fe58919064..12e1cd5c79b 100644
--- a/locale/hu_HU/gitlab.po
+++ b/locale/hu_HU/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/id_ID/gitlab.po b/locale/id_ID/gitlab.po
index 9c9444e6d46..16444a8f259 100644
--- a/locale/id_ID/gitlab.po
+++ b/locale/id_ID/gitlab.po
@@ -359,9 +359,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index b63371c5616..cd4681b0c5b 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Un insieme di grafici riguardo la Continuous Integration"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index f8654721e31..d36cf65b1ea 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -359,9 +359,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 36474fb85d1..ca8e7294869 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -359,9 +359,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "빈 프로ì íŠ¸ì—서는 기본 브랜치를 ì„ íƒí•  수 없습니다."
diff --git a/locale/mn_MN/gitlab.po b/locale/mn_MN/gitlab.po
index 3942a250799..257017265f1 100644
--- a/locale/mn_MN/gitlab.po
+++ b/locale/mn_MN/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/nb_NO/gitlab.po b/locale/nb_NO/gitlab.po
index fca801a32a5..88bfd23eeea 100644
--- a/locale/nb_NO/gitlab.po
+++ b/locale/nb_NO/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 44399927a09..c80b3e3c352 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/pa_IN/gitlab.po b/locale/pa_IN/gitlab.po
index 2291dc0e557..a1d196bc86e 100644
--- a/locale/pa_IN/gitlab.po
+++ b/locale/pa_IN/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index f3266c9c227..9d6fb6ecd90 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -464,9 +464,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index f5b206ba288..969dc528332 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Uma coleção de gráficos sobre Integração Contínua"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "Um branch padrão não pode ser escolhido para um projeto vazio."
diff --git a/locale/pt_PT/gitlab.po b/locale/pt_PT/gitlab.po
index 2d43d0ebe4d..829cd4bbdac 100644
--- a/locale/pt_PT/gitlab.po
+++ b/locale/pt_PT/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/ro_RO/gitlab.po b/locale/ro_RO/gitlab.po
index e4b0be5e2a8..76895485520 100644
--- a/locale/ro_RO/gitlab.po
+++ b/locale/ro_RO/gitlab.po
@@ -429,9 +429,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index ee94200a9f7..41c64745fc3 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -464,9 +464,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Графики непрерывной интеграции (CI)"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "Ð”Ð»Ñ Ð¿ÑƒÑтого проекта Ð½ÐµÐ»ÑŒÐ·Ñ Ð²Ñ‹Ð±Ñ€Ð°Ñ‚ÑŒ ветку по умолчанию."
diff --git a/locale/sk_SK/gitlab.po b/locale/sk_SK/gitlab.po
index e1d860895f6..2179aabc6fd 100644
--- a/locale/sk_SK/gitlab.po
+++ b/locale/sk_SK/gitlab.po
@@ -464,9 +464,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/sq_AL/gitlab.po b/locale/sq_AL/gitlab.po
index cb3bb5e42a1..f19f890be7e 100644
--- a/locale/sq_AL/gitlab.po
+++ b/locale/sq_AL/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/sr_CS/gitlab.po b/locale/sr_CS/gitlab.po
index 25f70503b18..c94d364a498 100644
--- a/locale/sr_CS/gitlab.po
+++ b/locale/sr_CS/gitlab.po
@@ -429,9 +429,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/sr_SP/gitlab.po b/locale/sr_SP/gitlab.po
index f60a4196aec..5a9e09718a0 100644
--- a/locale/sr_SP/gitlab.po
+++ b/locale/sr_SP/gitlab.po
@@ -429,9 +429,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/sv_SE/gitlab.po b/locale/sv_SE/gitlab.po
index ca342915099..d1d108c1b06 100644
--- a/locale/sv_SE/gitlab.po
+++ b/locale/sv_SE/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/sw_KE/gitlab.po b/locale/sw_KE/gitlab.po
index e4c780f147f..03a4884209f 100644
--- a/locale/sw_KE/gitlab.po
+++ b/locale/sw_KE/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr ""
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/tr_TR/gitlab.po b/locale/tr_TR/gitlab.po
index dd5e8f6db76..510aec20155 100644
--- a/locale/tr_TR/gitlab.po
+++ b/locale/tr_TR/gitlab.po
@@ -394,9 +394,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Sürekli Entegrasyon için bir grafik derlemesi"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 86b7048ad3d..a45ab9df805 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -464,9 +464,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Ðабір графіків відноÑно безперервної інтеграції"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "Гілку за замовчуваннÑм не може бути обрано Ð´Ð»Ñ Ð¿Ð¾Ñ€Ð¾Ð¶Ð½ÑŒÐ¾Ð³Ð¾ проекту."
@@ -4648,9 +4645,6 @@ msgstr "Вузол не працює або зламаний."
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr "Вузол працює повільно, перевантажений або тільки що відновивÑÑ Ð¿Ñ–ÑÐ»Ñ Ð·Ð±Ð¾ÑŽ."
-msgid "GeoNodes|Alternate URL"
-msgstr ""
-
msgid "GeoNodes|Checksummed"
msgstr "Із контрольною Ñумою"
@@ -4831,9 +4825,6 @@ msgstr "Ð’ÑÑ– проекти плануютьÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— пÐ
msgid "Geo|All projects are being scheduled for re-sync"
msgstr "Ð’ÑÑ– проекти плануютьÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— Ñинхронізації"
-msgid "Geo|Alternate URL"
-msgstr ""
-
msgid "Geo|Batch operations"
msgstr "Групові операції"
@@ -12610,4 +12601,3 @@ msgstr[3] "протÑгом %d хвилин "
msgid "yaml invalid"
msgstr ""
-
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index d89914c9775..9b43afb1d72 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -359,9 +359,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "无法为空项目选择默认分支。"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 7df18cc4d70..1d770d45c1a 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -359,9 +359,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index f374616a5fb..973492be822 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -359,9 +359,6 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "ä¸é–“æ–·æ•´åˆçš„圖表集åˆ"
-
msgid "A default branch cannot be chosen for an empty project."
msgstr "無法å°ä¸€å€‹ç©ºå°ˆæ¡ˆé¸æ“‡é è¨­åˆ†æ”¯ã€‚"
diff --git a/package.json b/package.json
index 934a30df261..54962834e71 100644
--- a/package.json
+++ b/package.json
@@ -2,11 +2,12 @@
"private": true,
"scripts": {
"clean": "rm -rf public/assets tmp/cache/*-loader",
- "dev-server": "nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
+ "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-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 .",
"jest": "BABEL_ENV=jest jest",
+ "jest-debug": "BABEL_ENV=jest node --inspect-brk node_modules/.bin/jest --runInBand",
"jsdoc": "jsdoc -c config/jsdocs.config.js",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
@@ -20,8 +21,8 @@
"stylelint-file": "node node_modules/stylelint/bin/stylelint.js",
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "yarn jest && yarn karma",
- "webpack": "webpack --config config/webpack.config.js",
- "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
+ "webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
+ "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
"@babel/core": "^7.2.2",
@@ -33,7 +34,7 @@
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.58.0",
- "@gitlab/ui": "^3.0.0",
+ "@gitlab/ui": "^3.1.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-upload-client": "^10.0.0",
@@ -109,7 +110,7 @@
"sql.js": "^0.4.0",
"stickyfilljs": "^2.0.5",
"style-loader": "^0.23.1",
- "stylelint-error-string-formatter": "^1.0.1",
+ "stylelint-error-string-formatter": "1.0.2",
"svg4everybody": "2.1.9",
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
@@ -138,7 +139,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
- "@gitlab/eslint-config": "^1.4.0",
+ "@gitlab/eslint-config": "^1.5.0",
"@vue/test-utils": "^1.0.0-beta.25",
"axios-mock-adapter": "^1.15.0",
"babel-jest": "^24.1.0",
diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb
index abd8ebb089f..89806430edb 100644
--- a/qa/qa/page/file/show.rb
+++ b/qa/qa/page/file/show.rb
@@ -6,7 +6,7 @@ module QA
view 'app/helpers/blob_helper.rb' do
element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
- element :delete_button, /label:\s+"Delete"/ # rubocop:disable QA/ElementWithPattern
+ element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/blob/_remove.html.haml' do
diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb
index ffd5b36e1ae..63a230fbdb2 100644
--- a/qa/qa/page/project/operations/kubernetes/add_existing.rb
+++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb
@@ -6,7 +6,7 @@ module QA
class AddExisting < Page::Base
view 'app/views/clusters/clusters/user/_form.html.haml' do
element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
- element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern
+ element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern
element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
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 1eada4a6c28..0ff71baed90 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
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/50
+ context 'Create', :quarantine do
describe 'Merge request creation' do
it 'user creates a new merge request', :smoke do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
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 b643468a664..f6f0468e76e 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
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create', :smoke do
+ # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/49
+ context 'Create', :smoke, :quarantine do
describe 'Snippet creation' do
it 'User creates a snippet' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
index 0837b720df1..33f342edb08 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
@@ -9,17 +9,17 @@ module QA
Resource::CiVariable.fabricate! do |resource|
resource.key = 'VARIABLE_KEY'
- resource.value = 'some CI variable'
+ resource.value = 'some_CI_variable'
end
Page::Project::Settings::CICD.perform do |settings|
settings.expand_ci_variables do |page|
expect(page).to have_field(with: 'VARIABLE_KEY')
- expect(page).not_to have_field(with: 'some CI variable')
+ expect(page).not_to have_field(with: 'some_CI_variable')
page.reveal_variables
- expect(page).to have_field(with: 'some CI variable')
+ expect(page).to have_field(with: 'some_CI_variable')
end
end
end
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 9f34e4218c1..caa9be341b4 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
@@ -8,11 +8,11 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
deploy_token_name = 'deploy token name'
- deploy_token_expires_at = Date.today + 7 # 1 Week from now
+ one_week_from_now = Date.today + 7
deploy_token = Resource::DeployToken.fabricate! do |resource|
resource.name = deploy_token_name
- resource.expires_at = deploy_token_expires_at
+ resource.expires_at = one_week_from_now
end
expect(deploy_token.username.length).to be > 0
diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb
index 49a1af8e9f0..de8cfa1aed9 100644
--- a/qa/qa/tools/generate_perf_testdata.rb
+++ b/qa/qa/tools/generate_perf_testdata.rb
@@ -33,6 +33,7 @@ module QA
add_new_file
methods_arr = [
method(:create_issues),
+ method(:create_labels),
method(:create_todos),
method(:create_merge_requests),
method(:create_issue_with_500_discussions),
@@ -80,6 +81,15 @@ module QA
STDOUT.puts "Created todos"
end
+ def create_labels
+ 30.times do |i|
+ post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/labels").url,
+ "name=label#{i}&color=#{Faker::Color.hex_color}"
+ end
+ @urls[:labels_page] = @urls[:project_page] + "/labels"
+ STDOUT.puts "Created labels"
+ end
+
def create_merge_requests
30.times do |i|
post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests").url, "source_branch=branch#{i}&target_branch=master&title=MR#{i}"
@@ -108,36 +118,77 @@ module QA
500.times do
post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues/#{issue_id}/discussions").url, "body=\"Let us discuss\""
end
+
+ labels_list = (0..15).map {|i| "label#{i}"}.join(',')
+ # Add description and labels
+ put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/issues/#{issue_id}").url, "description=#{Faker::Lorem.sentences(500).join(" ")}&labels=#{labels_list}"
@urls[:large_issue] = @urls[:project_page] + "/issues/#{issue_id}"
STDOUT.puts "Created Issue with 500 Discussions"
end
def create_mr_with_large_files
content_arr = []
- 20.times do |i|
+ 16.times do |i|
faker_line_arr = Faker::Lorem.sentences(1500)
content = faker_line_arr.join("\n\r")
- post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=master&commit_message=\"Add hello#{i}.txt\"&content=#{content}"
+ post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url,
+ "branch=master&commit_message=\"Add hello#{i}.txt\"&content=#{content}"
content_arr[i] = faker_line_arr
end
- post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/branches").url, "branch=performance&ref=master"
+ post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/branches").url,
+ "branch=performance&ref=master"
- 20.times do |i|
+ 16.times do |i|
missed_line_array = content_arr[i].each_slice(2).map(&:first)
content = missed_line_array.join("\n\rIm new!:D \n\r ")
- put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url, "branch=performance&commit_message=\"Update hello#{i}.txt\"&content=#{content}"
+ put Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/repository/files/hello#{i}.txt").url,
+ "branch=performance&commit_message=\"Update hello#{i}.txt\"&content=#{content}"
end
- create_mr_response = post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests").url, "source_branch=performance&target_branch=master&title=Large_MR"
+ create_mr_response = post Runtime::API::Request.new(@api_client, """/projects/#{@group_name}%2F#{@project_name}/merge_requests""").url,
+ "source_branch=performance&target_branch=master&title=Large_MR"
iid = JSON.parse(create_mr_response.body)["iid"]
- 500.times do
- post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url, "body=\"Let us discuss\""
+ diff_refs = JSON.parse(create_mr_response.body)["diff_refs"]
+
+ # Add discussions to diff tab and resolve a few!
+ should_resolve = false
+ 16.times do |i|
+ 1.upto(9) do |j|
+ create_diff_note(iid, i, j, diff_refs["head_sha"], diff_refs["start_sha"], diff_refs["base_sha"], "new_line")
+ create_diff_note_response = create_diff_note(iid, i, j, diff_refs["head_sha"], diff_refs["start_sha"], diff_refs["base_sha"], "old_line")
+
+ if should_resolve
+ discussion_id = JSON.parse(create_diff_note_response.body)["id"]
+ put Runtime::API::Request.new(@api_client, """/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions/#{discussion_id}""").url,
+ "resolved=true"
+ end
+
+ should_resolve ^= true
+ end
+ end
+
+ # Add discussions to main tab
+ 100.times do
+ post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url,
+ "body=\"Let us discuss\""
end
@urls[:large_mr] = JSON.parse(create_mr_response.body)["web_url"]
STDOUT.puts "Created MR with 500 Discussions and 20 Very Large Files"
end
+
+ def create_diff_note(iid, file_count, line_count, head_sha, start_sha, base_sha, line_type)
+ post Runtime::API::Request.new(@api_client, "/projects/#{@group_name}%2F#{@project_name}/merge_requests/#{iid}/discussions").url,
+ """body=\"Let us discuss\"&
+ position[position_type]=text&
+ position[new_path]=hello#{file_count}.txt&
+ position[old_path]=hello#{file_count}.txt&
+ position[#{line_type}]=#{line_count * 100}&
+ position[head_sha]=#{head_sha}&
+ position[start_sha]=#{start_sha}&
+ position[base_sha]=#{base_sha}"""
+ end
end
end
end
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index b1ada3c99db..efb620fe6dd 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -3,7 +3,7 @@ require Rails.root.join('config', 'object_store_settings.rb')
describe ObjectStoreSettings do
describe '.parse' do
- it 'should set correct default values' do
+ it 'sets correct default values' do
settings = described_class.parse(nil)
expect(settings['enabled']).to be false
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 9af472df74e..1a7be4c9a85 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -85,6 +85,13 @@ describe Admin::ApplicationSettingsController do
expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.receive_max_input_size).to eq(1024)
end
+
+ it 'updates the default_project_creation for string value' do
+ put :update, params: { application_setting: { default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } }
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
end
describe 'PUT #reset_registration_token' do
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 647fce0ecef..22165faa625 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -60,5 +60,11 @@ describe Admin::GroupsController do
expect(response).to redirect_to(admin_group_path(group))
expect(group.users).not_to include group_user
end
+
+ it 'updates the project_creation_level successfully' do
+ expect do
+ post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS } }
+ end.to change { group.reload.project_creation_level }.to(::Gitlab::Access::NO_ONE_PROJECT_ACCESS)
+ end
end
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index 8580900215c..a82b66361ca 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -117,7 +117,7 @@ describe IssuableCollections do
due_date: '2017-01-01',
group_id: '3',
iids: '4',
- label_name: 'foo',
+ label_name: ['foo'],
milestone_title: 'bar',
my_reaction_emoji: 'thumbsup',
non_archived: 'true',
@@ -142,7 +142,7 @@ describe IssuableCollections do
'author_id' => '2',
'author_username' => 'user2',
'confidential' => true,
- 'label_name' => 'foo',
+ 'label_name' => ['foo'],
'milestone_title' => 'bar',
'my_reaction_emoji' => 'thumbsup',
'due_date' => '2017-01-01',
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index ab40b4eb178..828de0e7ca5 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -75,7 +75,7 @@ describe Dashboard::MilestonesController do
expect(response.body).not_to include(project_milestone.title)
end
- it 'should show counts of group and project milestones to which the user belongs to' do
+ it 'shows counts of group and project milestones to which the user belongs to' do
get :index
expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>")
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index c857a78d5e8..b039ec2906c 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -23,4 +23,37 @@ describe DashboardController do
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
+
+ describe "#check_filters_presence!" do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ get :merge_requests, params: params
+ end
+
+ context "no filters" do
+ let(:params) { {} }
+
+ it 'sets @no_filters_set to false' do
+ expect(assigns[:no_filters_set]).to eq(true)
+ end
+ end
+
+ context "scalar filters" do
+ let(:params) { { author_id: user.id } }
+
+ it 'sets @no_filters_set to false' do
+ expect(assigns[:no_filters_set]).to eq(false)
+ end
+ end
+
+ context "array filters" do
+ let(:params) { { label_name: ['bug'] } }
+
+ it 'sets @no_filters_set to false' do
+ expect(assigns[:no_filters_set]).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index ef23ffaa843..e5180ec5c5c 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -455,7 +455,7 @@ describe Groups::ClustersController do
context 'when domain is invalid' do
let(:domain) { 'http://not-a-valid-domain' }
- it 'should not update cluster attributes' do
+ it 'does not update cluster attributes' do
go
cluster.reload
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index 15eb0a442a6..3290ed8b088 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -124,7 +124,7 @@ describe Groups::Settings::CiCdController do
end
context 'when explicitly enabling auto devops' do
- it 'should update group attribute' do
+ it 'updates group attribute' do
expect(group.auto_devops_enabled).to eq(true)
end
end
@@ -132,7 +132,7 @@ describe Groups::Settings::CiCdController do
context 'when explicitly disabling auto devops' do
let(:auto_devops_param) { '0' }
- it 'should update group attribute' do
+ it 'updates group attribute' do
expect(group.auto_devops_enabled).to eq(false)
end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index b2e6df6060a..4a28a27da79 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -349,6 +349,13 @@ describe GroupsController do
expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path')
end
+
+ it 'updates the project_creation_level successfully' do
+ post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } }
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
end
describe '#ensure_canonical_path' do
@@ -566,11 +573,11 @@ describe GroupsController do
}
end
- it 'should return a notice' do
+ it 'returns a notice' do
expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
end
- it 'should redirect to the new path' do
+ it 'redirects to the new path' do
expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}")
end
end
@@ -587,11 +594,11 @@ describe GroupsController do
}
end
- it 'should return a notice' do
+ it 'returns a notice' do
expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
end
- it 'should redirect to the new path' do
+ it 'redirects to the new path' do
expect(response).to redirect_to("/#{group.path}")
end
end
@@ -611,12 +618,12 @@ describe GroupsController do
}
end
- it 'should return an alert' do
+ it 'returns an alert' do
expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved"
end
- it 'should redirect to the current path' do
- expect(response).to render_template(:edit)
+ it 'redirects to the current path' do
+ expect(response).to redirect_to(edit_group_path(group))
end
end
@@ -633,7 +640,7 @@ describe GroupsController do
}
end
- it 'should be denied' do
+ it 'is denied' do
expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 06c6f49f7cc..b823a8d7463 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -55,7 +55,7 @@ describe OmniauthCallbacksController, type: :controller do
allow(@routes).to receive(:generate_extras) { [path, []] }
end
- it 'it calls through to the failure handler' do
+ it 'calls through to the failure handler' do
request.env['omniauth.error'] = OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch")
request.env['omniauth.error.strategy'] = OmniAuth::Strategies::SAML.new(nil)
stub_route_as('/users/auth/saml/callback')
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 3801fca09dc..32949e0e7d6 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -285,7 +285,7 @@ describe Projects::BlobController do
merge_request.update!(source_project: other_project, target_project: other_project)
end
- it "it redirect to blob" do
+ it "redirects to blob" do
put :update, params: mr_params
expect(response).to redirect_to(blob_after_edit_path)
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index cfa010c2d1c..0b79484bbfa 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -16,15 +16,15 @@ describe Projects::Ci::LintsController do
get :show, params: { namespace_id: project.namespace, project_id: project }
end
- it 'should be success' do
+ it 'is success' do
expect(response).to be_success
end
- it 'should render show page' do
+ it 'renders show page' do
expect(response).to render_template :show
end
- it 'should retrieve project' do
+ it 'retrieves project' do
expect(assigns(:project)).to eq(project)
end
end
@@ -36,7 +36,7 @@ describe Projects::Ci::LintsController do
get :show, params: { namespace_id: project.namespace, project_id: project }
end
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
@@ -74,7 +74,7 @@ describe Projects::Ci::LintsController do
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'should be success' do
+ it 'is success' do
expect(response).to be_success
end
@@ -82,7 +82,7 @@ describe Projects::Ci::LintsController do
expect(response).to render_template :show
end
- it 'should retrieve project' do
+ it 'retrieves project' do
expect(assigns(:project)).to eq(project)
end
end
@@ -102,7 +102,7 @@ describe Projects::Ci::LintsController do
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'should assign errors' do
+ it 'assigns errors' do
expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt')
end
end
@@ -114,7 +114,7 @@ describe Projects::Ci::LintsController do
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 8cb9130b834..9f753e5641f 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -15,7 +15,7 @@ describe Projects::CommitsController do
describe "GET commits_root" do
context "no ref is provided" do
- it 'should redirect to the default branch of the project' do
+ it 'redirects to the default branch of the project' do
get(:commits_root,
params: {
namespace_id: project.namespace,
@@ -113,6 +113,8 @@ describe Projects::CommitsController do
render_views
before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original unless id.include?(' ')
+
get(:signatures,
params: {
namespace_id: project.namespace,
diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
new file mode 100644
index 00000000000..5a0b92c2514
--- /dev/null
+++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Environments::PrometheusApiController do
+ set(:project) { create(:project) }
+ set(:environment) { create(:environment, project: project) }
+ set(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+ end
+
+ describe 'GET #proxy' do
+ let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) }
+ let(:expected_params) do
+ ActionController::Parameters.new(
+ environment_params(
+ proxy_path: 'query',
+ controller: 'projects/environments/prometheus_api',
+ action: 'proxy'
+ )
+ ).permit!
+ end
+
+ context 'with valid requests' do
+ before do
+ allow(Prometheus::ProxyService).to receive(:new)
+ .with(environment, 'GET', 'query', expected_params)
+ .and_return(prometheus_proxy_service)
+
+ allow(prometheus_proxy_service).to receive(:execute)
+ .and_return(service_result)
+ end
+
+ context 'with success result' do
+ let(:service_result) { { status: :success, body: prometheus_body } }
+ let(:prometheus_body) { '{"status":"success"}' }
+ let(:prometheus_json_body) { JSON.parse(prometheus_body) }
+
+ it 'returns prometheus response' do
+ get :proxy, params: environment_params
+
+ expect(Prometheus::ProxyService).to have_received(:new)
+ .with(environment, 'GET', 'query', expected_params)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(prometheus_json_body)
+ end
+ end
+
+ context 'with nil result' do
+ let(:service_result) { nil }
+
+ it 'returns 202 accepted' do
+ get :proxy, params: environment_params
+
+ expect(json_response['status']).to eq('processing')
+ expect(json_response['message']).to eq('Not ready yet. Try again later.')
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'with 404 result' do
+ let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } }
+
+ it 'returns body' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['body']).to eq('value')
+ end
+ end
+
+ context 'with error result' do
+ context 'with http_status' do
+ let(:service_result) do
+ { http_status: :service_unavailable, status: :error, message: 'error message' }
+ end
+
+ it 'sets the http response status code' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ expect(json_response['status']).to eq('error')
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+
+ context 'without http_status' do
+ let(:service_result) { { status: :error, message: 'error message' } }
+
+ it 'returns bad_request' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['status']).to eq('error')
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+ end
+ end
+
+ context 'with inappropriate requests' do
+ context 'with anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signin page' do
+ get :proxy, params: environment_params
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'without correct permissions' do
+ before do
+ project.team.truncate
+ end
+
+ it 'returns 404' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with invalid environment id' do
+ let(:other_environment) { create(:environment) }
+
+ it 'returns 404' do
+ get :proxy, params: environment_params(id: other_environment.id)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ private
+
+ def environment_params(params = {})
+ {
+ id: environment.id.to_s,
+ namespace_id: project.namespace.name,
+ project_id: project.name,
+ proxy_path: 'query',
+ query: '1'
+ }.merge(params)
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 36ce1119100..43639875265 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -283,7 +283,7 @@ describe Projects::EnvironmentsController do
.and_return([:fake_terminal])
expect(Gitlab::Workhorse)
- .to receive(:terminal_websocket)
+ .to receive(:channel_websocket)
.with(:fake_terminal)
.and_return(workhorse: :response)
@@ -392,7 +392,7 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
- get :additional_metrics, params: environment_params(format: :json)
+ additional_metrics
expect(response).to have_gitlab_http_status(204)
expect(json_response).to eq({})
@@ -412,7 +412,7 @@ describe Projects::EnvironmentsController do
end
it 'returns a metrics JSON document' do
- get :additional_metrics, params: environment_params(format: :json)
+ additional_metrics
expect(response).to be_ok
expect(json_response['success']).to be(true)
@@ -420,6 +420,32 @@ describe Projects::EnvironmentsController do
expect(json_response['last_update']).to eq(42)
end
end
+
+ context 'when only one time param is provided' do
+ context 'when :metrics_time_window feature flag is disabled' do
+ before do
+ stub_feature_flags(metrics_time_window: false)
+ expect(environment).to receive(:additional_metrics).with(no_args).and_return(nil)
+ end
+
+ it 'returns a time-window agnostic response' do
+ additional_metrics(start: '1552647300.651094')
+
+ expect(response).to have_gitlab_http_status(204)
+ expect(json_response).to eq({})
+ end
+ end
+
+ it 'raises an error when start is missing' do
+ expect { additional_metrics(start: '1552647300.651094') }
+ .to raise_error(ActionController::ParameterMissing)
+ end
+
+ it 'raises an error when end is missing' do
+ expect { additional_metrics(start: '1552647300.651094') }
+ .to raise_error(ActionController::ParameterMissing)
+ end
+ end
end
describe 'GET #search' do
@@ -500,4 +526,8 @@ describe Projects::EnvironmentsController do
project_id: project,
id: environment.id)
end
+
+ def additional_metrics(opts = {})
+ get :additional_metrics, params: environment_params(format: :json, **opts)
+ end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index d8a331b3cf0..23e4e9806c2 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -989,7 +989,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'and valid id' do
it 'returns the terminal for the job' do
expect(Gitlab::Workhorse)
- .to receive(:terminal_websocket)
+ .to receive(:channel_websocket)
.and_return(workhorse: :response)
get_terminal_websocket(id: job.id)
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c8fa93a74ee..017162519d8 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -60,6 +60,8 @@ describe Projects::MergeRequestsController do
end
it "renders merge request page" do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
go(format: :html)
expect(response).to be_success
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 86a12a5e903..f2b73956e8d 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -65,7 +65,7 @@ describe Projects::MirrorsController do
expect(flash[:notice]).to match(/successfully updated/)
end
- it 'should create a RemoteMirror object' do
+ it 'creates a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
end
end
@@ -82,7 +82,7 @@ describe Projects::MirrorsController do
expect(flash[:alert]).to match(/Only allowed protocols are/)
end
- it 'should not create a RemoteMirror object' do
+ it 'does not create a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index b64ae552efc..814100f7d5d 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -97,6 +97,8 @@ describe Projects::PipelinesController do
RequestStore.clear!
RequestStore.begin!
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
expect { get_pipelines_index_json }
.to change { Gitlab::GitalyClient.get_request_count }.by(2)
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 3cc3fe69fba..33486edcdd1 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::ProjectMembersController do
let(:project) { create(:project, :public, :access_requestable) }
describe 'GET index' do
- it 'should have the project_members address with a 200 status code' do
+ it 'has the project_members address with a 200 status code' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(200)
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 276cf340962..782f5f272d9 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -76,6 +76,15 @@ describe Projects::Serverless::FunctionsController do
end
end
+ describe 'GET #metrics' do
+ context 'invalid data' do
+ it 'has a bad function name' do
+ get :metrics, params: params({ format: :json, environment_id: "*", id: "foo" })
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+ end
+
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 601a292bf54..d00d5bf579d 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -147,7 +147,7 @@ describe Projects::ServicesController do
params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' } }
end
- it 'should not update the service' do
+ it 'does not update the service' do
service.reload
expect(service.namespace).not_to eq('updated_namespace')
end
@@ -172,7 +172,7 @@ describe Projects::ServicesController do
context 'with approved services' do
let(:service_id) { 'jira' }
- it 'should render edit page' do
+ it 'renders edit page' do
expect(response).to be_success
end
end
@@ -180,7 +180,7 @@ describe Projects::ServicesController do
context 'with a deprecated service' do
let(:service_id) { 'kubernetes' }
- it 'should render edit page' do
+ it 'renders edit page' do
expect(response).to be_success
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 356d606d5c5..af437c5561b 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -77,6 +77,10 @@ describe ProjectsController do
end
context "user has access to project" do
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ end
+
context "and does not have notification setting" do
it "initializes notification as disabled" do
get :show, params: { namespace_id: public_project.namespace, id: public_project }
@@ -703,6 +707,16 @@ describe ProjectsController do
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
+ context 'when not authorized' do
+ let(:private_project) { create(:project, :private) }
+
+ it 'returns 404' do
+ post :preview_markdown, params: { namespace_id: private_project.namespace, id: private_project, text: '*Markdown* text' }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
context 'state filter on references' do
let(:issue) { create(:issue, :closed, project: public_project) }
let(:merge_request) { create(:merge_request, :closed, target_project: public_project) }
diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb
index c71d75a3e7f..3cbbba934b8 100644
--- a/spec/controllers/user_callouts_controller_spec.rb
+++ b/spec/controllers/user_callouts_controller_spec.rb
@@ -14,11 +14,11 @@ describe UserCalloutsController do
let(:feature_name) { UserCallout.feature_names.keys.first }
context 'when callout entry does not exist' do
- it 'should create a callout entry with dismissed state' do
+ it 'creates a callout entry with dismissed state' do
expect { subject }.to change { UserCallout.count }.by(1)
end
- it 'should return success' do
+ it 'returns success' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -28,7 +28,7 @@ describe UserCalloutsController do
context 'when callout entry already exists' do
let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) }
- it 'should return success' do
+ it 'returns success' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -39,7 +39,7 @@ describe UserCalloutsController do
context 'with invalid feature name' do
let(:feature_name) { 'bogus_feature_name' }
- it 'should return bad request' do
+ it 'returns bad request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 067391c1179..f8c494c159e 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -336,6 +336,11 @@ FactoryBot.define do
failure_reason 2
end
+ trait :prerequisite_failure do
+ failed
+ failure_reason 10
+ end
+
trait :with_runner_session do
after(:build) do |build|
build.build_runner_session(url: 'https://localhost')
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index dcef8571f41..18a0c2ec731 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
path { name.downcase.gsub(/\s/, '_') }
type 'Group'
owner nil
+ project_creation_level ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS
after(:create) do |group|
if group.owner
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index ea69ec0319b..4c6175f5590 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -345,7 +345,7 @@ describe 'Issue Boards', :js do
click_link 'Create project label'
- fill_in('new_label_name', with: 'Testing New Label')
+ fill_in('new_label_name', with: 'Testing New Label - with list')
first('.suggest-colors a').click
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index ee38e756f9e..dfdb8d589eb 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -343,6 +343,24 @@ describe 'Issue Boards', :js do
expect(page).to have_link 'test label'
end
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'creates project label and list' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+ click_link 'Create project label'
+ fill_in 'new_label_name', with: 'test label'
+ first('.suggest-colors-dropdown a').click
+ first('.js-add-list').click
+ click_button 'Create'
+ wait_for_requests
+
+ expect(page).to have_link 'test label'
+ end
+ expect(page).to have_selector('.board', count: 4)
end
end
diff --git a/spec/features/commits/user_uses_quick_actions_spec.rb b/spec/features/commits/user_uses_quick_actions_spec.rb
index 9a4b7bd2444..4b7e7465df1 100644
--- a/spec/features/commits/user_uses_quick_actions_spec.rb
+++ b/spec/features/commits/user_uses_quick_actions_spec.rb
@@ -22,27 +22,6 @@ describe 'Commit > User uses quick actions', :js do
let(:tag_message) { 'Stable release' }
let(:truncated_commit_sha) { Commit.truncate_sha(commit.sha) }
- it 'tags this commit' do
- add_note("/tag #{tag_name} #{tag_message}")
-
- expect(page).to have_content 'Commands applied'
- expect(page).to have_content "tagged commit #{truncated_commit_sha}"
- expect(page).to have_content tag_name
-
- visit project_tag_path(project, tag_name)
- expect(page).to have_content tag_name
- expect(page).to have_content tag_message
- expect(page).to have_content truncated_commit_sha
- end
-
- describe 'preview', :js do
- it 'removes quick action from note and explains it' do
- preview_note("/tag #{tag_name} #{tag_message}")
-
- expect(page).not_to have_content '/tag'
- expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"}
- expect(page).to have_content tag_name
- end
- end
+ it_behaves_like 'tag quick action'
end
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 9ffa75aee47..4965770605a 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -44,6 +44,8 @@ describe 'Dashboard Merge Requests' do
end
context 'merge requests exist' do
+ let(:label) { create(:label) }
+
let!(:assigned_merge_request) do
create(:merge_request,
assignee: current_user,
@@ -72,6 +74,14 @@ describe 'Dashboard Merge Requests' do
target_project: public_project, source_project: forked_project)
end
+ let!(:labeled_merge_request) do
+ create(:labeled_merge_request,
+ source_branch: 'labeled',
+ labels: [label],
+ author: current_user,
+ source_project: project)
+ end
+
let!(:other_merge_request) do
create(:merge_request,
source_branch: 'fix',
@@ -90,6 +100,7 @@ describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(authored_merge_request.title)
expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
+ expect(page).not_to have_content(labeled_merge_request.title)
end
it 'shows authored merge requests', :js do
@@ -98,7 +109,21 @@ describe 'Dashboard Merge Requests' do
expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title)
+ expect(page).to have_content(labeled_merge_request.title)
+
+ expect(page).not_to have_content(assigned_merge_request.title)
+ expect(page).not_to have_content(assigned_merge_request_from_fork.title)
+ expect(page).not_to have_content(other_merge_request.title)
+ end
+
+ it 'shows labeled merge requests', :js do
+ reset_filters
+ input_filtered_search("label:#{label.name}")
+ expect(page).to have_content(labeled_merge_request.title)
+
+ expect(page).not_to have_content(authored_merge_request.title)
+ expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(assigned_merge_request.title)
expect(page).not_to have_content(assigned_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8d801161148..b2b3382666a 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -34,7 +34,7 @@ describe 'Expand and collapse diffs', :js do
define_method(file.split('.').first) { file_container(file) }
end
- it 'should show the diff content with a highlighted line when linking to line' do
+ it 'shows the diff content with a highlighted line when linking to line' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
@@ -48,7 +48,7 @@ describe 'Expand and collapse diffs', :js do
expect(large_diff).to have_selector('.hll')
end
- it 'should show the diff content when linking to file' do
+ it 'shows the diff content when linking to file' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index 8ed4051856e..56f6b1f7eaf 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -68,17 +68,17 @@ describe 'Explore Groups page', :js do
end
describe 'landing component' do
- it 'should show a landing component' do
+ it 'shows a landing component' do
expect(page).to have_content('Below you will find all the groups that are public.')
end
- it 'should be dismissable' do
+ it 'is dismissable' do
find('.dismiss-button').click
expect(page).not_to have_content('Below you will find all the groups that are public.')
end
- it 'should persistently not show once dismissed' do
+ it 'does not show persistently once dismissed' do
find('.dismiss-button').click
visit explore_groups_path
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 1a53e7c9512..fc5777e8c7c 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Group variables', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
- let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', group: group) }
+ let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', masked: true, group: group) }
let(:page_path) { group_settings_ci_cd_path(group) }
before do
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index 2410cd92e3f..b661b5cbaef 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -69,7 +69,7 @@ describe 'User Cluster', :js do
end
it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ expect(page).to have_css('.gl-field-error')
end
end
end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 378e4d5febc..5cef5f0521f 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -77,6 +77,14 @@ describe 'Edit group settings' do
end
end
+ describe 'project creation level menu' do
+ it 'shows the selection menu' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Allowed to create projects')
+ end
+ end
+
describe 'edit group avatar' do
before do
visit edit_group_path(group)
diff --git a/spec/features/groups/settings/ci_cd_spec.rb b/spec/features/groups/settings/ci_cd_spec.rb
index 0f793dbab6e..5b1a9512c55 100644
--- a/spec/features/groups/settings/ci_cd_spec.rb
+++ b/spec/features/groups/settings/ci_cd_spec.rb
@@ -43,7 +43,7 @@ describe 'Group CI/CD settings' do
end
context 'as owner first visiting group settings' do
- it 'should see instance enabled badge' do
+ it 'sees instance enabled badge' do
visit group_settings_ci_cd_path(group)
page.within '#auto-devops-settings' do
@@ -53,7 +53,7 @@ describe 'Group CI/CD settings' do
end
context 'when Auto DevOps group has been enabled' do
- it 'should see group enabled badge' do
+ it 'sees group enabled badge' do
group.update!(auto_devops_enabled: true)
visit group_settings_ci_cd_path(group)
@@ -65,7 +65,7 @@ describe 'Group CI/CD settings' do
end
context 'when Auto DevOps group has been disabled' do
- it 'should not see a badge' do
+ it 'does not see a badge' do
group.update!(auto_devops_enabled: false)
visit group_settings_ci_cd_path(group)
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c2f32c76422..8e7f78cab81 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -237,7 +237,7 @@ describe 'Group' do
let!(:project) { create(:project, namespace: group) }
let!(:path) { group_path(group) }
- it 'it renders projects and groups on the page' do
+ it 'renders projects and groups on the page' do
visit path
wait_for_requests
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index e24b1f4349d..bcd2b90d3bb 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -82,15 +82,15 @@ describe 'Help Pages' do
visit help_path
end
- it 'should display custom help page text' do
+ it 'displays custom help page text' do
expect(page).to have_text "My Custom Text"
end
- it 'should hide marketing content when enabled' do
+ it 'hides marketing content when enabled' do
expect(page).not_to have_link "Get a support subscription"
end
- it 'should use a custom support url' do
+ it 'uses a custom support url' do
expect(page).to have_link "See our website for getting help", href: "http://example.com/help"
end
end
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index 23385ba65fc..870e92b8de8 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -70,7 +70,7 @@ describe "Internal references", :js do
page.within("#merge-requests ul") do
expect(page).to have_content(private_project_merge_request.title)
- expect(page).to have_css(".merge-request-status")
+ expect(page).to have_css(".ic-issue-open-m")
end
expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index e0b1e286dee..75313442b65 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -42,7 +42,7 @@ describe 'Dropdown assignee', :js do
expect(page).to have_css(js_dropdown_assignee, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:')
@@ -51,13 +51,13 @@ describe 'Dropdown assignee', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the assignees when opened' do
+ it 'loads all the assignees when opened' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(4)
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index bedc61b9eed..bc8d9bc8450 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -50,7 +50,7 @@ describe 'Dropdown author', :js do
expect(page).to have_css(js_dropdown_author, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('author:')
@@ -58,13 +58,13 @@ describe 'Dropdown author', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
send_keys_to_filtered_search('author:')
expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading')
end
- it 'should load all the authors when opened' do
+ it 'loads all the authors when opened' do
send_keys_to_filtered_search('author:')
expect(dropdown_author_size).to eq(4)
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index f36d4e8f23f..a5c3ab7e7d0 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -69,7 +69,7 @@ describe 'Dropdown emoji', :js do
expect(page).to have_css(js_dropdown_emoji, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('my-reaction:')
@@ -77,13 +77,13 @@ describe 'Dropdown emoji', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
send_keys_to_filtered_search('my-reaction:')
expect(page).not_to have_css('#js-dropdown-my-reaction .filter-dropdown-loading')
end
- it 'should load all the emojis when opened' do
+ it 'loads all the emojis when opened' do
send_keys_to_filtered_search('my-reaction:')
expect(dropdown_emoji_size).to eq(4)
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index b330eafe1d1..7584339ccc0 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -49,7 +49,7 @@ describe 'Dropdown milestone', :js do
expect(page).to have_css(js_dropdown_milestone, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('milestone:')
@@ -57,13 +57,13 @@ describe 'Dropdown milestone', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
filtered_search.set('milestone:')
expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the milestones when opened' do
+ it 'loads all the milestones when opened' do
filtered_search.set('milestone:')
expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index f2e4c5779df..26c781350e5 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -45,7 +45,7 @@ describe 'New/edit issue', :js do
wait_for_requests
end
- it 'should display selected users even if they are not part of the original API call' do
+ it 'displays selected users even if they are not part of the original API call' do
find('.dropdown-input-field').native.send_keys user2.name
page.within '.dropdown-menu-user' do
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 76bc93e9766..791bd003597 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -26,7 +26,7 @@ describe 'Issue Detail', :js do
wait_for_requests
end
- it 'should encode the description to prevent xss issues' do
+ it 'encodes the description to prevent xss issues' do
page.within('.issuable-details .detail-page-description') do
expect(page).to have_selector('img', count: 1)
expect(find('img')['onerror']).to be_nil
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index 362f8a468ec..6a8b5e76cda 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -43,6 +43,7 @@ describe 'Issues > User uses quick actions', :js do
describe 'issue-only commands' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
before do
project.add_maintainer(user)
@@ -55,6 +56,9 @@ describe 'Issues > User uses quick actions', :js do
wait_for_requests
end
+ it_behaves_like 'confidential quick action'
+ it_behaves_like 'remove_due_date quick action'
+
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
@@ -73,24 +77,6 @@ describe 'Issues > User uses quick actions', :js do
end
end
- describe 'removing a due date from note' do
- let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
-
- it_behaves_like 'remove_due_date action available and due date can be removed'
-
- context 'when the current user cannot update the due date' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it_behaves_like 'remove_due_date action not available'
- end
- end
-
describe 'toggling the WIP prefix from the title from note' do
let(:issue) { create(:issue, project: project) }
@@ -137,42 +123,6 @@ describe 'Issues > User uses quick actions', :js do
end
end
- describe 'make issue confidential' do
- let(:issue) { create(:issue, project: project) }
- let(:original_issue) { create(:issue, project: project) }
-
- context 'when the current user can update issues' do
- it 'does not create a note, and marks the issue as confidential' do
- add_note("/confidential")
-
- expect(page).not_to have_content "/confidential"
- expect(page).to have_content 'Commands applied'
- expect(page).to have_content "made the issue confidential"
-
- expect(issue.reload).to be_confidential
- end
- end
-
- context 'when the current user cannot update the issue' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it 'does not create a note, and does not mark the issue as confidential' do
- add_note("/confidential")
-
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content "made the issue confidential"
-
- expect(issue.reload).not_to be_confidential
- end
- end
- end
-
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 7c31e67a7fa..bac297de4a6 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -145,7 +145,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do
visit new_project_issue_path(project_1)
end
- it 'should be able to assign ancestor group labels' do
+ it 'is able to assign ancestor group labels' do
fill_in 'issue_title', with: 'new created issue'
fill_in 'issue_description', with: 'new issue description'
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index 0ccab5b2fac..b8c4a78e24f 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -76,7 +76,7 @@ describe 'create a merge request, allowing commits from members who can merge to
sign_in(member)
end
- it 'it hides the option from members' do
+ it 'hides the option from members' do
visit edit_project_merge_request_path(target_project, merge_request)
expect(page).not_to have_content('Allows commits from members who can merge to the target branch')
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index dc0862be6fc..e5770905dbd 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -67,18 +67,7 @@ describe 'Merge request > User posts notes', :js do
end
end
- describe 'when reply_to_individual_notes feature flag is disabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: false)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not show a reply button' do
- expect(page).to have_no_selector('.js-reply-button')
- end
- end
-
- describe 'when reply_to_individual_notes feature flag is not set' do
+ describe 'reply button' do
before do
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 2609546990d..40ba676ff92 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -302,7 +302,7 @@ describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project)
end
- it 'should be allowed to merge' do
+ it 'is allowed to merge' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 5c45e363997..6eae3fd4676 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -230,7 +230,7 @@ describe 'Merge request > User sees versions', :js do
wait_for_requests
end
- it 'should only show diffs from the commit' do
+ it 'only shows diffs from the commit' do
diff_commit_ids = find_all('.diff-file [data-commit-id]').map {|diff| diff['data-commit-id']}
expect(diff_commit_ids).not_to be_empty
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index c19e299097e..1b5dd6945e0 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -6,6 +6,14 @@ describe 'User comments on a diff', :js do
include MergeRequestDiffHelpers
include RepoHelpers
+ def expect_suggestion_has_content(element, expected_changing_content, expected_suggested_content)
+ changing_content = element.all(:css, '.line_holder.old').map(&:text)
+ suggested_content = element.all(:css, '.line_holder.new').map(&:text)
+
+ expect(changing_content).to eq(expected_changing_content)
+ expect(suggested_content).to eq(expected_suggested_content)
+ end
+
let(:project) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
@@ -33,8 +41,18 @@ describe 'User comments on a diff', :js do
page.within('.diff-discussions') do
expect(page).to have_button('Apply suggestion')
expect(page).to have_content('Suggested change')
- expect(page).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git')
- expect(page).to have_content('# change to a comment')
+ end
+
+ page.within('.md-suggestion-diff') do
+ expected_changing_content = [
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git"
+ ]
+
+ expected_suggested_content = [
+ "6 # change to a comment"
+ ]
+
+ expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content)
end
end
@@ -64,7 +82,7 @@ describe 'User comments on a diff', :js do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
- fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion\n# or that\n```")
+ fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion:-2\n# or that\n# heh\n```")
click_button('Comment')
end
@@ -74,11 +92,90 @@ describe 'User comments on a diff', :js do
suggestion_1 = page.all(:css, '.md-suggestion-diff')[0]
suggestion_2 = page.all(:css, '.md-suggestion-diff')[1]
- expect(suggestion_1).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git')
- expect(suggestion_1).to have_content('# change to a comment')
+ suggestion_1_expected_changing_content = [
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git"
+ ]
+ suggestion_1_expected_suggested_content = [
+ "6 # change to a comment"
+ ]
+
+ suggestion_2_expected_changing_content = [
+ "4 [submodule \"gitlab-shell\"]",
+ "5 path = gitlab-shell",
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git"
+ ]
+ suggestion_2_expected_suggested_content = [
+ "4 # or that",
+ "5 # heh"
+ ]
+
+ expect_suggestion_has_content(suggestion_1,
+ suggestion_1_expected_changing_content,
+ suggestion_1_expected_suggested_content)
+
+ expect_suggestion_has_content(suggestion_2,
+ suggestion_2_expected_changing_content,
+ suggestion_2_expected_suggested_content)
+ end
+ end
+ end
+
+ context 'multi-line suggestions' do
+ it 'suggestion is presented' do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
+ click_button('Comment')
+ end
- expect(suggestion_2).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git')
- expect(suggestion_2).to have_content('# or that')
+ wait_for_requests
+
+ page.within('.diff-discussions') do
+ expect(page).to have_button('Apply suggestion')
+ expect(page).to have_content('Suggested change')
+ end
+
+ page.within('.md-suggestion-diff') do
+ expected_changing_content = [
+ "3 url = git://github.com/randx/six.git",
+ "4 [submodule \"gitlab-shell\"]",
+ "5 path = gitlab-shell",
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git",
+ "7 [submodule \"gitlab-grack\"]",
+ "8 path = gitlab-grack",
+ "9 url = https://gitlab.com/gitlab-org/gitlab-grack.git"
+ ]
+
+ expected_suggested_content = [
+ "3 # change to a",
+ "4 # comment",
+ "5 # with",
+ "6 # broken",
+ "7 # lines"
+ ]
+
+ expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content)
+ end
+ end
+
+ it 'suggestion is appliable' do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.diff-discussions') do
+ expect(page).not_to have_content('Applied')
+
+ click_button('Apply suggestion')
+ wait_for_requests
+
+ expect(page).to have_content('Applied')
end
end
end
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index a2b5859bd1e..5e466fb41d0 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -56,6 +56,9 @@ describe 'Merge request > User uses quick actions', :js do
project.add_maintainer(user)
end
+ it_behaves_like 'merge quick action'
+ it_behaves_like 'target_branch quick action'
+
describe 'toggling the WIP prefix in the title from note' do
context 'when the current user can toggle the WIP prefix' do
before do
@@ -102,151 +105,5 @@ describe 'Merge request > User uses quick actions', :js do
end
end
end
-
- describe 'merging the MR from the note' do
- context 'when the current user can merge the MR' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'merges the MR' do
- add_note("/merge")
-
- expect(page).to have_content 'Commands applied'
-
- expect(merge_request.reload).to be_merged
- end
- end
-
- context 'when the head diff changes in the meanwhile' do
- before do
- merge_request.source_branch = 'another_branch'
- merge_request.save
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not merge the MR' do
- add_note("/merge")
-
- expect(page).not_to have_content 'Your commands have been executed!'
-
- expect(merge_request.reload).not_to be_merged
- end
- end
-
- context 'when the current user cannot merge the MR' do
- before do
- project.add_guest(guest)
- sign_in(guest)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not merge the MR' do
- add_note("/merge")
-
- expect(page).not_to have_content 'Your commands have been executed!'
-
- expect(merge_request.reload).not_to be_merged
- end
- end
- end
-
- describe 'adding a due date from note' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it_behaves_like 'due quick action not available'
- end
-
- describe 'removing a due date from note' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it_behaves_like 'remove_due_date action not available'
- end
-
- describe '/target_branch command in merge request' do
- let(:another_project) { create(:project, :public, :repository) }
- let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
-
- before do
- another_project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'changes target_branch in new merge_request' do
- visit project_new_merge_request_path(another_project, new_url_opts)
-
- fill_in "merge_request_title", with: 'My brand new feature'
- fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
- click_button "Submit merge request"
-
- merge_request = another_project.merge_requests.first
- expect(merge_request.description).to eq "le feature \nFeature description:"
- expect(merge_request.target_branch).to eq 'fix'
- end
-
- it 'does not change target branch when merge request is edited' do
- new_merge_request = create(:merge_request, source_project: another_project)
-
- visit edit_project_merge_request_path(another_project, new_merge_request)
- fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
- click_button "Save changes"
-
- new_merge_request = another_project.merge_requests.first
- expect(new_merge_request.description).to include('/target_branch')
- expect(new_merge_request.target_branch).not_to eq('fix')
- end
- end
-
- describe '/target_branch command from note' do
- context 'when the current user can change target branch' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'changes target branch from a note' do
- add_note("message start \n/target_branch merge-test\n message end.")
-
- wait_for_requests
- expect(page).not_to have_content('/target_branch')
- expect(page).to have_content('message start')
- expect(page).to have_content('message end.')
-
- expect(merge_request.reload.target_branch).to eq 'merge-test'
- end
-
- it 'does not fail when target branch does not exists' do
- add_note('/target_branch totally_not_existing_branch')
-
- expect(page).not_to have_content('/target_branch')
-
- expect(merge_request.target_branch).to eq 'feature'
- end
- end
-
- context 'when current user can not change target branch' do
- before do
- project.add_guest(guest)
- sign_in(guest)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not change target branch' do
- add_note('/target_branch merge-test')
-
- expect(page).not_to have_content '/target_branch merge-test'
-
- expect(merge_request.target_branch).to eq 'feature'
- end
- end
- end
end
end
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index 6bdf5df1036..76abc640077 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value') }
+ let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value', masked: true) }
let(:page_path) { project_settings_ci_cd_path(project) }
before do
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index a7aa63018fd..aa2e538cc8e 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -572,7 +572,7 @@ describe 'File blob', :js do
visit_blob('files/ruby/test.rb', ref: 'feature')
end
- it 'should show the realtime pipeline status' do
+ it 'shows the realtime pipeline status' do
page.within('.commit-actions') do
expect(page).to have_css('.ci-status-icon')
expect(page).to have_css('.ci-status-icon-running')
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index c8dc72a34ec..3e75890725e 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -35,7 +35,7 @@ describe 'Download buttons in branches page' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, 'binary-encoding/download', job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index aa1c3902f0f..527508b3519 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -80,7 +80,7 @@ describe 'Clusters Applications', :js do
context 'on an abac cluster' do
let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) }
- it 'should show info block and not be installable' do
+ it 'shows info block and not be installable' do
page.within('.js-cluster-application-row-knative') do
expect(page).to have_css('.rbac-notice')
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
@@ -91,7 +91,7 @@ describe 'Clusters Applications', :js do
context 'on an rbac cluster' do
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- it 'should not show callout block and be installable' do
+ it 'does not show callout block and be installable' do
page.within('.js-cluster-application-row-knative') do
expect(page).not_to have_css('.rbac-notice')
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 9322e29d744..83e582c34f0 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do
end
it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ expect(page).to have_css('.gl-field-error')
end
end
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 1f2f7592d8b..fe4f737a7da 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -53,7 +53,7 @@ describe 'User Cluster', :js do
end
it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ expect(page).to have_css('.gl-field-error')
end
end
end
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 19f6ebf2c1a..614f11c8392 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -43,7 +43,7 @@ describe 'Mini Pipeline Graph in Commit View', :js do
visit project_commit_path(project, project.commit.id)
end
- it 'should not display a mini pipeline graph' do
+ it 'does not display a mini pipeline graph' do
expect(page).not_to have_selector('.mr-widget-pipeline-graph')
end
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index fe71cb7661a..da4ef6428d4 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -159,7 +159,7 @@ describe 'Environment' do
context 'for project maintainer' do
let(:role) { :maintainer }
- it 'it shows the terminal button' do
+ it 'shows the terminal button' do
expect(page).to have_terminal_button
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index b2a435e554d..7b7e45312d9 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -30,7 +30,7 @@ describe 'Environments page', :js do
end
describe 'in available tab page' do
- it 'should show one environment' do
+ it 'shows one environment' do
visit_environments(project, scope: 'available')
expect(page).to have_css('.environments-container')
@@ -44,7 +44,7 @@ describe 'Environments page', :js do
create_list(:environment, 4, project: project, state: :available)
end
- it 'should render second page of pipelines' do
+ it 'renders second page of pipelines' do
visit_environments(project, scope: 'available')
find('.js-next-button').click
@@ -56,7 +56,7 @@ describe 'Environments page', :js do
end
describe 'in stopped tab page' do
- it 'should show no environments' do
+ it 'shows no environments' do
visit_environments(project, scope: 'stopped')
expect(page).to have_css('.environments-container')
@@ -72,7 +72,7 @@ describe 'Environments page', :js do
allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
end
- it 'should show one environment without error' do
+ it 'shows one environment without error' do
visit_environments(project, scope: 'available')
expect(page).to have_css('.environments-container')
@@ -87,7 +87,7 @@ describe 'Environments page', :js do
end
describe 'in available tab page' do
- it 'should show no environments' do
+ it 'shows no environments' do
visit_environments(project, scope: 'available')
expect(page).to have_css('.environments-container')
@@ -96,7 +96,7 @@ describe 'Environments page', :js do
end
describe 'in stopped tab page' do
- it 'should show one environment' do
+ it 'shows one environment' do
visit_environments(project, scope: 'stopped')
expect(page).to have_css('.environments-container')
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index 03cb3530e2b..111972a6b00 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -30,7 +30,7 @@ describe 'Projects > Files > Download buttons in files tree' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index b2d2dba55f1..7432c600c1e 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -159,7 +159,7 @@ describe 'Project > Members > Invite group', :js do
open_select2 '#link_group_id'
end
- it 'should infinitely scroll' do
+ it 'infinitely scrolls' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 75c72a68069..b54ea929978 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -252,4 +252,23 @@ describe 'New project' do
end
end
end
+
+ context 'Namespace selector' do
+ context 'with group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+
+ before do
+ group.add_developer(user)
+ visit new_project_path(namespace_id: group.id)
+ end
+
+ it 'selects the group namespace' do
+ page.within('#blank-project-pane') do
+ namespace = find('#project_namespace_id option[selected]')
+
+ expect(namespace.text).to eq group.full_path
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index ee6b67b2188..b1a705f09ce 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -93,14 +93,14 @@ describe 'Pipeline Schedules', :js do
expect(page).to have_button('UTC')
end
- it 'it creates a new scheduled pipeline' do
+ it 'creates a new scheduled pipeline' do
fill_in_schedule_form
save_pipeline_schedule
expect(page).to have_content('my fancy description')
end
- it 'it prevents an invalid form from being submitted' do
+ it 'prevents an invalid form from being submitted' do
save_pipeline_schedule
expect(page).to have_content('This field is required')
@@ -112,7 +112,7 @@ describe 'Pipeline Schedules', :js do
edit_pipeline_schedule
end
- it 'it displays existing properties' do
+ it 'displays existing properties' do
description = find_field('schedule_description').value
expect(description).to eq('pipeline schedule')
expect(page).to have_button('master')
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index b197557039d..cf334e1e4da 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -154,7 +154,7 @@ describe 'Pipeline', :js do
end
end
- it 'should be possible to retry the success job' do
+ it 'is possible to retry the success job' do
find('#ci-badge-build .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
@@ -194,13 +194,13 @@ describe 'Pipeline', :js do
end
end
- it 'should be possible to retry the failed build' do
+ it 'is possible to retry the failed build' do
find('#ci-badge-test .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
- it 'should include the failure reason' do
+ it 'includes the failure reason' do
page.within('#ci-badge-test') do
build_link = page.find('.js-pipeline-graph-job-link')
expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)')
@@ -220,7 +220,7 @@ describe 'Pipeline', :js do
end
end
- it 'should be possible to play the manual job' do
+ it 'is possible to play the manual job' do
find('#ci-badge-manual-build .ci-action-icon-container').click
expect(page).not_to have_content('Play job')
@@ -454,7 +454,7 @@ describe 'Pipeline', :js do
expect(page).to have_content('Cancel running')
end
- it 'should not link to job' do
+ it 'does not link to job' do
expect(page).not_to have_selector('.js-pipeline-graph-job-link')
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 7ca3b3d8edd..de780f13681 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -542,19 +542,19 @@ describe 'Pipelines', :js do
visit_project_pipelines
end
- it 'should render a mini pipeline graph' do
+ it 'renders a mini pipeline graph' do
expect(page).to have_selector('.js-mini-pipeline-graph')
expect(page).to have_selector('.js-builds-dropdown-button')
end
context 'when clicking a stage badge' do
- it 'should open a dropdown' do
+ it 'opens a dropdown' do
find('.js-builds-dropdown-button').click
expect(page).to have_link build.name
end
- it 'should be possible to cancel pending build' do
+ it 'is possible to cancel pending build' do
find('.js-builds-dropdown-button').click
find('.js-ci-action').click
wait_for_requests
@@ -570,7 +570,7 @@ describe 'Pipelines', :js do
name: 'build')
end
- it 'should display the failure reason' do
+ it 'displays the failure reason' do
find('.js-builds-dropdown-button').click
within('.js-builds-dropdown-list') do
@@ -587,21 +587,21 @@ describe 'Pipelines', :js do
create(:ci_empty_pipeline, project: project)
end
- it 'should render pagination' do
+ it 'renders pagination' do
visit project_pipelines_path(project)
wait_for_requests
expect(page).to have_selector('.gl-pagination')
end
- it 'should render second page of pipelines' do
+ it 'renders second page of pipelines' do
visit project_pipelines_path(project, page: '2')
wait_for_requests
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- it 'should show updated content' do
+ it 'shows updated content' do
visit project_pipelines_path(project)
wait_for_requests
page.find('.js-next-button .page-link').click
@@ -685,7 +685,7 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run Pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
@@ -698,7 +698,7 @@ describe 'Pipelines', :js do
fill_in "Input variable value", with: "value"
end
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run Pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last.variables.map { |var| var.slice(:key, :secret_value) })
@@ -709,7 +709,7 @@ describe 'Pipelines', :js do
context 'without gitlab-ci.yml' do
before do
- click_on 'Create pipeline'
+ click_on 'Run Pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
@@ -722,14 +722,14 @@ describe 'Pipelines', :js do
click_link 'master'
end
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run Pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
end
end
- describe 'Create pipelines' do
+ describe 'Run Pipelines' do
let(:project) { create(:project, :repository) }
before do
@@ -740,7 +740,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
- expect(page).to have_content('Create for')
+ expect(page).to have_content('Run for')
end
end
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index aa71669de98..e14934b1672 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -50,7 +50,7 @@ describe 'Functions', :js do
end
it 'sees an empty listing of serverless functions' do
- expect(page).to have_selector('.gl-responsive-table-row')
+ expect(page).to have_selector('.empty-state')
end
end
end
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index 3a2dcc5aa55..fee5f8001b0 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -35,11 +35,10 @@ describe 'Projects > Show > Download buttons' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
it 'download links have download attribute' do
- expect(page).to have_selector('a', text: 'Download')
page.all('a', text: 'Download').each do |link|
expect(link[:download]).to eq ''
end
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
index 9c1ef78b0ca..4e1e2f330ec 100644
--- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -23,14 +23,14 @@ describe 'Projects > Snippets > User comments on a snippet', :js do
expect(page).to have_content('Good snippet!')
end
- it 'should have autocomplete' do
+ it 'has autocomplete' do
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
expect(page).to have_selector('.atwho-view')
end
- it 'should have zen mode' do
+ it 'has zen mode' do
find('.js-zen-enter').click
expect(page).to have_selector('.fullscreen')
end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index fbfd8cee7aa..4c8ec53836a 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -36,7 +36,7 @@ describe 'Download buttons in tags page' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{tag}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 8d7e2883b2a..c0932539131 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -54,4 +54,31 @@ describe 'User creates a project', :js do
expect(project.namespace).to eq(subgroup)
end
end
+
+ context 'in a group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'creates a new project' do
+ visit(new_project_path)
+
+ fill_in :project_name, with: 'a-new-project'
+ fill_in :project_path, with: 'a-new-project'
+
+ page.find('.js-select-namespace').click
+ page.find("div[role='option']", text: group.full_path).click
+
+ page.within('#content-body') do
+ click_button('Create project')
+ end
+
+ expect(page).to have_content("Project 'a-new-project' was successfully created")
+
+ project = Project.find_by(name: 'a-new-project')
+ expect(project.namespace).to eq(group)
+ end
+ end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_pages_spec.rb b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
new file mode 100644
index 00000000000..5c16d7783f0
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User views wiki pages' do
+ include WikiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+
+ let!(:wiki_page1) do
+ create(:wiki_page, wiki: project.wiki, attrs: { title: '3 home', content: '3' })
+ end
+ let!(:wiki_page2) do
+ create(:wiki_page, wiki: project.wiki, attrs: { title: '1 home', content: '1' })
+ end
+ let!(:wiki_page3) do
+ create(:wiki_page, wiki: project.wiki, attrs: { title: '2 home', content: '2' })
+ end
+
+ let(:pages) do
+ page.find('.wiki-pages-list').all('li').map { |li| li.find('a') }
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit(project_wikis_pages_path(project))
+ end
+
+ context 'ordered by title' do
+ let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] }
+
+ context 'asc' do
+ it 'pages are displayed in direct order' do
+ pages.each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_title[index].title)
+ end
+ end
+ end
+
+ context 'desc' do
+ before do
+ page.within('.wiki-sort-dropdown') do
+ page.find('.qa-reverse-sort').click
+ end
+ end
+
+ it 'pages are displayed in reversed order' do
+ pages.reverse_each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_title[index].title)
+ end
+ end
+ end
+ end
+
+ context 'ordered by created_at' do
+ let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] }
+
+ before do
+ page.within('.wiki-sort-dropdown') do
+ click_button('Title')
+ click_link('Created date')
+ end
+ end
+
+ context 'asc' do
+ it 'pages are displayed in direct order' do
+ pages.each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
+ end
+ end
+ end
+
+ context 'desc' do
+ before do
+ page.within('.wiki-sort-dropdown') do
+ page.find('.qa-reverse-sort').click
+ end
+ end
+
+ it 'pages are displayed in reversed order' do
+ pages.reverse_each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index b0923b451ee..9a049764dec 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -3,13 +3,13 @@ require 'spec_helper'
describe 'RavenJS' do
let(:raven_path) { '/raven.chunk.js' }
- it 'should not load raven if sentry is disabled' do
+ it 'does not load raven if sentry is disabled' do
visit new_user_session_path
expect(has_requested_raven).to eq(false)
end
- it 'should load raven if sentry is enabled' do
+ it 'loads raven if sentry is enabled' do
stub_application_setting(clientside_sentry_dsn: 'https://key@domain.com/id', clientside_sentry_enabled: true)
visit new_user_session_path
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index fc6726985ae..78e0a43ce6d 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -83,7 +83,7 @@ describe 'Comments on personal snippets', :js do
expect(find('div#notes')).to have_content('This is awesome!')
end
- it 'should not have autocomplete' do
+ it 'does not have autocomplete' do
wait_for_requests
find('#note_note').native.send_keys('')
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index 3b48ea4786d..d2cdade88d1 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -17,12 +17,14 @@ describe 'Merge request > User sees revert modal', :js do
end
it 'shows the revert modal' do
- expect(page).to have_content('Revert this merge request')
+ page.within('.modal-header') do
+ expect(page).to have_content 'Revert this merge request'
+ end
end
it 'closes the revert modal with escape keypress' do
find('#modal-revert-commit').send_keys(:escape)
- expect(page).not_to have_content('Revert this merge request')
+ expect(page).not_to have_selector('#modal-revert-commit', visible: true)
end
end
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 3db9ae7a951..bfa85696e19 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -93,7 +93,7 @@ describe 'Overview tab on a user profile', :js do
describe 'user has no personal projects' do
include_context 'visit overview tab'
- it 'it shows an empty project list with an info message' do
+ it 'shows an empty project list with an info message' do
page.within('.projects-block') do
expect(page).to have_selector('.loading', visible: false)
expect(page).to have_content('You haven\'t created any personal projects.')
@@ -113,7 +113,7 @@ describe 'Overview tab on a user profile', :js do
include_context 'visit overview tab'
- it 'it shows one entry in the list of projects' do
+ it 'shows one entry in the list of projects' do
page.within('.projects-block') do
expect(page).to have_selector('.project-row', count: 1)
end
@@ -139,7 +139,7 @@ describe 'Overview tab on a user profile', :js do
include_context 'visit overview tab'
- it 'it shows max. ten entries in the list of projects' do
+ it 'shows max. ten entries in the list of projects' do
page.within('.projects-block') do
expect(page).to have_selector('.project-row', count: 10)
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 00b6cad1a66..fe53fabe54c 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -719,7 +719,7 @@ describe IssuesFinder do
end
end
- describe '#use_subquery_for_search?' do
+ describe '#use_cte_for_search?' do
let(:finder) { described_class.new(nil, params) }
before do
@@ -731,7 +731,7 @@ describe IssuesFinder do
let(:params) { { attempt_group_search_optimizations: true } }
it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
+ expect(finder.use_cte_for_search?).to be_falsey
end
end
@@ -743,15 +743,15 @@ describe IssuesFinder do
end
it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
+ expect(finder.use_cte_for_search?).to be_falsey
end
end
- context 'when the attempt_group_search_optimizations param is falsey' do
+ context 'when the force_cte param is falsey' do
let(:params) { { search: 'foo' } }
it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
+ expect(finder.use_cte_for_search?).to be_falsey
end
end
@@ -763,80 +763,39 @@ describe IssuesFinder do
end
it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
+ expect(finder.use_cte_for_search?).to be_falsey
end
end
- context 'when force_cte? is true' do
- let(:params) { { search: 'foo', attempt_group_search_optimizations: true, force_cte: true } }
-
- it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
- end
- end
-
- context 'when all conditions are met' do
- let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
-
- it 'returns true' do
- expect(finder.use_subquery_for_search?).to be_truthy
- end
- end
- end
+ context 'when attempt_group_search_optimizations is unset and attempt_project_search_optimizations is set' do
+ let(:params) { { search: 'foo', attempt_project_search_optimizations: true } }
- describe '#use_cte_for_count?' do
- let(:finder) { described_class.new(nil, params) }
-
- before do
- allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
- stub_feature_flags(attempt_group_search_optimizations: true)
- end
-
- context 'when there is no search param' do
- let(:params) { { attempt_group_search_optimizations: true, force_cte: true } }
-
- it 'returns false' do
- expect(finder.use_cte_for_count?).to be_falsey
- end
- end
-
- context 'when the database is not Postgres' do
- let(:params) { { search: 'foo', force_cte: true, attempt_group_search_optimizations: true } }
-
- before do
- allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
- end
-
- it 'returns false' do
- expect(finder.use_cte_for_count?).to be_falsey
- end
- end
-
- context 'when the force_cte param is falsey' do
- let(:params) { { search: 'foo' } }
+ context 'and the corresponding feature flag is disabled' do
+ before do
+ stub_feature_flags(attempt_project_search_optimizations: false)
+ end
- it 'returns false' do
- expect(finder.use_cte_for_count?).to be_falsey
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
end
- end
- context 'when the attempt_group_search_optimizations flag is disabled' do
- let(:params) { { search: 'foo', force_cte: true, attempt_group_search_optimizations: true } }
-
- before do
- stub_feature_flags(attempt_group_search_optimizations: false)
- end
+ context 'and the corresponding feature flag is enabled' do
+ before do
+ stub_feature_flags(attempt_project_search_optimizations: true)
+ end
- it 'returns false' do
- expect(finder.use_cte_for_count?).to be_falsey
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ end
end
end
context 'when all conditions are met' do
- let(:params) { { search: 'foo', force_cte: true, attempt_group_search_optimizations: true } }
+ let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
it 'returns true' do
- expect(finder.use_cte_for_count?).to be_truthy
+ expect(finder.use_cte_for_search?).to be_truthy
end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 56136eb84bc..f508b9bdb6f 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -83,6 +83,14 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request2)
end
+ it 'filters by source project id' do
+ params = { source_project_id: merge_request2.source_project_id }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3)
+ end
+
it 'filters by state' do
params = { state: 'locked' }
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index ecffbb9e197..34c7b508c56 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -9,7 +9,7 @@ describe MilestonesFinder do
let!(:milestone_3) { create(:milestone, project: project_1, state: 'active', due_date: Date.tomorrow) }
let!(:milestone_4) { create(:milestone, project: project_2, state: 'active') }
- it 'it returns milestones for projects' do
+ it 'returns milestones for projects' do
result = described_class.new(project_ids: [project_1.id, project_2.id], state: 'all').execute
expect(result).to contain_exactly(milestone_3, milestone_4)
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 35279906854..3ad38207da4 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Projects::Serverless::FunctionsFinder do
include KubernetesHelpers
+ include PrometheusHelpers
include ReactiveCachingHelpers
let(:user) { create(:user) }
@@ -24,12 +25,12 @@ describe Projects::Serverless::FunctionsFinder do
describe 'retrieve data from knative' do
it 'does not have knative installed' do
- expect(described_class.new(project.clusters).execute).to be_empty
+ expect(described_class.new(project).execute).to be_empty
end
context 'has knative installed' do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let(:finder) { described_class.new(project.clusters) }
+ let(:finder) { described_class.new(project) }
it 'there are no functions' do
expect(finder.execute).to be_empty
@@ -58,13 +59,36 @@ describe Projects::Serverless::FunctionsFinder do
expect(result).not_to be_empty
expect(result["metadata"]["name"]).to be_eql(cluster.project.name)
end
+
+ it 'has metrics', :use_clean_rails_memory_store_caching do
+ end
+ end
+
+ context 'has prometheus' do
+ let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
+ let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:finder) { described_class.new(project) }
+
+ before do
+ allow(finder).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix'))
+ end
+
+ it 'is available' do
+ expect(finder.has_prometheus?("*")).to be true
+ end
+
+ it 'has query data' do
+ expect(finder.invocation_metrics("*", cluster.project.name)).not_to be_nil
+ end
end
end
describe 'verify if knative is installed' do
context 'knative is not installed' do
it 'does not have knative installed' do
- expect(described_class.new(project.clusters).installed?).to be false
+ expect(described_class.new(project).installed?).to be false
end
end
@@ -72,7 +96,7 @@ describe Projects::Serverless::FunctionsFinder do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
it 'does have knative installed' do
- expect(described_class.new(project.clusters).installed?).to be true
+ expect(described_class.new(project).installed?).to be true
end
end
end
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
index 7e9e048a9fd..214b67a9a0f 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
@@ -51,6 +51,5 @@
"toggle_subscription_path": { "type": "string" },
"move_issue_path": { "type": "string" },
"projects_autocomplete_path": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
index cd50be00418..918f2c4b47d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -119,6 +119,12 @@
"merge_status", "sha", "merge_commit_sha", "user_notes_count",
"should_remove_source_branch", "force_remove_source_branch",
"web_url", "squash"
- ]
+ ],
+ "head_pipeline": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "pipeline/detail.json" }
+ ]
+ }
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
index 56f86856dd4..a7207d2d991 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
@@ -13,6 +13,5 @@
"ref": { "type": "string" },
"status": { "type": "string" },
"web_url": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json
new file mode 100644
index 00000000000..63e130d4055
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json
@@ -0,0 +1,32 @@
+{
+ "type": "object",
+ "allOf": [
+ { "$ref": "basic.json" },
+ {
+ "properties": {
+ "before_sha": { "type": ["string", "null"] },
+ "tag": { "type": ["boolean"] },
+ "yaml_errors": { "type": ["string", "null"] },
+ "user": {
+ "anyOf": [
+ { "type": ["object", "null"] },
+ { "$ref": "../user/basic.json" }
+ ]
+ },
+ "created_at": { "type": ["date", "null"] },
+ "updated_at": { "type": ["date", "null"] },
+ "started_at": { "type": ["date", "null"] },
+ "finished_at": { "type": ["date", "null"] },
+ "committed_at": { "type": ["date", "null"] },
+ "duration": { "type": ["number", "null"] },
+ "coverage": { "type": ["string", "null"] },
+ "detailed_status": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "../../../status/ci_detailed_status.json" }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json
index 6f6b044115b..305071a6b3f 100644
--- a/spec/fixtures/api/schemas/variable.json
+++ b/spec/fixtures/api/schemas/variable.json
@@ -4,12 +4,14 @@
"id",
"key",
"value",
+ "masked",
"protected"
],
"properties": {
"id": { "type": "integer" },
"key": { "type": "string" },
"value": { "type": "string" },
+ "masked": { "type": "boolean" },
"protected": { "type": "boolean" },
"environment_scope": { "type": "string", "optional": true }
},
diff --git a/spec/fixtures/blockquote_fence_after.md b/spec/fixtures/blockquote_fence_after.md
index 2652a842c0e..555905bf07e 100644
--- a/spec/fixtures/blockquote_fence_after.md
+++ b/spec/fixtures/blockquote_fence_after.md
@@ -18,10 +18,13 @@ Double `>>>` inside code block:
Blockquote outside code block:
+
> Quote
+
Code block inside blockquote:
+
> Quote
>
> ```
@@ -30,8 +33,10 @@ Code block inside blockquote:
>
> Quote
+
Single `>>>` inside code block inside blockquote:
+
> Quote
>
> ```
@@ -42,8 +47,10 @@ Single `>>>` inside code block inside blockquote:
>
> Quote
+
Double `>>>` inside code block inside blockquote:
+
> Quote
>
> ```
@@ -56,6 +63,7 @@ Double `>>>` inside code block inside blockquote:
>
> Quote
+
Single `>>>` inside HTML:
<pre>
@@ -76,10 +84,13 @@ Double `>>>` inside HTML:
Blockquote outside HTML:
+
> Quote
+
HTML inside blockquote:
+
> Quote
>
> <pre>
@@ -88,8 +99,10 @@ HTML inside blockquote:
>
> Quote
+
Single `>>>` inside HTML inside blockquote:
+
> Quote
>
> <pre>
@@ -100,8 +113,10 @@ Single `>>>` inside HTML inside blockquote:
>
> Quote
+
Double `>>>` inside HTML inside blockquote:
+
> Quote
>
> <pre>
@@ -113,3 +128,4 @@ Double `>>>` inside HTML inside blockquote:
> </pre>
>
> Quote
+
diff --git a/spec/fixtures/valid.po b/spec/fixtures/valid.po
index dbe2f952bad..155b6cbb95d 100644
--- a/spec/fixtures/valid.po
+++ b/spec/fixtures/valid.po
@@ -35,9 +35,6 @@ msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
msgstr[1] "%d pipelines"
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Una colección de gráficos sobre Integración Continua"
-
msgid "About auto deploy"
msgstr "Acerca del auto despliegue"
diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js
new file mode 100644
index 00000000000..2e8bff298c4
--- /dev/null
+++ b/spec/frontend/helpers/monitor_helper_spec.js
@@ -0,0 +1,45 @@
+import * as monitorHelper from '~/helpers/monitor_helper';
+
+describe('monitor helper', () => {
+ const defaultConfig = { default: true, name: 'default name' };
+ const name = 'data name';
+ const series = [[1, 1], [2, 2], [3, 3]];
+ const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }];
+
+ describe('makeDataSeries', () => {
+ const expectedDataSeries = [
+ {
+ ...defaultConfig,
+ data: series,
+ },
+ ];
+
+ it('converts query results to data series', () => {
+ expect(monitorHelper.makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(
+ expectedDataSeries,
+ );
+ });
+
+ it('returns an empty array if no query results exist', () => {
+ expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]);
+ });
+
+ it('handles multi-series query results', () => {
+ const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' };
+
+ expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
+ expectedData,
+ expectedData,
+ ]);
+ });
+
+ it('excludes NaN values', () => {
+ expect(
+ monitorHelper.makeDataSeries(
+ data({ metric: {}, values: [[1, 1], [2, NaN]] }),
+ defaultConfig,
+ ),
+ ).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]);
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 5de7a281d34..40d47aaad03 100644
--- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -18,7 +18,7 @@ describe('IDE commit module mutations', () => {
describe('UPDATE_COMMIT_ACTION', () => {
it('updates commitAction', () => {
- mutations.UPDATE_COMMIT_ACTION(state, 'testing');
+ mutations.UPDATE_COMMIT_ACTION(state, { commitAction: 'testing' });
expect(state.commitAction).toBe('testing');
});
@@ -39,4 +39,20 @@ describe('IDE commit module mutations', () => {
expect(state.submitCommitLoading).toBeTruthy();
});
});
+
+ describe('TOGGLE_SHOULD_CREATE_MR', () => {
+ it('changes shouldCreateMR to true when initial state is false', () => {
+ state.shouldCreateMR = false;
+ mutations.TOGGLE_SHOULD_CREATE_MR(state);
+
+ expect(state.shouldCreateMR).toBe(true);
+ });
+
+ it('changes shouldCreateMR to false when initial state is true', () => {
+ state.shouldCreateMR = true;
+ mutations.TOGGLE_SHOULD_CREATE_MR(state);
+
+ expect(state.shouldCreateMR).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js
index acfdc885032..d54e0eab845 100644
--- a/spec/frontend/labels_select_spec.js
+++ b/spec/frontend/labels_select_spec.js
@@ -13,40 +13,104 @@ const mockLabels = [
},
];
+const mockScopedLabels = [
+ {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#333ABC',
+ text_color: '#FFFFFF',
+ },
+];
+
describe('LabelsSelect', () => {
describe('getLabelTemplate', () => {
- const label = mockLabels[0];
- let $labelEl;
-
- beforeEach(() => {
- $labelEl = $(
- LabelsSelect.getLabelTemplate({
- labels: mockLabels,
- issueUpdateURL: mockUrl,
- }),
- );
- });
+ describe('when normal label is present', () => {
+ const label = mockLabels[0];
+ let $labelEl;
- it('generated label item template has correct label URL', () => {
- expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
- });
+ beforeEach(() => {
+ $labelEl = $(
+ LabelsSelect.getLabelTemplate({
+ labels: mockLabels,
+ issueUpdateURL: mockUrl,
+ enableScopedLabels: true,
+ scopedLabelsDocumentationLink: 'docs-link',
+ }),
+ );
+ });
- it('generated label item template has correct label title', () => {
- expect($labelEl.find('span.label').text()).toBe(label.title);
- });
+ it('generated label item template has correct label URL', () => {
+ expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
+ });
- it('generated label item template has label description as title attribute', () => {
- expect($labelEl.find('span.label').attr('title')).toBe(label.description);
- });
+ it('generated label item template has correct label title', () => {
+ expect($labelEl.find('span.label').text()).toBe(label.title);
+ });
+
+ it('generated label item template has label description as title attribute', () => {
+ expect($labelEl.find('span.label').attr('title')).toBe(label.description);
+ });
- it('generated label item template has correct label styles', () => {
- expect($labelEl.find('span.label').attr('style')).toBe(
- `background-color: ${label.color}; color: ${label.text_color};`,
- );
+ it('generated label item template has correct label styles', () => {
+ expect($labelEl.find('span.label').attr('style')).toBe(
+ `background-color: ${label.color}; color: ${label.text_color};`,
+ );
+ });
+
+ it('generated label item has a badge class', () => {
+ expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ });
+
+ it('generated label item template does not have scoped-label class', () => {
+ expect($labelEl.find('.scoped-label')).toHaveLength(0);
+ });
});
- it('generated label item has a badge class', () => {
- expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ describe('when scoped label is present', () => {
+ const label = mockScopedLabels[0];
+ let $labelEl;
+
+ beforeEach(() => {
+ $labelEl = $(
+ LabelsSelect.getLabelTemplate({
+ labels: mockScopedLabels,
+ issueUpdateURL: mockUrl,
+ enableScopedLabels: true,
+ scopedLabelsDocumentationLink: 'docs-link',
+ }),
+ );
+ });
+
+ it('generated label item template has correct label URL', () => {
+ expect($labelEl.find('a').attr('href')).toBe('/foo/bar?label_name[]=Foo%3A%3ABar');
+ });
+
+ it('generated label item template has correct label title', () => {
+ expect($labelEl.find('span.label').text()).toBe(label.title);
+ });
+
+ it('generated label item template has html flag as true', () => {
+ expect($labelEl.find('span.label').attr('data-html')).toBe('true');
+ });
+
+ it('generated label item template has question icon', () => {
+ expect($labelEl.find('i.fa-question-circle')).toHaveLength(1);
+ });
+
+ it('generated label item template has scoped-label class', () => {
+ expect($labelEl.find('.scoped-label')).toHaveLength(1);
+ });
+
+ it('generated label item template has correct label styles', () => {
+ expect($labelEl.find('span.label').attr('style')).toBe(
+ `background-color: ${label.color}; color: ${label.text_color};`,
+ );
+ });
+
+ it('generated label item has a badge class', () => {
+ expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ });
});
});
});
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 0a266b19ea5..3f331055a32 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -151,4 +151,31 @@ describe('text_utility', () => {
);
});
});
+
+ describe('truncateNamespace', () => {
+ it(`should return the root namespace if the namespace only includes one level`, () => {
+ expect(textUtils.truncateNamespace('a / b')).toBe('a');
+ });
+
+ it(`should return the first 2 namespaces if the namespace inlcudes exactly 2 levels`, () => {
+ expect(textUtils.truncateNamespace('a / b / c')).toBe('a / b');
+ });
+
+ it(`should return the first and last namespaces, separated by "...", if the namespace inlcudes more than 2 levels`, () => {
+ expect(textUtils.truncateNamespace('a / b / c / d')).toBe('a / ... / c');
+ expect(textUtils.truncateNamespace('a / b / c / d / e / f / g / h / i')).toBe('a / ... / h');
+ });
+
+ it(`should return an empty string for invalid inputs`, () => {
+ [undefined, null, 4, {}, true, new Date()].forEach(input => {
+ expect(textUtils.truncateNamespace(input)).toBe('');
+ });
+ });
+
+ it(`should not alter strings that aren't formatted as namespaces`, () => {
+ ['', ' ', '\t', 'a', 'a \\ b'].forEach(input => {
+ expect(textUtils.truncateNamespace(input)).toBe(input);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap
new file mode 100644
index 00000000000..add0c36a120
--- /dev/null
+++ b/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Resizable Chart Container renders the component 1`] = `
+<div>
+ <div
+ class="slot"
+ >
+ <span
+ class="width"
+ >
+ 0
+ </span>
+
+ <span
+ class="height"
+ >
+ 0
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
new file mode 100644
index 00000000000..866d6eb05c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
@@ -0,0 +1,98 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import SuggestionDiffRow from '~/vue_shared/components/markdown/suggestion_diff_row.vue';
+
+const oldLine = {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-oldtext',
+ text: '-oldtext',
+ type: 'old',
+};
+
+const newLine = {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: 6,
+ old_line: null,
+ rich_text: '-newtext',
+ text: '-newtext',
+ type: 'new',
+};
+
+describe(SuggestionDiffRow.name, () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(SuggestionDiffRow, {
+ localVue,
+ ...options,
+ });
+ };
+
+ const findOldLineWrapper = () => wrapper.find('.old_line');
+ const findNewLineWrapper = () => wrapper.find('.new_line');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correctly', () => {
+ factory({
+ propsData: {
+ line: oldLine,
+ },
+ });
+
+ expect(wrapper.is('.line_holder')).toBe(true);
+ });
+
+ describe('when passed line has type old', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ line: oldLine,
+ },
+ });
+ });
+
+ it('has old class when line has type old', () => {
+ expect(wrapper.find('td').classes()).toContain('old');
+ });
+
+ it('has old line number rendered', () => {
+ expect(findOldLineWrapper().text()).toBe('5');
+ });
+
+ it('has no new line number rendered', () => {
+ expect(findNewLineWrapper().text()).toBe('');
+ });
+ });
+
+ describe('when passed line has type new', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ line: newLine,
+ },
+ });
+ });
+
+ it('has new class when line has type new', () => {
+ expect(wrapper.find('td').classes()).toContain('new');
+ });
+
+ it('has no old line number rendered', () => {
+ expect(findOldLineWrapper().text()).toBe('');
+ });
+
+ it('has no new line number rendered', () => {
+ expect(findNewLineWrapper().text()).toBe('6');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
new file mode 100644
index 00000000000..8f533e8ab24
--- /dev/null
+++ b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
+import $ from 'jquery';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ debounceByAnimationFrame(callback) {
+ return jest.spyOn({ callback }, 'callback');
+ },
+}));
+
+describe('Resizable Chart Container', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(ResizableChartContainer, {
+ attachToDocument: true,
+ scopedSlots: {
+ default: `
+ <div class="slot" slot-scope="{ width, height }">
+ <span class="width">{{width}}</span>
+ <span class="height">{{height}}</span>
+ </div>
+ `,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the component', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('updates the slot width and height props', () => {
+ const width = 1920;
+ const height = 1080;
+
+ // JSDOM mocks and sets clientWidth/clientHeight to 0 so we set manually
+ wrapper.vm.$refs.chartWrapper = { clientWidth: width, clientHeight: height };
+
+ $(document).trigger('content.resize');
+
+ return Vue.nextTick().then(() => {
+ const widthNode = wrapper.find('.slot > .width');
+ const heightNode = wrapper.find('.slot > .height');
+
+ expect(parseInt(widthNode.text(), 10)).toEqual(width);
+ expect(parseInt(heightNode.text(), 10)).toEqual(height);
+ });
+ });
+
+ it('calls onResize on manual resize', () => {
+ $(document).trigger('content.resize');
+ expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
+ });
+
+ it('calls onResize on page resize', () => {
+ window.dispatchEvent(new Event('resize'));
+ expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
+ });
+});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index a229d29afa6..00e31568a9e 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -5,61 +5,245 @@ require 'spec_helper'
describe 'Gitlab::Graphql::Authorization' do
set(:user) { create(:user) }
+ let(:permission_single) { :foo }
+ let(:permission_collection) { [:foo, :bar] }
let(:test_object) { double(name: 'My name') }
- let(:object_type) { object_type_class }
- let(:query_type) { query_type_class(object_type, test_object) }
- let(:schema) { schema_class(query_type) }
+ let(:query_string) { '{ object() { name } }' }
+ let(:result) { execute_query(query_type)['data'] }
- let(:execute) do
- schema.execute(
- query_string,
- context: { current_user: user },
- variables: {}
- )
+ subject { result['object'] }
+
+ shared_examples 'authorization with a single permission' do
+ it 'returns the protected field when user has permission' do
+ permit(permission_single)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user is not authorized' do
+ expect(subject).to be_nil
+ end
end
- let(:result) { execute['data'] }
+ shared_examples 'authorization with a collection of permissions' do
+ it 'returns the protected field when user has all permissions' do
+ permit(*permission_collection)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user only has one of the permissions' do
+ permit(permission_collection.first)
+
+ expect(subject).to be_nil
+ end
+
+ it 'returns nil when user only has none of the permissions' do
+ expect(subject).to be_nil
+ end
+ end
before do
# By default, disallow all permissions.
allow(Ability).to receive(:allowed?).and_return(false)
end
- describe 'authorizing with a single permission' do
- let(:query_string) { '{ singlePermission() { name } }' }
+ describe 'Field authorizations' do
+ let(:type) { type_factory }
+
+ describe 'with a single permission' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_single
+ end
+ end
+
+ include_examples 'authorization with a single permission'
+ end
+
+ describe 'with a collection of permissions' do
+ let(:query_type) do
+ permissions = permission_collection
+ query_factory do |qt|
+ qt.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object } do
+ authorize permissions
+ end
+ end
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+ end
+
+ describe 'Field authorizations when field is a built in type' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
+ end
+ end
- subject { result['singlePermission'] }
+ describe 'with a single permission' do
+ let(:type) do
+ type_factory do |type|
+ type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single
+ end
+ end
+
+ it 'returns the protected field when user has permission' do
+ permit(permission_single)
- it 'should return the protected field when user has permission' do
- permit(:foo)
+ expect(subject).to eq('name' => test_object.name)
+ end
- expect(subject['name']).to eq(test_object.name)
+ it 'returns nil when user is not authorized' do
+ expect(subject).to eq('name' => nil)
+ end
end
- it 'should return nil when user is not authorized' do
- expect(subject).to be_nil
+ describe 'with a collection of permissions' do
+ let(:type) do
+ permissions = permission_collection
+ type_factory do |type|
+ type.field :name, GraphQL::STRING_TYPE, null: true do
+ authorize permissions
+ end
+ end
+ end
+
+ it 'returns the protected field when user has all permissions' do
+ permit(*permission_collection)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user only has one of the permissions' do
+ permit(permission_collection.first)
+
+ expect(subject).to eq('name' => nil)
+ end
+
+ it 'returns nil when user only has none of the permissions' do
+ expect(subject).to eq('name' => nil)
+ end
+ end
+ end
+
+ describe 'Type authorizations' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
+ end
+ end
+
+ describe 'with a single permission' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
+
+ include_examples 'authorization with a single permission'
+ end
+
+ describe 'with a collection of permissions' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_collection
+ end
+ end
+
+ include_examples 'authorization with a collection of permissions'
end
end
- describe 'authorizing with an Array of permissions' do
- let(:query_string) { '{ permissionCollection() { name } }' }
+ describe 'type and field authorizations together' do
+ let(:permission_1) { permission_collection.first }
+ let(:permission_2) { permission_collection.last }
+
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_1
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_2
+ end
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
- subject { result['permissionCollection'] }
+ describe 'type authorizations when applied to a relay connection' do
+ let(:query_string) { '{ object() { edges { node { name } } } }' }
- it 'should return the protected field when user has all permissions' do
- permit(:foo, :bar)
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
- expect(subject['name']).to eq(test_object.name)
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object] }
+ end
end
- it 'should return nil when user only has one of the permissions' do
- permit(:foo)
+ subject { result.dig('object', 'edges') }
- expect(subject).to be_nil
+ it 'returns the protected field when user has permission' do
+ permit(permission_single)
+
+ expect(subject).not_to be_empty
+ expect(subject.first['node']).to eq('name' => test_object.name)
end
- it 'should return nil when user only has none of the permissions' do
- expect(subject).to be_nil
+ it 'returns nil when user is not authorized' do
+ expect(subject).to be_empty
+ end
+ end
+
+ describe 'type authorizations when applied to a basic connection' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, [type], null: true, resolve: ->(obj, args, ctx) { [test_object] }
+ end
+ end
+
+ subject { result['object'].first }
+
+ include_examples 'authorization with a single permission'
+ end
+
+ describe 'when connections do not follow the correct specification' do
+ let(:query_string) { '{ object() { edges { node { name }} } }' }
+
+ let(:type) do
+ bad_node = type_factory do |type|
+ type.graphql_name 'BadNode'
+ type.field :bad_node, GraphQL::STRING_TYPE, null: true
+ end
+
+ type_factory do |type|
+ type.field :edges, [bad_node], null: true
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true
+ end
+ end
+
+ it 'throws an error' do
+ expect { result }.to raise_error(Gitlab::Graphql::Errors::ConnectionDefinitionError)
end
end
@@ -71,36 +255,34 @@ describe 'Gitlab::Graphql::Authorization' do
end
end
- def object_type_class
+ def type_factory
Class.new(Types::BaseObject) do
- graphql_name 'TestObject'
+ graphql_name 'TestType'
field :name, GraphQL::STRING_TYPE, null: true
+
+ yield(self) if block_given?
end
end
- def query_type_class(type, object)
+ def query_factory
Class.new(Types::BaseObject) do
graphql_name 'TestQuery'
- field :single_permission, type,
- null: true,
- authorize: :foo,
- resolve: ->(obj, args, ctx) { object }
-
- field :permission_collection, type,
- null: true,
- resolve: ->(obj, args, ctx) { object } do
- authorize [:foo, :bar]
- end
+ yield(self) if block_given?
end
end
- def schema_class(query)
- Class.new(GraphQL::Schema) do
+ def execute_query(query_type)
+ schema = Class.new(GraphQL::Schema) do
use Gitlab::Graphql::Authorize
-
- query(query)
+ query(query_type)
end
+
+ schema.execute(
+ query_string,
+ context: { current_user: user },
+ variables: {}
+ )
end
end
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index b9ddb427e85..74e93b2c4df 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema do
@@ -31,6 +33,46 @@ describe GitlabSchema do
expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection)
end
+ context 'for different types of users' do
+ it 'returns DEFAULT_MAX_COMPLEXITY for no context' do
+ expect(GraphQL::Schema)
+ .to receive(:execute)
+ .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
+
+ described_class.execute('query')
+ end
+
+ it 'returns DEFAULT_MAX_COMPLEXITY for no user' do
+ expect(GraphQL::Schema)
+ .to receive(:execute)
+ .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
+
+ described_class.execute('query', context: {})
+ end
+
+ it 'returns AUTHENTICATED_COMPLEXITY for a logged in user' do
+ user = build :user
+
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::AUTHENTICATED_COMPLEXITY))
+
+ described_class.execute('query', context: { current_user: user })
+ end
+
+ it 'returns ADMIN_COMPLEXITY for an admin user' do
+ user = build :user, :admin
+
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::ADMIN_COMPLEXITY))
+
+ described_class.execute('query', context: { current_user: user })
+ end
+
+ it 'returns what was passed on the query' do
+ expect(GraphQL::Schema).to receive(:execute).with('query', { max_complexity: 1234 })
+
+ described_class.execute('query', max_complexity: 1234)
+ end
+ end
+
def field_instrumenters
described_class.instrumenters[:field]
end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
new file mode 100644
index 00000000000..b5697ee5245
--- /dev/null
+++ b/spec/graphql/types/base_field_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::BaseField do
+ context 'when considering complexity' do
+ it 'defaults to 1' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
+
+ expect(field.to_graphql.complexity).to eq 1
+ end
+
+ it 'has specified value' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
+
+ expect(field.to_graphql.complexity).to eq 12
+ end
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 63a07647a60..dc37b15001f 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -4,4 +4,6 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
it { expect(described_class.graphql_name).to eq('Issue') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_issue) }
end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index c369953e3ea..89c12879074 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -3,14 +3,9 @@ require 'spec_helper'
describe GitlabSchema.types['MergeRequest'] do
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
- describe 'head pipeline' do
- it 'has a head pipeline field' do
- expect(described_class).to have_graphql_field(:head_pipeline)
- end
+ it { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
- it 'authorizes the field' do
- expect(described_class.fields['headPipeline'])
- .to require_graphql_authorizations(:read_pipeline)
- end
+ describe 'nested head pipeline' do
+ it { expect(described_class).to have_graphql_field(:head_pipeline) }
end
end
diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb
new file mode 100644
index 00000000000..f7ee79eae9f
--- /dev/null
+++ b/spec/graphql/types/milestone_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Milestone'] do
+ it { expect(described_class.graphql_name).to eq('Milestone') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_milestone) }
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index e8f1c84f8d6..e0ad09bdf22 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -5,19 +5,11 @@ describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') }
+ it { expect(described_class).to require_graphql_authorizations(:read_project) }
+
describe 'nested merge request' do
it { expect(described_class).to have_graphql_field(:merge_requests) }
it { expect(described_class).to have_graphql_field(:merge_request) }
-
- it 'authorizes the merge request' do
- expect(described_class.fields['mergeRequest'])
- .to require_graphql_authorizations(:read_merge_request)
- end
-
- it 'authorizes the merge requests' do
- expect(described_class.fields['mergeRequests'])
- .to require_graphql_authorizations(:read_merge_request)
- end
end
describe 'nested issues' do
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 07c61ea7647..69e3ea8a4a9 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -15,10 +15,6 @@ describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::ProjectType)
is_expected.to have_graphql_resolver(Resolvers::ProjectResolver)
end
-
- it 'authorizes with read_project' do
- is_expected.to require_graphql_authorizations(:read_project)
- end
end
describe 'metadata field' do
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
new file mode 100644
index 00000000000..8134cc13eb4
--- /dev/null
+++ b/spec/graphql/types/user_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['User'] do
+ it { expect(described_class.graphql_name).to eq('User') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_user) }
+end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 4b40d523287..37e9ddadb8c 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -59,19 +59,19 @@ describe IconsHelper do
describe 'non existing icon' do
non_existing = 'non_existing_icon_sprite'
- it 'should raise in development mode' do
+ it 'raises in development mode' do
allow(Rails.env).to receive(:development?).and_return(true)
expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/)
end
- it 'should raise in test mode' do
+ it 'raises in test mode' do
allow(Rails.env).to receive(:test?).and_return(true)
expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/)
end
- it 'should not raise in production mode' do
+ it 'does not raise in production mode' do
allow(Rails.env).to receive(:test?).and_return(false)
allow(Rails.env).to receive(:development?).and_return(false)
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 012678db9c2..a049b5a6133 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -249,4 +249,24 @@ describe LabelsHelper do
.to match_array([label2, label4, label1, label3])
end
end
+
+ describe 'label_from_hash' do
+ it 'builds a group label with whitelisted attributes' do
+ label = label_from_hash({ title: 'foo', color: 'bar', id: 1, group_id: 1 })
+
+ expect(label).to be_a(GroupLabel)
+ expect(label.id).to be_nil
+ expect(label.title).to eq('foo')
+ expect(label.color).to eq('bar')
+ end
+
+ it 'builds a project label with whitelisted attributes' do
+ label = label_from_hash({ title: 'foo', color: 'bar', id: 1, project_id: 1 })
+
+ expect(label).to be_a(ProjectLabel)
+ expect(label.id).to be_nil
+ expect(label.title).to eq('foo')
+ expect(label.color).to eq('bar')
+ end
+ end
end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 7ccbdcd1332..601f864ef36 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -1,10 +1,38 @@
require 'spec_helper'
-describe NamespacesHelper do
+describe NamespacesHelper, :postgresql do
let!(:admin) { create(:admin) }
- let!(:admin_group) { create(:group, :private) }
+ let!(:admin_project_creation_level) { nil }
+ let!(:admin_group) do
+ create(:group,
+ :private,
+ project_creation_level: admin_project_creation_level)
+ end
let!(:user) { create(:user) }
- let!(:user_group) { create(:group, :private) }
+ let!(:user_project_creation_level) { nil }
+ let!(:user_group) do
+ create(:group,
+ :private,
+ project_creation_level: user_project_creation_level)
+ end
+ let!(:subgroup1) do
+ create(:group,
+ :private,
+ parent: admin_group,
+ project_creation_level: nil)
+ end
+ let!(:subgroup2) do
+ create(:group,
+ :private,
+ parent: admin_group,
+ project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+ let!(:subgroup3) do
+ create(:group,
+ :private,
+ parent: admin_group,
+ project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
before do
admin_group.add_owner(admin)
@@ -105,5 +133,43 @@ describe NamespacesHelper do
helper.namespaces_options
end
end
+
+ describe 'include_groups_with_developer_maintainer_access parameter' do
+ context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set for a project' do
+ let!(:admin_project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
+
+ it 'returns groups where user is a developer' do
+ allow(helper).to receive(:current_user).and_return(user)
+ stub_application_setting(default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ admin_group.add_user(user, GroupMember::DEVELOPER)
+
+ options = helper.namespaces_options_with_developer_maintainer_access
+
+ expect(options).to include(admin_group.name)
+ expect(options).not_to include(subgroup1.name)
+ expect(options).to include(subgroup2.name)
+ expect(options).not_to include(subgroup3.name)
+ expect(options).to include(user_group.name)
+ expect(options).to include(user.name)
+ end
+ end
+
+ context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set globally' do
+ it 'return groups where default is not overridden' do
+ allow(helper).to receive(:current_user).and_return(user)
+ stub_application_setting(default_project_creation: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ admin_group.add_user(user, GroupMember::DEVELOPER)
+
+ options = helper.namespaces_options_with_developer_maintainer_access
+
+ expect(options).to include(admin_group.name)
+ expect(options).to include(subgroup1.name)
+ expect(options).to include(subgroup2.name)
+ expect(options).not_to include(subgroup3.name)
+ expect(options).to include(user_group.name)
+ expect(options).to include(user.name)
+ end
+ end
+ end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 9cff0291250..2f59cfda0a0 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -12,7 +12,7 @@ describe SearchHelper do
allow(self).to receive(:current_user).and_return(nil)
end
- it "it returns nil" do
+ it "returns nil" do
expect(search_autocomplete_opts("q")).to be_nil
end
end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index bfec7ad4bba..e384e2bf9a0 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe VersionCheckHelper do
describe '#version_status_badge' do
- it 'should return nil if not dev environment and not enabled' do
+ it 'returns nil if not dev environment and not enabled' do
allow(Rails.env).to receive(:production?) { false }
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
@@ -16,16 +16,16 @@ describe VersionCheckHelper do
allow(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
end
- it 'should return an image tag' do
+ it 'returns an image tag' do
expect(helper.version_status_badge).to start_with('<img')
end
- it 'should have a js prefixed css class' do
+ it 'has a js prefixed css class' do
expect(helper.version_status_badge)
.to match(/class="js-version-status-badge lazy"/)
end
- it 'should have a VersionCheck url as the src' do
+ it 'has a VersionCheck url as the src' do
expect(helper.version_status_badge)
.to include(%{src="https://version.host.com/check.svg?gitlab_info=xxx"})
end
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index 92c6f27a867..8eab40aeaf3 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -18,4 +18,56 @@ describe WikiHelper do
end
end
end
+
+ describe '#wiki_sort_controls' do
+ let(:project) { create(:project) }
+ let(:wiki_link) { helper.wiki_sort_controls(project, sort, direction) }
+ let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort" }
+
+ def expected_link(sort, direction, icon_class)
+ path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}"
+
+ helper.link_to(path, type: 'button', class: classes, title: 'Sort direction') do
+ helper.sprite_icon("sort-#{icon_class}", size: 16)
+ end
+ end
+
+ context 'initial call' do
+ let(:sort) { nil }
+ let(:direction) { nil }
+
+ it 'renders with default values' do
+ expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
+ end
+ end
+
+ context 'sort by title' do
+ let(:sort) { 'title' }
+ let(:direction) { 'asc' }
+
+ it 'renders a link with opposite direction' do
+ expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
+ end
+ end
+
+ context 'sort by created_at' do
+ let(:sort) { 'created_at' }
+ let(:direction) { 'desc' }
+
+ it 'renders a link with opposite direction' do
+ expect(wiki_link).to eq(expected_link('created_at', 'asc', 'highest'))
+ end
+ end
+ end
+
+ describe '#wiki_sort_title' do
+ it 'returns a title corresponding to a key' do
+ expect(helper.wiki_sort_title('created_at')).to eq('Created date')
+ expect(helper.wiki_sort_title('title')).to eq('Title')
+ end
+
+ it 'defaults to Title if a key is unknown' do
+ expect(helper.wiki_sort_title('unknown')).to eq('Title')
+ end
+ end
end
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 54fb0e8228b..e4ff3eb381f 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -178,6 +178,7 @@ describe('Issue model', () => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
expect(data.issue.assignee_ids).toEqual([1]);
done();
+ return Promise.resolve();
});
issue.update('url');
@@ -187,6 +188,7 @@ describe('Issue model', () => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
expect(data.issue.assignee_ids).toEqual([0]);
done();
+ return Promise.resolve();
});
issue.removeAllAssignees();
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
index 70f49469300..394e60fc22c 100644
--- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -127,20 +127,25 @@ describe('VariableList', () => {
variableList.init();
});
- it('should add another row when editing the last rows protected checkbox', done => {
+ it('should not add another row when editing the last rows protected checkbox', done => {
const $row = $wrapper.find('.js-row:last-child');
$row.find('.ci-variable-protected-item .js-project-feature-toggle').click();
getSetTimeoutPromise()
.then(() => {
- expect($wrapper.find('.js-row').length).toBe(2);
+ expect($wrapper.find('.js-row').length).toBe(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
- // Check for the correct default in the new row
- const $protectedInput = $wrapper
- .find('.js-row:last-child')
- .find('.js-ci-variable-input-protected');
+ it('should not add another row when editing the last rows masked checkbox', done => {
+ const $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
- expect($protectedInput.val()).toBe('false');
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find('.js-row').length).toBe(1);
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index 0d3dcc29f22..0a98df45b5d 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -300,9 +300,13 @@ describe('Clusters', () => {
describe('toggleIngressDomainHelpText', () => {
const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
+ let ingressPreviousState;
+ let ingressNewState;
- const ingressPreviousState = { status: INSTALLABLE };
- const ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' };
+ beforeEach(() => {
+ ingressPreviousState = { status: INSTALLABLE };
+ ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' };
+ });
describe(`when ingress application new status is ${INSTALLED}`, () => {
beforeEach(() => {
@@ -333,7 +337,7 @@ describe('Clusters', () => {
});
describe('when ingress application new status and old status are the same', () => {
- it('does not modify custom domain help text', () => {
+ it('does not display custom domain help text', () => {
ingressPreviousState.status = INSTALLED;
ingressNewState.status = ingressPreviousState.status;
@@ -342,5 +346,15 @@ describe('Clusters', () => {
expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
});
});
+
+ describe(`when ingress new status is ${INSTALLED} and there isn’t an ip assigned`, () => {
+ it('does not display custom domain help text', () => {
+ ingressNewState.externalIp = null;
+
+ cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
+
+ expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
+ });
+ });
});
});
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 8d7c52a2876..3ce69bc3c20 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -57,6 +57,24 @@ describe('diffs/components/app', () => {
wrapper.destroy();
});
+ it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.showTreeList = false;
+ state.diffs.isParallelView = false;
+ });
+
+ expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true);
+ });
+
+ it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.showTreeList = true;
+ state.diffs.isParallelView = false;
+ });
+
+ expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false);
+ });
+
it('displays loading icon on loading', () => {
createComponent({}, ({ state }) => {
state.diffs.isLoading = true;
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index e886f962d2f..77f8352047c 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -66,6 +66,26 @@ describe('CompareVersions', () => {
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', () => {
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 6614069f44d..e1170c9762e 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -672,7 +672,7 @@ describe('diff_file_header', () => {
vm = mountComponentWithStore(Component, { props, store });
- expect(vm.$el.querySelector('.js-expand-file').textContent).toContain('Show changes only');
+ expect(vm.$el.querySelector('.ic-doc-changes')).not.toBeNull();
});
it('shows expand text', () => {
@@ -680,7 +680,7 @@ describe('diff_file_header', () => {
vm = mountComponentWithStore(Component, { props, store });
- expect(vm.$el.querySelector('.js-expand-file').textContent).toContain('Show full file');
+ expect(vm.$el.querySelector('.ic-doc-expand')).not.toBeNull();
});
it('renders loading icon', () => {
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 645b3aa788a..0f3f9a10f94 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -65,3 +65,61 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
store_frontend_fixture(response, fixture_file_name)
end
end
+
+describe API::Issues, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+
+ def get_related_merge_requests(project_id, issue_iid, user = nil)
+ get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
+ end
+
+ def create_referencing_mr(user, project, issue)
+ attributes = {
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: "master",
+ target_branch: "test",
+ assignee: user,
+ description: "See #{issue.to_reference}"
+ }
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ it 'issues/related_merge_requests.json' do |example|
+ user = create(:user)
+ project = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ issue_title = 'foo'
+ issue_description = 'closed'
+ milestone = create(:milestone, title: '1.0.0', project: project)
+ issue = create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+
+ project.add_reporter(user)
+ create_referencing_mr(user, project, issue)
+
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "Some description")
+ project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline))
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
index 7deed985219..f00bc2eeb6d 100644
--- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,25 +1,31 @@
import Vue from 'vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'spec/helpers/vue_component_helper';
import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
const createComponent = () => {
const Component = Vue.extend(frequentItemsListItemComponent);
- return mountComponent(Component, {
- itemId: mockProject.id,
- itemName: mockProject.name,
- namespace: mockProject.namespace,
- webUrl: mockProject.webUrl,
- avatarUrl: mockProject.avatarUrl,
+ return shallowMount(Component, {
+ propsData: {
+ itemId: mockProject.id,
+ itemName: mockProject.name,
+ namespace: mockProject.namespace,
+ webUrl: mockProject.webUrl,
+ avatarUrl: mockProject.avatarUrl,
+ },
});
};
describe('FrequentItemsListItemComponent', () => {
+ let wrapper;
let vm;
beforeEach(() => {
- vm = createComponent();
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
});
afterEach(() => {
@@ -29,11 +35,11 @@ describe('FrequentItemsListItemComponent', () => {
describe('computed', () => {
describe('hasAvatar', () => {
it('should return `true` or `false` if whether avatar is present or not', () => {
- vm.avatarUrl = 'path/to/avatar.png';
+ wrapper.setProps({ avatarUrl: 'path/to/avatar.png' });
expect(vm.hasAvatar).toBe(true);
- vm.avatarUrl = null;
+ wrapper.setProps({ avatarUrl: null });
expect(vm.hasAvatar).toBe(false);
});
@@ -41,41 +47,49 @@ describe('FrequentItemsListItemComponent', () => {
describe('highlightedItemName', () => {
it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
- vm.matcher = 'lab';
+ wrapper.setProps({ matcher: 'lab' });
- expect(vm.highlightedItemName).toContain('<b>Lab</b>');
+ expect(wrapper.find('.js-frequent-items-item-title').html()).toContain(
+ '<b>L</b><b>a</b><b>b</b>',
+ );
});
it('should return project name as it is if `matcher` is not available', () => {
- vm.matcher = null;
+ wrapper.setProps({ matcher: null });
- expect(vm.highlightedItemName).toBe(mockProject.name);
+ expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe(
+ mockProject.name,
+ );
});
});
describe('truncatedNamespace', () => {
it('should truncate project name from namespace string', () => {
- vm.namespace = 'platform / nokia-3310';
+ wrapper.setProps({ namespace: 'platform / nokia-3310' });
- expect(vm.truncatedNamespace).toBe('platform');
+ expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform');
});
it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
- vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310';
+ wrapper.setProps({
+ namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310',
+ });
- expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset');
+ expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe(
+ 'platform / ... / Mobile Chipset',
+ );
});
});
});
describe('template', () => {
it('should render component element', () => {
- expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('a').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1);
+ expect(wrapper.classes()).toContain('frequent-items-list-item-container');
+ expect(wrapper.findAll('a').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1);
});
});
});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
index d564292f1ba..ddbbc5c2d29 100644
--- a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
@@ -1,19 +1,22 @@
import Vue from 'vue';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import eventHub from '~/frequent_items/event_hub';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
const createComponent = (namespace = 'projects') => {
const Component = Vue.extend(searchComponent);
- return mountComponent(Component, { namespace });
+ return shallowMount(Component, { propsData: { namespace } });
};
describe('FrequentItemsSearchInputComponent', () => {
+ let wrapper;
let vm;
beforeEach(() => {
- vm = createComponent();
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
});
afterEach(() => {
@@ -35,7 +38,7 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('mounted', () => {
it('should listen `dropdownOpen` event', done => {
spyOn(eventHub, '$on');
- const vmX = createComponent();
+ const vmX = createComponent().vm;
Vue.nextTick(() => {
expect(eventHub.$on).toHaveBeenCalledWith(
@@ -49,7 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('beforeDestroy', () => {
it('should unbind event listeners on eventHub', done => {
- const vmX = createComponent();
+ const vmX = createComponent().vm;
spyOn(eventHub, '$off');
vmX.$mount();
@@ -67,12 +70,12 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('template', () => {
it('should render component element', () => {
- const inputEl = vm.$el.querySelector('input.form-control');
-
- expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
- expect(inputEl).not.toBe(null);
- expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
- expect(vm.$el.querySelector('.search-icon')).toBeDefined();
+ expect(wrapper.classes()).toContain('search-input-container');
+ expect(wrapper.contains('input.form-control')).toBe(true);
+ expect(wrapper.contains('.search-icon')).toBe(true);
+ expect(wrapper.find('input.form-control').attributes('placeholder')).toBe(
+ 'Search your projects',
+ );
});
});
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
index 3a5d6c8a90b..23e6a055518 100644
--- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import store from '~/ide/stores';
+import consts from '~/ide/stores/modules/commit/constants';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from 'spec/ide/helpers';
@@ -7,20 +8,33 @@ import { projectData } from 'spec/ide/mock_data';
describe('IDE commit sidebar actions', () => {
let vm;
-
- beforeEach(done => {
+ const createComponent = ({
+ hasMR = false,
+ commitAction = consts.COMMIT_TO_NEW_BRANCH,
+ mergeRequestsEnabled = true,
+ currentBranchId = 'master',
+ shouldCreateMR = false,
+ } = {}) => {
const Component = Vue.extend(commitActions);
vm = createComponentWithStore(Component, store);
- vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.currentBranchId = currentBranchId;
vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.commit.commitAction = commitAction;
Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
+ vm.$store.state.projects.abcproject.merge_requests_enabled = mergeRequestsEnabled;
+ vm.$store.state.commit.shouldCreateMR = shouldCreateMR;
- vm.$mount();
+ if (hasMR) {
+ vm.$store.state.currentMergeRequestId = '1';
+ vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
+ store.state.currentMergeRequestId
+ ] = { foo: 'bar' };
+ }
- Vue.nextTick(done);
- });
+ return vm.$mount();
+ };
afterEach(() => {
vm.$destroy();
@@ -28,16 +42,20 @@ describe('IDE commit sidebar actions', () => {
resetStore(vm.$store);
});
- it('renders 3 groups', () => {
- expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3);
+ it('renders 2 groups', () => {
+ createComponent();
+
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
});
it('renders current branch text', () => {
+ createComponent();
+
expect(vm.$el.textContent).toContain('Commit to master branch');
});
it('hides merge request option when project merge requests are disabled', done => {
- vm.$store.state.projects.abcproject.merge_requests_enabled = false;
+ createComponent({ mergeRequestsEnabled: false });
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
@@ -49,9 +67,53 @@ describe('IDE commit sidebar actions', () => {
describe('commitToCurrentBranchText', () => {
it('escapes current branch', () => {
- vm.$store.state.currentBranchId = '<img src="x" />';
+ const injectedSrc = '<img src="x" />';
+ createComponent({ currentBranchId: injectedSrc });
+
+ expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
+ });
+ });
+
+ describe('create new MR checkbox', () => {
+ it('disables `createMR` button when an MR already exists and committing to current branch', () => {
+ createComponent({ hasMR: true, commitAction: consts.COMMIT_TO_CURRENT_BRANCH });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(true);
+ });
+
+ it('does not disable checkbox if MR does not exist', () => {
+ createComponent({ hasMR: false });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false);
+ });
+
+ it('does not disable checkbox when creating a new branch', () => {
+ createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false);
+ });
+
+ it('toggles off new MR when switching back to commit to current branch and MR exists', () => {
+ createComponent({
+ commitAction: consts.COMMIT_TO_NEW_BRANCH,
+ shouldCreateMR: true,
+ });
+ const currentBranchRadio = vm.$el.querySelector(
+ `input[value="${consts.COMMIT_TO_CURRENT_BRANCH}"`,
+ );
+ currentBranchRadio.click();
+
+ vm.$nextTick(() => {
+ expect(vm.$store.state.commit.shouldCreateMR).toBe(false);
+ });
+ });
+
+ it('toggles `shouldCreateMR` when clicking checkbox', () => {
+ createComponent();
+ const el = vm.$el.querySelector('input[type="checkbox"]');
+ el.dispatchEvent(new Event('change'));
- expect(vm.commitToCurrentBranchText).not.toContain('<img src="x" />');
+ expect(vm.$store.state.commit.shouldCreateMR).toBe(true);
});
});
});
diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js
index c93a939ad71..d7fed3f0681 100644
--- a/spec/javascripts/ide/components/file_row_extra_spec.js
+++ b/spec/javascripts/ide/components/file_row_extra_spec.js
@@ -20,7 +20,7 @@ describe('IDE extra file row component', () => {
file: {
...file('test'),
},
- mouseOver: false,
+ dropdownOpen: false,
});
spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount);
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
index 83e530f0a6a..aaebe88f314 100644
--- a/spec/javascripts/ide/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -56,11 +56,11 @@ describe('new dropdown component', () => {
});
});
- describe('dropdownOpen', () => {
+ describe('isOpen', () => {
it('scrolls dropdown into view', done => {
spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
- vm.dropdownOpen = true;
+ vm.isOpen = true;
setTimeout(() => {
expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js
index 68487733cb9..80829f2358a 100644
--- a/spec/javascripts/ide/components/pipelines/list_spec.js
+++ b/spec/javascripts/ide/components/pipelines/list_spec.js
@@ -11,6 +11,8 @@ describe('IDE pipelines list', () => {
let vm;
let mock;
+ const findLoadingState = () => vm.$el.querySelector('.loading-container');
+
beforeEach(done => {
const store = createStore();
@@ -95,7 +97,7 @@ describe('IDE pipelines list', () => {
describe('empty state', () => {
it('renders pipelines empty state', done => {
- vm.$store.state.pipelines.latestPipeline = false;
+ vm.$store.state.pipelines.latestPipeline = null;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
@@ -106,15 +108,30 @@ describe('IDE pipelines list', () => {
});
describe('loading state', () => {
- it('renders loading state when there is no latest pipeline', done => {
- vm.$store.state.pipelines.latestPipeline = null;
+ beforeEach(() => {
vm.$store.state.pipelines.isLoadingPipeline = true;
+ });
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+ it('does not render when pipeline has loaded before', done => {
+ vm.$store.state.pipelines.hasLoadedPipeline = true;
- done();
- });
+ vm.$nextTick()
+ .then(() => {
+ expect(findLoadingState()).toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders loading state when there is no latest pipeline', done => {
+ vm.$store.state.pipelines.hasLoadedPipeline = false;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(findLoadingState()).not.toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index a5839630657..4dd0c1150eb 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import actions, {
- getMergeRequestsForBranch,
getMergeRequestData,
getMergeRequestChanges,
getMergeRequestVersions,
@@ -12,13 +11,17 @@ import service from '~/ide/services';
import { activityBarViews } from '~/ide/constants';
import { resetStore } from '../../helpers';
+const TEST_PROJECT = 'abcproject';
+const TEST_PROJECT_ID = 17;
+
describe('IDE store merge request actions', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- store.state.projects.abcproject = {
+ store.state.projects[TEST_PROJECT] = {
+ id: TEST_PROJECT_ID,
mergeRequests: {},
};
});
@@ -41,10 +44,11 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequests service method', done => {
store
- .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' })
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.then(() => {
- expect(service.getProjectMergeRequests).toHaveBeenCalledWith('abcproject', {
+ expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, {
source_branch: 'bar',
+ source_project_id: TEST_PROJECT_ID,
order_by: 'created_at',
per_page: 1,
});
@@ -56,13 +60,11 @@ describe('IDE store merge request actions', () => {
it('sets the "Merge Request" Object', done => {
store
- .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' })
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.then(() => {
- expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(1);
- expect(Object.keys(store.state.projects.abcproject.mergeRequests)[0]).toEqual('2');
- expect(store.state.projects.abcproject.mergeRequests[2]).toEqual(
- jasmine.objectContaining(mrData),
- );
+ expect(store.state.projects.abcproject.mergeRequests).toEqual({
+ '2': jasmine.objectContaining(mrData),
+ });
done();
})
.catch(done.fail);
@@ -70,7 +72,7 @@ describe('IDE store merge request actions', () => {
it('sets "Current Merge Request" object to the most recent MR', done => {
store
- .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'bar' })
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.then(() => {
expect(store.state.currentMergeRequestId).toEqual('2');
done();
@@ -87,9 +89,9 @@ describe('IDE store merge request actions', () => {
it('does not fail if there are no merge requests for current branch', done => {
store
- .dispatch('getMergeRequestsForBranch', { projectId: 'abcproject', branchId: 'foo' })
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' })
.then(() => {
- expect(Object.keys(store.state.projects.abcproject.mergeRequests).length).toEqual(0);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({});
expect(store.state.currentMergeRequestId).toEqual('');
done();
})
@@ -106,7 +108,8 @@ describe('IDE store merge request actions', () => {
it('flashes message, if error', done => {
const flashSpy = spyOnDependency(actions, 'flash');
- getMergeRequestsForBranch({ commit() {} }, { projectId: 'abcproject', branchId: 'bar' })
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.then(() => {
fail('Expected getMergeRequestsForBranch to throw an error');
})
@@ -132,9 +135,9 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequestData service method', done => {
store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1, {
render_html: true,
});
@@ -145,10 +148,12 @@ describe('IDE store merge request actions', () => {
it('sets the Merge Request Object', done => {
store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
expect(store.state.currentMergeRequestId).toBe(1);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe(
+ 'mergerequest',
+ );
done();
})
@@ -170,7 +175,7 @@ describe('IDE store merge request actions', () => {
dispatch,
state: store.state,
},
- { projectId: 'abcproject', mergeRequestId: 1 },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
)
.then(done.fail)
.catch(() => {
@@ -179,7 +184,7 @@ describe('IDE store merge request actions', () => {
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
mergeRequestId: 1,
force: false,
},
@@ -193,7 +198,7 @@ describe('IDE store merge request actions', () => {
describe('getMergeRequestChanges', () => {
beforeEach(() => {
- store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
+ store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] };
});
describe('success', () => {
@@ -207,9 +212,9 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequestChanges service method', done => {
store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1);
done();
})
@@ -218,9 +223,9 @@ describe('IDE store merge request actions', () => {
it('sets the Merge Request Changes Object', done => {
store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe(
'mergerequest',
);
done();
@@ -243,7 +248,7 @@ describe('IDE store merge request actions', () => {
dispatch,
state: store.state,
},
- { projectId: 'abcproject', mergeRequestId: 1 },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
)
.then(done.fail)
.catch(() => {
@@ -252,7 +257,7 @@ describe('IDE store merge request actions', () => {
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
mergeRequestId: 1,
force: false,
},
@@ -266,7 +271,7 @@ describe('IDE store merge request actions', () => {
describe('getMergeRequestVersions', () => {
beforeEach(() => {
- store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
+ store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] };
});
describe('success', () => {
@@ -279,9 +284,9 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequestVersions service method', done => {
store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1);
done();
})
@@ -290,9 +295,9 @@ describe('IDE store merge request actions', () => {
it('sets the Merge Request Versions Object', done => {
store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1);
done();
})
.catch(done.fail);
@@ -313,7 +318,7 @@ describe('IDE store merge request actions', () => {
dispatch,
state: store.state,
},
- { projectId: 'abcproject', mergeRequestId: 1 },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
)
.then(done.fail)
.catch(() => {
@@ -322,7 +327,7 @@ describe('IDE store merge request actions', () => {
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
mergeRequestId: 1,
force: false,
},
@@ -336,7 +341,7 @@ describe('IDE store merge request actions', () => {
describe('openMergeRequest', () => {
const mr = {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
targetProjectId: 'defproject',
mergeRequestId: 2,
};
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index 7b0963713fb..cd519eaed7c 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -279,5 +279,22 @@ describe('IDE store project actions', () => {
.then(done)
.catch(done.fail);
});
+
+ it('creates a new file supplied via URL if the file does not exist yet', done => {
+ openBranch(store, { ...branch, basePath: 'not-existent.md' })
+ .then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ jasmine.anything(),
+ );
+
+ expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
+ name: 'not-existent.md',
+ type: 'blob',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index fbb676aab33..5ed9b9003a7 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'spec/helpers/vuex_action_helper';
-import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree';
+import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
@@ -206,4 +206,35 @@ describe('Multi-file store tree actions', () => {
);
});
});
+
+ describe('setDirectoryData', () => {
+ it('sets tree correctly if there are no opened files yet', done => {
+ const treeFile = file({ name: 'README.md' });
+ store.state.trees['abcproject/master'] = {};
+
+ testAction(
+ setDirectoryData,
+ { projectId: 'abcproject', branchId: 'master', treeList: [treeFile] },
+ store.state,
+ [
+ {
+ type: types.SET_DIRECTORY_DATA,
+ payload: {
+ treePath: 'abcproject/master',
+ data: [treeFile],
+ },
+ },
+ {
+ type: types.TOGGLE_LOADING,
+ payload: {
+ entry: {},
+ forceValue: false,
+ },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 34d97347438..abc97f3072c 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -3,7 +3,7 @@ import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
-import * as consts from '~/ide/stores/modules/commit/constants';
+import consts from '~/ide/stores/modules/commit/constants';
import { resetStore, file } from 'spec/ide/helpers';
describe('IDE commit module actions', () => {
@@ -389,7 +389,8 @@ describe('IDE commit module actions', () => {
it('redirects to new merge request page', done => {
spyOn(eventHub, '$on');
- store.state.commit.commitAction = '3';
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = true;
store
.dispatch('commit/commitChanges')
@@ -405,6 +406,21 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
+ it('does not redirect to new merge request page when shouldCreateMR is not checked', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = false;
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
it('resets changed files before redirecting', done => {
spyOn(eventHub, '$on');
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index 702e78ef773..e00fd7199d7 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -1,5 +1,5 @@
import commitState from '~/ide/stores/modules/commit/state';
-import * as consts from '~/ide/stores/modules/commit/constants';
+import consts from '~/ide/stores/modules/commit/constants';
import * as getters from '~/ide/stores/modules/commit/getters';
describe('IDE commit module getters', () => {
@@ -46,7 +46,7 @@ describe('IDE commit module getters', () => {
currentBranchId: 'master',
};
const localGetters = {
- placeholderBranchName: 'newBranchName',
+ placeholderBranchName: 'placeholder-branch-name',
};
beforeEach(() => {
@@ -59,25 +59,28 @@ describe('IDE commit module getters', () => {
expect(getters.branchName(state, null, rootState)).toBe('master');
});
- ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => {
- describe(type, () => {
- beforeEach(() => {
- Object.assign(state, {
- commitAction: consts[type],
- });
+ describe('COMMIT_TO_NEW_BRANCH', () => {
+ beforeEach(() => {
+ Object.assign(state, {
+ commitAction: consts.COMMIT_TO_NEW_BRANCH,
});
+ });
- it('uses newBranchName when not empty', () => {
- expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName');
+ it('uses newBranchName when not empty', () => {
+ const newBranchName = 'nonempty-branch-name';
+ Object.assign(state, {
+ newBranchName,
});
- it('uses placeholderBranchName when state newBranchName is empty', () => {
- Object.assign(state, {
- newBranchName: '',
- });
+ expect(getters.branchName(state, localGetters, rootState)).toBe(newBranchName);
+ });
- expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName');
+ it('uses placeholderBranchName when state newBranchName is empty', () => {
+ Object.assign(state, {
+ newBranchName: '',
});
+
+ expect(getters.branchName(state, localGetters, rootState)).toBe('placeholder-branch-name');
});
});
});
@@ -141,4 +144,33 @@ describe('IDE commit module getters', () => {
});
});
});
+
+ describe('shouldDisableNewMrOption', () => {
+ it('returns false if commitAction `COMMIT_TO_NEW_BRANCH`', () => {
+ state.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ const rootState = {
+ currentMergeRequest: { foo: 'bar' },
+ };
+
+ expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy();
+ });
+
+ it('returns false if there is no current merge request', () => {
+ state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ const rootState = {
+ currentMergeRequest: null,
+ };
+
+ expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy();
+ });
+
+ it('returns true an MR exists and commit action is `COMMIT_TO_CURRENT_BRANCH`', () => {
+ state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ const rootState = {
+ currentMergeRequest: { foo: 'bar' },
+ };
+
+ expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeTruthy();
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
index eb7346bd5fc..b558c45f574 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
@@ -27,63 +27,71 @@ describe('IDE pipelines mutations', () => {
});
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
- it('sets loading to false on success', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
- mockedState,
- fullPipelinesResponse.data.pipelines[0],
- );
+ const itSetsPipelineLoadingStates = () => {
+ it('sets has loaded to true', () => {
+ expect(mockedState.hasLoadedPipeline).toBe(true);
+ });
- expect(mockedState.isLoadingPipeline).toBe(false);
- });
+ it('sets loading to false on success', () => {
+ expect(mockedState.isLoadingPipeline).toBe(false);
+ });
+ };
+
+ describe('with pipeline', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
+ });
- it('sets latestPipeline', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
- mockedState,
- fullPipelinesResponse.data.pipelines[0],
- );
+ itSetsPipelineLoadingStates();
- expect(mockedState.latestPipeline).toEqual({
- id: '51',
- path: 'test',
- commit: { id: '123' },
- details: { status: jasmine.any(Object) },
- yamlError: undefined,
+ it('sets latestPipeline', () => {
+ expect(mockedState.latestPipeline).toEqual({
+ id: '51',
+ path: 'test',
+ commit: { id: '123' },
+ details: { status: jasmine.any(Object) },
+ yamlError: undefined,
+ });
});
- });
- it('does not set latest pipeline if pipeline is null', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
-
- expect(mockedState.latestPipeline).toEqual(false);
+ it('sets stages', () => {
+ expect(mockedState.stages.length).toBe(2);
+ expect(mockedState.stages).toEqual([
+ {
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ name: stages[0].name,
+ status: stages[0].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ {
+ id: 1,
+ dropdownPath: stages[1].dropdown_path,
+ name: stages[1].name,
+ status: stages[1].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ ]);
+ });
});
- it('sets stages', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
- mockedState,
- fullPipelinesResponse.data.pipelines[0],
- );
+ describe('with null', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
+ });
- expect(mockedState.stages.length).toBe(2);
- expect(mockedState.stages).toEqual([
- {
- id: 0,
- dropdownPath: stages[0].dropdown_path,
- name: stages[0].name,
- status: stages[0].status,
- isCollapsed: false,
- isLoading: false,
- jobs: [],
- },
- {
- id: 1,
- dropdownPath: stages[1].dropdown_path,
- name: stages[1].name,
- status: stages[1].status,
- isCollapsed: false,
- isLoading: false,
- jobs: [],
- },
- ]);
+ itSetsPipelineLoadingStates();
+
+ it('does not set latest pipeline if pipeline is null', () => {
+ expect(mockedState.latestPipeline).toEqual(null);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
index 67e9f7509da..7f9c978aa46 100644
--- a/spec/javascripts/ide/stores/mutations/tree_spec.js
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -26,17 +26,11 @@ describe('Multi-file store tree mutations', () => {
});
describe('SET_DIRECTORY_DATA', () => {
- const data = [
- {
- name: 'tree',
- },
- {
- name: 'submodule',
- },
- {
- name: 'blob',
- },
- ];
+ let data;
+
+ beforeEach(() => {
+ data = [file('tree'), file('foo'), file('blob')];
+ });
it('adds directory data', () => {
localState.trees['project/master'] = {
@@ -52,7 +46,7 @@ describe('Multi-file store tree mutations', () => {
expect(tree.tree.length).toBe(3);
expect(tree.tree[0].name).toBe('tree');
- expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[1].name).toBe('foo');
expect(tree.tree[2].name).toBe('blob');
});
@@ -65,6 +59,49 @@ describe('Multi-file store tree mutations', () => {
expect(localState.trees['project/master'].loading).toBe(true);
});
+
+ it('does not override tree already in state, but merges the two with correct order', () => {
+ const openedFile = file('new');
+
+ localState.trees['project/master'] = {
+ loading: true,
+ tree: [openedFile],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const { tree } = localState.trees['project/master'];
+
+ expect(tree.length).toBe(4);
+ expect(tree[0].name).toBe('blob');
+ expect(tree[1].name).toBe('foo');
+ expect(tree[2].name).toBe('new');
+ expect(tree[3].name).toBe('tree');
+ });
+
+ it('returns tree unchanged if the opened file is already in the tree', () => {
+ const openedFile = file('foo');
+ localState.trees['project/master'] = {
+ loading: true,
+ tree: [openedFile],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const { tree } = localState.trees['project/master'];
+
+ expect(tree.length).toBe(3);
+
+ expect(tree[0].name).toBe('tree');
+ expect(tree[1].name).toBe('foo');
+ expect(tree[2].name).toBe('blob');
+ });
});
describe('REMOVE_ALL_CHANGES_FILES', () => {
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index 9f18034f8a3..c4f122efdda 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -235,4 +235,129 @@ describe('Multi-file store utils', () => {
]);
});
});
+
+ describe('mergeTrees', () => {
+ let fromTree;
+ let toTree;
+
+ beforeEach(() => {
+ fromTree = [file('foo')];
+ toTree = [file('bar')];
+ });
+
+ it('merges simple trees with sorting the result', () => {
+ toTree = [file('beta'), file('alpha'), file('gamma')];
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res.length).toEqual(4);
+ expect(res[0].name).toEqual('alpha');
+ expect(res[1].name).toEqual('beta');
+ expect(res[2].name).toEqual('foo');
+ expect(res[3].name).toEqual('gamma');
+ expect(res[2]).toBe(fromTree[0]);
+ });
+
+ it('handles edge cases', () => {
+ expect(utils.mergeTrees({}, []).length).toEqual(0);
+
+ let res = utils.mergeTrees({}, toTree);
+
+ expect(res.length).toEqual(1);
+ expect(res[0].name).toEqual('bar');
+
+ res = utils.mergeTrees(fromTree, []);
+
+ expect(res.length).toEqual(1);
+ expect(res[0].name).toEqual('foo');
+ expect(res[0]).toBe(fromTree[0]);
+ });
+
+ it('merges simple trees without producing duplicates', () => {
+ toTree.push(file('foo'));
+
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res.length).toEqual(2);
+ expect(res[0].name).toEqual('bar');
+ expect(res[1].name).toEqual('foo');
+ expect(res[1]).not.toBe(fromTree[0]);
+ });
+
+ it('merges nested tree into the main one without duplicates', () => {
+ fromTree[0].tree.push({
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('beta.md'),
+ path: 'foo/alpha/beta.md',
+ },
+ ],
+ });
+
+ toTree.push({
+ ...file('foo'),
+ tree: [
+ {
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('gamma.md'),
+ path: 'foo/alpha/gamma.md',
+ },
+ ],
+ },
+ ],
+ });
+
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res.length).toEqual(2);
+ expect(res[1].name).toEqual('foo');
+
+ const finalBranch = res[1].tree[0].tree;
+
+ expect(finalBranch.length).toEqual(2);
+ expect(finalBranch[0].name).toEqual('beta.md');
+ expect(finalBranch[1].name).toEqual('gamma.md');
+ });
+
+ it('marks correct folders as opened as the parsing goes on', () => {
+ fromTree[0].tree.push({
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('beta.md'),
+ path: 'foo/alpha/beta.md',
+ },
+ ],
+ });
+
+ toTree.push({
+ ...file('foo'),
+ tree: [
+ {
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('gamma.md'),
+ path: 'foo/alpha/gamma.md',
+ },
+ ],
+ },
+ ],
+ });
+
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res[1].name).toEqual('foo');
+ expect(res[1].opened).toEqual(true);
+
+ expect(res[1].tree[0].name).toEqual('alpha');
+ expect(res[1].tree[0].opened).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index ba5d672f189..cef40117304 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -17,6 +17,7 @@ describe('Job App ', () => {
const props = {
endpoint: `${gl.TEST_HOST}jobs/123.json`,
runnerHelpUrl: 'help/runner',
+ deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
@@ -253,6 +254,41 @@ describe('Job App ', () => {
});
});
+ describe('unmet prerequisites block', () => {
+ it('renders unmet prerequisites block when there is an unmet prerequisites failure', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ illustration: {
+ content: 'Retry this job in order to create the necessary resources.',
+ image: 'path',
+ size: 'svg-430',
+ title: 'Failed to create resources',
+ },
+ },
+ failure_reason: 'unmet_prerequisites',
+ has_trace: false,
+ runners: {
+ available: true,
+ },
+ tags: [],
+ }),
+ );
+ vm = mountComponentWithStore(Component, { props, store });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull();
+ done();
+ }, 0);
+ });
+ });
+
describe('environments block', () => {
it('renders environment block when job has environment', done => {
mock.onGet(props.endpoint).replyOnce(
diff --git a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js
new file mode 100644
index 00000000000..68fcb321214
--- /dev/null
+++ b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import component from '~/jobs/components/unmet_prerequisites_block.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Unmet Prerequisites Block Job component', () => {
+ const Component = Vue.extend(component);
+ let vm;
+ const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs';
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ hasNoRunnersForProject: true,
+ helpPath,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders an alert with the correct message', () => {
+ const container = vm.$el.querySelector('.js-failed-unmet-prerequisites');
+ const alertMessage =
+ 'This job failed because the necessary resources were not successfully created.';
+
+ expect(container).not.toBeNull();
+ expect(container.innerHTML).toContain(alertMessage);
+ });
+
+ it('renders link to help page', () => {
+ const helpLink = vm.$el.querySelector('.js-help-path');
+
+ expect(helpLink).not.toBeNull();
+ expect(helpLink.innerHTML).toContain('More information');
+ expect(helpLink.getAttribute('href')).toEqual(helpPath);
+ });
+});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 2084c36e484..da012e1d5f7 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -2,7 +2,7 @@ import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter';
import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data';
-import BreakpointInstance from '~/breakpoints';
+import breakpointInstance from '~/breakpoints';
const PIXEL_TOLERANCE = 0.2;
@@ -383,7 +383,7 @@ describe('common_utils', () => {
describe('contentTop', () => {
it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => {
- spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('sm');
+ spyOn(breakpointInstance, 'isDesktop').and.returnValue(false);
setFixtures(`
<div class="diff-file file-title-flex-parent">
@@ -398,7 +398,7 @@ describe('common_utils', () => {
});
it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => {
- spyOn(BreakpointInstance, 'getBreakpointSize').and.returnValue('lg');
+ spyOn(breakpointInstance, 'isDesktop').and.returnValue(true);
setFixtures(`
<div class="diff-file file-title-flex-parent">
diff --git a/spec/javascripts/lib/utils/higlight_spec.js b/spec/javascripts/lib/utils/higlight_spec.js
new file mode 100644
index 00000000000..638bbf65ae9
--- /dev/null
+++ b/spec/javascripts/lib/utils/higlight_spec.js
@@ -0,0 +1,43 @@
+import highlight from '~/lib/utils/highlight';
+
+describe('highlight', () => {
+ it(`should appropriately surround substring matches`, () => {
+ const expected = 'g<b>i</b><b>t</b>lab';
+
+ expect(highlight('gitlab', 'it')).toBe(expected);
+ });
+
+ it(`should return an empty string in the case of invalid inputs`, () => {
+ [null, undefined].forEach(input => {
+ expect(highlight(input, 'match')).toBe('');
+ });
+ });
+
+ it(`should return the original value if match is null, undefined, or ''`, () => {
+ [null, undefined].forEach(match => {
+ expect(highlight('gitlab', match)).toBe('gitlab');
+ });
+ });
+
+ it(`should highlight matches in non-string inputs`, () => {
+ const expected = '123<b>4</b><b>5</b>6';
+
+ expect(highlight(123456, 45)).toBe(expected);
+ });
+
+ it(`should sanitize the input string before highlighting matches`, () => {
+ const expected = 'hello <b>w</b>orld';
+
+ expect(highlight('hello <b>world</b>', 'w')).toBe(expected);
+ });
+
+ it(`should not highlight anything if no matches are found`, () => {
+ expect(highlight('gitlab', 'hello')).toBe('gitlab');
+ });
+
+ it(`should allow wrapping elements to be customized`, () => {
+ const expected = '1<hello>2</hello>3';
+
+ expect(highlight('123', '2', '<hello>', '</hello>')).toBe(expected);
+ });
+});
diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js
index 549a7935c0f..41a6c04efb9 100644
--- a/spec/javascripts/monitoring/charts/area_spec.js
+++ b/spec/javascripts/monitoring/charts/area_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
import Area from '~/monitoring/components/charts/area.vue';
import MonitoringStore from '~/monitoring/stores/monitoring_store';
@@ -65,7 +65,7 @@ describe('Area component', () => {
expect(props.data).toBe(areaChart.vm.chartData);
expect(props.option).toBe(areaChart.vm.chartOptions);
expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText);
- expect(props.thresholds).toBe(areaChart.props('alertData'));
+ expect(props.thresholds).toBe(areaChart.vm.thresholds);
});
it('recieves a tooltip title', () => {
@@ -105,12 +105,13 @@ describe('Area component', () => {
seriesName: areaChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
+ seriesIndex: 0,
},
],
value: mockDate,
});
- describe('series is of line type', () => {
+ describe('when series is of line type', () => {
beforeEach(() => {
areaChart.vm.formatTooltipText(generateSeriesData('line'));
});
@@ -120,18 +121,20 @@ describe('Area component', () => {
});
it('formats tooltip content', () => {
- expect(areaChart.vm.tooltip.content).toEqual([{ name: 'Core Usage', value: '5.556' }]);
+ const name = 'Core Usage';
+ const value = '5.556';
+ const seriesLabel = areaChart.find(GlChartSeriesLabel);
+
+ expect(seriesLabel.vm.color).toBe('');
+ expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
+ expect(areaChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
expect(
- shallowWrapperContainsSlotText(
- areaChart.find(GlAreaChart),
- 'tooltipContent',
- 'Core Usage 5.556',
- ),
+ shallowWrapperContainsSlotText(areaChart.find(GlAreaChart), 'tooltipContent', value),
).toBe(true);
});
});
- describe('series is of scatter type', () => {
+ describe('when series is of scatter type', () => {
beforeEach(() => {
areaChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 454777fa912..ce2c6c43c0f 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
+import { timeWindows } from '~/monitoring/constants';
import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data';
@@ -50,7 +51,7 @@ describe('Dashboard', () => {
it('shows a getting started empty state when no metrics are present', () => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData,
+ propsData: { ...propsData, showTimeWindowDropdown: false },
});
expect(component.$el.querySelector('.prometheus-graphs')).toBe(null);
@@ -66,7 +67,7 @@ describe('Dashboard', () => {
it('shows up a loading state', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true },
+ propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: false },
});
Vue.nextTick(() => {
@@ -78,7 +79,12 @@ describe('Dashboard', () => {
it('hides the legend when showLegend is false', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showLegend: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showLegend: false,
+ showTimeWindowDropdown: false,
+ },
});
setTimeout(() => {
@@ -92,7 +98,12 @@ describe('Dashboard', () => {
it('hides the group panels when showPanels is false', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ },
});
setTimeout(() => {
@@ -106,7 +117,12 @@ describe('Dashboard', () => {
it('renders the environments dropdown with a number of environments', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ },
});
component.store.storeEnvironmentsData(environmentData);
@@ -124,7 +140,12 @@ describe('Dashboard', () => {
it('hides the environments dropdown list when there is no environments', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ },
});
component.store.storeEnvironmentsData([]);
@@ -142,7 +163,12 @@ describe('Dashboard', () => {
it('renders the environments dropdown with a single is-active element', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ },
});
component.store.storeEnvironmentsData(environmentData);
@@ -166,6 +192,7 @@ describe('Dashboard', () => {
hasMetrics: true,
showPanels: false,
environmentsEndpoint: '',
+ showTimeWindowDropdown: false,
},
});
@@ -176,6 +203,51 @@ describe('Dashboard', () => {
done();
});
});
+
+ it('does not show the time window dropdown when the feature flag is not set', done => {
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ },
+ });
+
+ setTimeout(() => {
+ const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
+
+ expect(timeWindowDropdown).toBeNull();
+
+ done();
+ });
+ });
+
+ it('renders the time window dropdown with a set of options', done => {
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: true,
+ },
+ });
+ const numberOfTimeWindows = Object.keys(timeWindows).length;
+
+ setTimeout(() => {
+ const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
+ const timeWindowDropdownEls = component.$el.querySelectorAll(
+ '.js-time-window-dropdown .dropdown-item',
+ );
+
+ expect(timeWindowDropdown).not.toBeNull();
+ expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
+
+ done();
+ });
+ });
});
describe('when the window resizes', () => {
@@ -191,7 +263,12 @@ describe('Dashboard', () => {
it('sets elWidth to page width when the sidebar is resized', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ },
});
expect(component.elWidth).toEqual(0);
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index d604e90b529..0cfcc994234 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -128,87 +128,33 @@ describe('noteActions', () => {
});
});
- describe('with feature flag replyToIndividualNotes enabled', () => {
+ describe('for showReply = true', () => {
beforeEach(() => {
- gon.features = {
- replyToIndividualNotes: true,
- };
- });
-
- afterEach(() => {
- gon.features = {};
- });
-
- describe('for showReply = true', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: true,
- });
- });
-
- it('shows a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
-
- expect(replyButton.exists()).toBe(true);
+ wrapper = shallowMountNoteActions({
+ ...props,
+ showReply: true,
});
});
- describe('for showReply = false', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: false,
- });
- });
-
- it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
+ it('shows a reply button', () => {
+ const replyButton = wrapper.find({ ref: 'replyButton' });
- expect(replyButton.exists()).toBe(false);
- });
+ expect(replyButton.exists()).toBe(true);
});
});
- describe('with feature flag replyToIndividualNotes disabled', () => {
+ describe('for showReply = false', () => {
beforeEach(() => {
- gon.features = {
- replyToIndividualNotes: false,
- };
- });
-
- afterEach(() => {
- gon.features = {};
- });
-
- describe('for showReply = true', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: true,
- });
- });
-
- it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
-
- expect(replyButton.exists()).toBe(false);
+ wrapper = shallowMountNoteActions({
+ ...props,
+ showReply: false,
});
});
- describe('for showReply = false', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: false,
- });
- });
-
- it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
+ it('does not show a reply button', () => {
+ const replyButton = wrapper.find({ ref: 'replyButton' });
- expect(replyButton.exists()).toBe(false);
- });
+ expect(replyButton.exists()).toBe(false);
});
});
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 348743081eb..1df5cf9ef68 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -44,8 +44,7 @@ export const noteableDataMock = {
milestone: null,
milestone_id: null,
moved_to_id: null,
- preview_note_path:
- '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+ preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue',
project_id: 2,
state: 'opened',
time_estimate: 0,
@@ -347,8 +346,7 @@ export const loggedOutnoteableData = {
},
noteable_note_url: '/group/project/merge_requests/1#note_1',
create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
- preview_note_path:
- '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+ preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue',
};
export const collapseNotesMock = [
diff --git a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js
new file mode 100644
index 00000000000..29760f79c3c
--- /dev/null
+++ b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js
@@ -0,0 +1,89 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
+import createStore from '~/related_merge_requests/store/index';
+
+const FIXTURE_PATH = 'issues/related_merge_requests.json';
+const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
+const localVue = createLocalVue();
+
+describe('RelatedMergeRequests', () => {
+ let wrapper;
+ let mock;
+ let mockData;
+
+ beforeEach(done => {
+ loadFixtures(FIXTURE_PATH);
+ mockData = getJSONFixture(FIXTURE_PATH);
+ mock = new MockAdapter(axios);
+ mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
+
+ wrapper = mount(RelatedMergeRequests, {
+ localVue,
+ sync: false,
+ store: createStore(),
+ propsData: {
+ endpoint: API_ENDPOINT,
+ projectNamespace: 'gitlab-org',
+ projectPath: 'gitlab-ce',
+ },
+ });
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('methods', () => {
+ describe('getAssignees', () => {
+ const assignees = [{ name: 'foo' }, { name: 'bar' }];
+
+ describe('when there is assignees array', () => {
+ it('should return assignees array', () => {
+ const mr = { assignees };
+
+ expect(wrapper.vm.getAssignees(mr)).toEqual(assignees);
+ });
+ });
+
+ it('should return an array with single assingee', () => {
+ const mr = { assignee: assignees[0] };
+
+ expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
+ });
+
+ it('should return empty array when assignee is not set', () => {
+ expect(wrapper.vm.getAssignees({})).toEqual([]);
+ expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render related merge request items', () => {
+ expect(wrapper.find('.js-items-count').text()).toEqual('2');
+ expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
+
+ const props = wrapper
+ .findAll(RelatedIssuableItem)
+ .at(1)
+ .props();
+ const data = mockData[1];
+
+ expect(props.idKey).toEqual(data.id);
+ expect(props.pathIdSeparator).toEqual('!');
+ expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status);
+ expect(props.assignees).toEqual([data.assignee]);
+ expect(props.isMergeRequest).toBe(true);
+ expect(props.confidential).toEqual(false);
+ expect(props.title).toEqual(data.title);
+ expect(props.state).toEqual(data.state);
+ expect(props.createdAt).toEqual(data.created_at);
+ });
+ });
+});
diff --git a/spec/javascripts/related_merge_requests/store/actions_spec.js b/spec/javascripts/related_merge_requests/store/actions_spec.js
new file mode 100644
index 00000000000..65e436fbb17
--- /dev/null
+++ b/spec/javascripts/related_merge_requests/store/actions_spec.js
@@ -0,0 +1,110 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as types from '~/related_merge_requests/store/mutation_types';
+import actionsModule, * as actions from '~/related_merge_requests/store/actions';
+import testAction from 'spec/helpers/vuex_action_helper';
+
+describe('RelatedMergeRequest store actions', () => {
+ let state;
+ let flashSpy;
+ let mock;
+
+ beforeEach(() => {
+ state = {
+ apiEndpoint: '/api/related_merge_requests',
+ };
+ flashSpy = spyOnDependency(actionsModule, 'createFlash');
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setInitialState', () => {
+ it('commits types.SET_INITIAL_STATE with given props', done => {
+ const props = { a: 1, b: 2 };
+
+ testAction(
+ actions.setInitialState,
+ props,
+ {},
+ [{ type: types.SET_INITIAL_STATE, payload: props }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestData', () => {
+ it('commits types.REQUEST_DATA', done => {
+ testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
+ });
+ });
+
+ describe('receiveDataSuccess', () => {
+ it('commits types.RECEIVE_DATA_SUCCESS with data', done => {
+ const data = { a: 1, b: 2 };
+
+ testAction(
+ actions.receiveDataSuccess,
+ data,
+ {},
+ [{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveDataError', () => {
+ it('commits types.RECEIVE_DATA_ERROR', done => {
+ testAction(
+ actions.receiveDataError,
+ null,
+ {},
+ [{ type: types.RECEIVE_DATA_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchMergeRequests', () => {
+ describe('for a successful request', () => {
+ it('should dispatch success action', done => {
+ const data = { a: 1 };
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
+
+ testAction(
+ actions.fetchMergeRequests,
+ null,
+ state,
+ [],
+ [{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
+ done,
+ );
+ });
+ });
+
+ describe('for a failing request', () => {
+ it('should dispatch error action', done => {
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
+
+ testAction(
+ actions.fetchMergeRequests,
+ null,
+ state,
+ [],
+ [{ type: 'requestData' }, { type: 'receiveDataError' }],
+ () => {
+ expect(flashSpy).toHaveBeenCalledTimes(1);
+ expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong'));
+
+ done();
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/related_merge_requests/store/mutations_spec.js b/spec/javascripts/related_merge_requests/store/mutations_spec.js
new file mode 100644
index 00000000000..21b6e26376b
--- /dev/null
+++ b/spec/javascripts/related_merge_requests/store/mutations_spec.js
@@ -0,0 +1,49 @@
+import mutations from '~/related_merge_requests/store/mutations';
+import * as types from '~/related_merge_requests/store/mutation_types';
+
+describe('RelatedMergeRequests Store Mutations', () => {
+ describe('SET_INITIAL_STATE', () => {
+ it('should set initial state according to given data', () => {
+ const apiEndpoint = '/api';
+ const state = {};
+
+ mutations[types.SET_INITIAL_STATE](state, { apiEndpoint });
+
+ expect(state.apiEndpoint).toEqual(apiEndpoint);
+ });
+ });
+
+ describe('REQUEST_DATA', () => {
+ it('should set loading flag', () => {
+ const state = {};
+
+ mutations[types.REQUEST_DATA](state);
+
+ expect(state.isFetchingMergeRequests).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_DATA_SUCCESS', () => {
+ it('should set loading flag and data', () => {
+ const state = {};
+ const mrs = [1, 2, 3];
+
+ mutations[types.RECEIVE_DATA_SUCCESS](state, { data: mrs, total: mrs.length });
+
+ expect(state.isFetchingMergeRequests).toEqual(false);
+ expect(state.mergeRequests).toEqual(mrs);
+ expect(state.totalCount).toEqual(mrs.length);
+ });
+ });
+
+ describe('RECEIVE_DATA_ERROR', () => {
+ it('should set loading and error flags', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_DATA_ERROR](state);
+
+ expect(state.isFetchingMergeRequests).toEqual(false);
+ expect(state.hasErrorFetchingMergeRequests).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/components/area_spec.js b/spec/javascripts/serverless/components/area_spec.js
new file mode 100644
index 00000000000..2be6ac3d268
--- /dev/null
+++ b/spec/javascripts/serverless/components/area_spec.js
@@ -0,0 +1,121 @@
+import { shallowMount } from '@vue/test-utils';
+import Area from '~/serverless/components/area.vue';
+import { mockNormalizedMetrics } from '../mock_data';
+
+describe('Area component', () => {
+ const mockWidgets = 'mockWidgets';
+ const mockGraphData = mockNormalizedMetrics;
+ let areaChart;
+
+ beforeEach(() => {
+ areaChart = shallowMount(Area, {
+ propsData: {
+ graphData: mockGraphData,
+ containerWidth: 0,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ });
+ });
+
+ afterEach(() => {
+ areaChart.destroy();
+ });
+
+ it('renders chart title', () => {
+ expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets);
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = mockNormalizedMetrics.queries[0].result[0].values[0].time;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ componentSubType: type,
+ value: [mockDate, 4],
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('series is of line type', () => {
+ beforeEach(() => {
+ areaChart.vm.formatTooltipText(generateSeriesData('line'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(areaChart.vm.tooltipPopoverTitle).toBe('28 Feb 2019, 11:11AM');
+ });
+
+ it('formats tooltip content', () => {
+ expect(areaChart.vm.tooltipPopoverContent).toBe('Invocations (requests): 4');
+ });
+ });
+
+ it('verify default interval value of 1', () => {
+ expect(areaChart.vm.getInterval).toBe(1);
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+
+ beforeEach(() => {
+ spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ width: mockWidth,
+ }));
+ areaChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(areaChart.vm.width).toBe(mockWidth);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ it('utilizes all data points', () => {
+ expect(Object.keys(areaChart.vm.chartData)).toEqual(['requests']);
+ expect(areaChart.vm.chartData.requests.length).toBe(2);
+ });
+
+ it('creates valid data', () => {
+ const data = areaChart.vm.chartData.requests;
+
+ expect(
+ data.filter(
+ datum => new Date(datum.time).getTime() > 0 && typeof datum.value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
+ });
+
+ describe('generateSeries', () => {
+ it('utilizes correct time data', () => {
+ expect(areaChart.vm.generateSeries.data).toEqual([
+ ['2019-02-28T11:11:38.756Z', 0],
+ ['2019-02-28T11:12:38.756Z', 0],
+ ]);
+ });
+ });
+
+ describe('xAxisLabel', () => {
+ it('constructs a label for the chart x-axis', () => {
+ expect(areaChart.vm.xAxisLabel).toBe('invocations / minute');
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(areaChart.vm.yAxisLabel).toBe('Invocations (requests)');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/javascripts/serverless/components/environment_row_spec.js
index bdf7a714910..932d712dbec 100644
--- a/spec/javascripts/serverless/components/environment_row_spec.js
+++ b/spec/javascripts/serverless/components/environment_row_spec.js
@@ -1,81 +1,70 @@
-import Vue from 'vue';
-
import environmentRowComponent from '~/serverless/components/environment_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
+import { translate } from '~/serverless/utils';
-const createComponent = (env, envName) =>
- mountComponent(Vue.extend(environmentRowComponent), { env, envName });
+const createComponent = (localVue, env, envName) =>
+ shallowMount(environmentRowComponent, { localVue, propsData: { env, envName } }).vm;
describe('environment row component', () => {
describe('default global cluster case', () => {
+ let localVue;
let vm;
beforeEach(() => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctions);
- vm = createComponent(store.state.functions['*'], '*');
+ localVue = createLocalVue();
+ vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*');
});
+ afterEach(() => vm.$destroy());
+
it('has the correct envId', () => {
expect(vm.envId).toEqual('env-global');
- vm.$destroy();
});
it('is open by default', () => {
expect(vm.isOpenClass).toEqual({ 'is-open': true });
- vm.$destroy();
});
it('generates correct output', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(2);
expect(vm.$el.id).toEqual('env-global');
expect(vm.$el.classList.contains('is-open')).toBe(true);
expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*');
-
- vm.$destroy();
});
it('opens and closes correctly', () => {
expect(vm.isOpen).toBe(true);
vm.toggleOpen();
- Vue.nextTick(() => {
- expect(vm.isOpen).toBe(false);
- });
- vm.$destroy();
+ expect(vm.isOpen).toBe(false);
});
});
describe('default named cluster case', () => {
let vm;
+ let localVue;
beforeEach(() => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv);
- vm = createComponent(store.state.functions.test, 'test');
+ localVue = createLocalVue();
+ vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test');
});
+ afterEach(() => vm.$destroy());
+
it('has the correct envId', () => {
expect(vm.envId).toEqual('env-test');
- vm.$destroy();
});
it('is open by default', () => {
expect(vm.isOpenClass).toEqual({ 'is-open': true });
- vm.$destroy();
});
it('generates correct output', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(1);
expect(vm.$el.id).toEqual('env-test');
expect(vm.$el.classList.contains('is-open')).toBe(true);
expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test');
-
- vm.$destroy();
});
});
});
diff --git a/spec/javascripts/serverless/components/function_details_spec.js b/spec/javascripts/serverless/components/function_details_spec.js
new file mode 100644
index 00000000000..a29d4a296ef
--- /dev/null
+++ b/spec/javascripts/serverless/components/function_details_spec.js
@@ -0,0 +1,113 @@
+import Vuex from 'vuex';
+
+import functionDetailsComponent from '~/serverless/components/function_details.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
+
+describe('functionDetailsComponent', () => {
+ let localVue;
+ let component;
+ let store;
+
+ beforeEach(() => {
+ localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ store = createStore();
+ });
+
+ afterEach(() => {
+ component.vm.$destroy();
+ });
+
+ describe('Verify base functionality', () => {
+ const serviceStub = {
+ name: 'test',
+ description: 'a description',
+ environment: '*',
+ url: 'http://service.com/test',
+ namespace: 'test-ns',
+ podcount: 0,
+ metricsUrl: '/metrics',
+ };
+
+ it('has a name, description, URL, and no pods loaded', () => {
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-name').innerHTML.trim(),
+ ).toContain('test');
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-description').innerHTML.trim(),
+ ).toContain('a description');
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain(
+ 'No pods loaded at this time.',
+ );
+ });
+
+ it('has a pods loaded', () => {
+ serviceStub.podcount = 1;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('1 pod in use');
+ });
+
+ it('has multiple pods loaded', () => {
+ serviceStub.podcount = 3;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('3 pods in use');
+ });
+
+ it('can support a missing description', () => {
+ serviceStub.description = null;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-description').querySelector('div')
+ .innerHTML.length,
+ ).toEqual(0);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/javascripts/serverless/components/function_row_spec.js
index 6933a8f6c87..3987e1753bd 100644
--- a/spec/javascripts/serverless/components/function_row_spec.js
+++ b/spec/javascripts/serverless/components/function_row_spec.js
@@ -1,11 +1,9 @@
-import Vue from 'vue';
-
import functionRowComponent from '~/serverless/components/function_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
import { mockServerlessFunction } from '../mock_data';
-const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func });
+const createComponent = func => shallowMount(functionRowComponent, { propsData: { func } }).vm;
describe('functionRowComponent', () => {
it('Parses the function details correctly', () => {
@@ -13,10 +11,7 @@ describe('functionRowComponent', () => {
expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name);
expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image);
- expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null);
- expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual(
- mockServerlessFunction.url,
- );
+ expect(vm.$el.querySelector('timeago-stub').getAttribute('time')).not.toBe(null);
vm.$destroy();
});
@@ -25,8 +20,6 @@ describe('functionRowComponent', () => {
const vm = createComponent(mockServerlessFunction);
expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row
- expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image
- expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar
vm.$destroy();
});
diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js
index 85cfe71281f..c32978ea58a 100644
--- a/spec/javascripts/serverless/components/functions_spec.js
+++ b/spec/javascripts/serverless/components/functions_spec.js
@@ -1,68 +1,101 @@
-import Vue from 'vue';
+import Vuex from 'vuex';
import functionsComponent from '~/serverless/components/functions.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
-
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
import { mockServerlessFunctions } from '../mock_data';
-const createComponent = (
- functions,
- installed = true,
- loadingData = true,
- hasFunctionData = true,
-) => {
- const component = Vue.extend(functionsComponent);
+describe('functionsComponent', () => {
+ let component;
+ let store;
+ let localVue;
+
+ beforeEach(() => {
+ localVue = createLocalVue();
+ localVue.use(Vuex);
- return mountComponent(component, {
- functions,
- installed,
- clustersPath: '/testClusterPath',
- helpPath: '/helpPath',
- loadingData,
- hasFunctionData,
+ store = createStore();
});
-};
-describe('functionsComponent', () => {
- it('should render empty state when Knative is not installed', () => {
- const vm = createComponent({}, false);
+ afterEach(() => {
+ component.vm.$destroy();
+ });
- expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true);
- expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual(
- 'Getting started with serverless',
- );
+ it('should render empty state when Knative is not installed', () => {
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: false,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
- vm.$destroy();
+ expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null);
});
it('should render a loading component', () => {
- const vm = createComponent({});
+ store.dispatch('requestFunctionsLoading');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
- expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null);
- expect(vm.$el.querySelector('div.animation-container')).not.toBe(null);
+ expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null);
});
it('should render empty state when there is no function data', () => {
- const vm = createComponent({}, true, false, false);
+ store.dispatch('receiveFunctionsNoDataSuccess');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
expect(
- vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'),
+ component.vm.$el
+ .querySelector('.empty-state, .js-empty-state')
+ .classList.contains('js-empty-state'),
).toBe(true);
- expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual(
+ expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual(
'No functions available',
);
-
- vm.$destroy();
});
it('should render the functions list', () => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctions);
- const vm = createComponent(store.state.functions, true, false);
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
- expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null);
- expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true);
+ return component.vm.$nextTick().then(() => {
+ expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null);
+ });
});
});
diff --git a/spec/javascripts/serverless/components/missing_prometheus_spec.js b/spec/javascripts/serverless/components/missing_prometheus_spec.js
new file mode 100644
index 00000000000..77aca03772b
--- /dev/null
+++ b/spec/javascripts/serverless/components/missing_prometheus_spec.js
@@ -0,0 +1,37 @@
+import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = missingData =>
+ shallowMount(missingPrometheusComponent, {
+ propsData: {
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ missingData,
+ },
+ }).vm;
+
+describe('missingPrometheusComponent', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render missing prometheus message', () => {
+ vm = createComponent(false);
+
+ expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
+ 'Function invocation metrics require Prometheus to be installed first.',
+ );
+
+ expect(vm.$el.querySelector('glbutton-stub').getAttribute('variant')).toEqual('success');
+ });
+
+ it('should render no prometheus data message', () => {
+ vm = createComponent(true);
+
+ expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
+ 'Invocation metrics loading or not available at this time.',
+ );
+ });
+});
diff --git a/spec/javascripts/serverless/components/pod_box_spec.js b/spec/javascripts/serverless/components/pod_box_spec.js
new file mode 100644
index 00000000000..69ac1a2bb5f
--- /dev/null
+++ b/spec/javascripts/serverless/components/pod_box_spec.js
@@ -0,0 +1,22 @@
+import podBoxComponent from '~/serverless/components/pod_box.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = count =>
+ shallowMount(podBoxComponent, {
+ propsData: {
+ count,
+ },
+ }).vm;
+
+describe('podBoxComponent', () => {
+ it('should render three boxes', () => {
+ const count = 3;
+ const vm = createComponent(count);
+ const rects = vm.$el.querySelectorAll('rect');
+
+ expect(rects.length).toEqual(3);
+ expect(parseInt(rects[2].getAttribute('x'), 10)).toEqual(40);
+
+ vm.$destroy();
+ });
+});
diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/javascripts/serverless/components/url_spec.js
index 21a879a49bb..08c3e4146b1 100644
--- a/spec/javascripts/serverless/components/url_spec.js
+++ b/spec/javascripts/serverless/components/url_spec.js
@@ -1,15 +1,13 @@
import Vue from 'vue';
-
import urlComponent from '~/serverless/components/url.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = uri => {
- const component = Vue.extend(urlComponent);
+import { shallowMount } from '@vue/test-utils';
- return mountComponent(component, {
- uri,
- });
-};
+const createComponent = uri =>
+ shallowMount(Vue.extend(urlComponent), {
+ propsData: {
+ uri,
+ },
+ }).vm;
describe('urlComponent', () => {
it('should render correctly', () => {
@@ -17,9 +15,7 @@ describe('urlComponent', () => {
const vm = createComponent(uri);
expect(vm.$el.classList.contains('clipboard-group')).toBe(true);
- expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual(
- uri,
- );
+ expect(vm.$el.querySelector('clipboardbutton-stub').getAttribute('text')).toEqual(uri);
expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri);
diff --git a/spec/javascripts/serverless/mock_data.js b/spec/javascripts/serverless/mock_data.js
index ecd393b174c..a2c18616324 100644
--- a/spec/javascripts/serverless/mock_data.js
+++ b/spec/javascripts/serverless/mock_data.js
@@ -77,3 +77,60 @@ export const mockMultilineServerlessFunction = {
description: 'testfunc1\nA test service line\\nWith additional services',
image: 'knative-test-container-buildtemplate',
};
+
+export const mockMetrics = {
+ success: true,
+ last_update: '2019-02-28T19:11:38.926Z',
+ metrics: {
+ id: 22,
+ title: 'Knative function invocations',
+ required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
+ weight: 0,
+ y_label: 'Invocations',
+ queries: [
+ {
+ query_range:
+ 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
+ unit: 'requests',
+ label: 'invocations / minute',
+ result: [
+ {
+ metric: {},
+ values: [[1551352298.756, '0'], [1551352358.756, '0']],
+ },
+ ],
+ },
+ ],
+ },
+};
+
+export const mockNormalizedMetrics = {
+ id: 22,
+ title: 'Knative function invocations',
+ required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
+ weight: 0,
+ y_label: 'Invocations',
+ queries: [
+ {
+ query_range:
+ 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
+ unit: 'requests',
+ label: 'invocations / minute',
+ result: [
+ {
+ metric: {},
+ values: [
+ {
+ time: '2019-02-28T11:11:38.756Z',
+ value: 0,
+ },
+ {
+ time: '2019-02-28T11:12:38.756Z',
+ value: 0,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/javascripts/serverless/store/actions_spec.js b/spec/javascripts/serverless/store/actions_spec.js
new file mode 100644
index 00000000000..602798573e9
--- /dev/null
+++ b/spec/javascripts/serverless/store/actions_spec.js
@@ -0,0 +1,88 @@
+import MockAdapter from 'axios-mock-adapter';
+import statusCodes from '~/lib/utils/http_status';
+import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
+import axios from '~/lib/utils/axios_utils';
+import testAction from '../../helpers/vuex_action_helper';
+import { adjustMetricQuery } from '../utils';
+
+describe('ServerlessActions', () => {
+ describe('fetchFunctions', () => {
+ it('should successfully fetch functions', done => {
+ const endpoint = '/functions';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions));
+
+ testAction(
+ fetchFunctions,
+ { functionsPath: endpoint },
+ {},
+ [],
+ [
+ { type: 'requestFunctionsLoading' },
+ { type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions },
+ ],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should successfully retry', done => {
+ const endpoint = '/functions';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
+
+ testAction(
+ fetchFunctions,
+ { functionsPath: endpoint },
+ {},
+ [],
+ [{ type: 'requestFunctionsLoading' }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('fetchMetrics', () => {
+ it('should return no prometheus', done => {
+ const endpoint = '/metrics';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
+
+ testAction(
+ fetchMetrics,
+ { metricsPath: endpoint, hasPrometheus: false },
+ {},
+ [],
+ [{ type: 'receiveMetricsNoPrometheus' }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should successfully fetch metrics', done => {
+ const endpoint = '/metrics';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics));
+
+ testAction(
+ fetchMetrics,
+ { metricsPath: endpoint, hasPrometheus: true },
+ {},
+ [],
+ [{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/store/getters_spec.js b/spec/javascripts/serverless/store/getters_spec.js
new file mode 100644
index 00000000000..fb549c8f153
--- /dev/null
+++ b/spec/javascripts/serverless/store/getters_spec.js
@@ -0,0 +1,43 @@
+import serverlessState from '~/serverless/store/state';
+import * as getters from '~/serverless/store/getters';
+import { mockServerlessFunctions } from '../mock_data';
+
+describe('Serverless Store Getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = serverlessState;
+ });
+
+ describe('hasPrometheusMissingData', () => {
+ it('should return false if Prometheus is not installed', () => {
+ state.hasPrometheus = false;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(false);
+ });
+
+ it('should return false if Prometheus is installed and there is data', () => {
+ state.hasPrometheusData = true;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(false);
+ });
+
+ it('should return true if Prometheus is installed and there is no data', () => {
+ state.hasPrometheus = true;
+ state.hasPrometheusData = false;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(true);
+ });
+ });
+
+ describe('getFunctions', () => {
+ it('should translate the raw function array to group the functions per environment scope', () => {
+ state.functions = mockServerlessFunctions;
+
+ const funcs = getters.getFunctions(state);
+
+ expect(Object.keys(funcs)).toContain('*');
+ expect(funcs['*'].length).toEqual(2);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/store/mutations_spec.js b/spec/javascripts/serverless/store/mutations_spec.js
new file mode 100644
index 00000000000..ca3053e5c38
--- /dev/null
+++ b/spec/javascripts/serverless/store/mutations_spec.js
@@ -0,0 +1,86 @@
+import mutations from '~/serverless/store/mutations';
+import * as types from '~/serverless/store/mutation_types';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
+
+describe('ServerlessMutations', () => {
+ describe('Functions List Mutations', () => {
+ it('should ensure loading is true', () => {
+ const state = {};
+
+ mutations[types.REQUEST_FUNCTIONS_LOADING](state);
+
+ expect(state.isLoading).toEqual(true);
+ });
+
+ it('should set proper state once functions are loaded', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_SUCCESS](state, mockServerlessFunctions);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(true);
+ expect(state.functions).toEqual(mockServerlessFunctions);
+ });
+
+ it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(false);
+ expect(state.functions).toBe(undefined);
+ });
+
+ it('should ensure loading has stopped, and an error is raised', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_ERROR](state, 'sample error');
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(false);
+ expect(state.functions).toBe(undefined);
+ expect(state.error).not.toBe(undefined);
+ });
+ });
+
+ describe('Function Details Metrics Mutations', () => {
+ it('should ensure isLoading and hasPrometheus data flags indicate data is loaded', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_SUCCESS](state, mockMetrics);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(true);
+ expect(state.graphData).toEqual(mockMetrics);
+ });
+
+ it('should ensure isLoading and hasPrometheus data flags are cleared indicating no functions available', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_NODATA_SUCCESS](state);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(false);
+ expect(state.graphData).toBe(undefined);
+ });
+
+ it('should properly indicate an error', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_ERROR](state, 'sample error');
+
+ expect(state.hasPrometheusData).toEqual(false);
+ expect(state.error).not.toBe(undefined);
+ });
+
+ it('should properly indicate when prometheus is installed', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_NO_PROMETHEUS](state);
+
+ expect(state.hasPrometheus).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js
deleted file mode 100644
index 72fd903d7d1..00000000000
--- a/spec/javascripts/serverless/stores/serverless_store_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import ServerlessStore from '~/serverless/stores/serverless_store';
-import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
-
-describe('Serverless Functions Store', () => {
- let store;
-
- beforeEach(() => {
- store = new ServerlessStore(false, '/cluster_path', 'help_path');
- });
-
- describe('#updateFunctionsFromServer', () => {
- it('should pass an empty hash object', () => {
- store.updateFunctionsFromServer();
-
- expect(store.state.functions).toEqual({});
- });
-
- it('should group functions to one global environment', () => {
- const mockServerlessData = mockServerlessFunctions;
- store.updateFunctionsFromServer(mockServerlessData);
-
- expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*']));
- expect(store.state.functions['*'].length).toEqual(2);
- });
-
- it('should group functions to multiple environments', () => {
- const mockServerlessData = mockServerlessFunctionsDiffEnv;
- store.updateFunctionsFromServer(mockServerlessData);
-
- expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*']));
- expect(store.state.functions['*'].length).toEqual(1);
- expect(store.state.functions.test.length).toEqual(1);
- expect(store.state.functions.test[0].name).toEqual('testfunc2');
- });
- });
-});
diff --git a/spec/javascripts/serverless/utils.js b/spec/javascripts/serverless/utils.js
new file mode 100644
index 00000000000..5ce2e37d493
--- /dev/null
+++ b/spec/javascripts/serverless/utils.js
@@ -0,0 +1,20 @@
+export const adjustMetricQuery = data => {
+ const updatedMetric = data.metrics;
+
+ const queries = data.metrics.queries.map(query => ({
+ ...query,
+ result: query.result.map(result => ({
+ ...result,
+ values: result.values.map(([timestamp, value]) => ({
+ time: new Date(timestamp * 1000).toISOString(),
+ value: Number(value),
+ })),
+ })),
+ }));
+
+ updatedMetric.queries = queries;
+ return updatedMetric;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js
index 657e88ecb96..f46ea5a0499 100644
--- a/spec/javascripts/sidebar/todo_spec.js
+++ b/spec/javascripts/sidebar/todo_spec.js
@@ -116,7 +116,7 @@ describe('SidebarTodo', () => {
const dataAttributes = {
issuableId: '1',
issuableType: 'epic',
- originalTitle: 'Mark todo as done',
+ originalTitle: '',
placement: 'left',
container: 'body',
boundary: 'viewport',
@@ -130,6 +130,10 @@ describe('SidebarTodo', () => {
});
});
+ it('check button label computed property', () => {
+ expect(vm.buttonLabel).toEqual('Mark todo as done');
+ });
+
it('renders button label element when `collapsed` prop is `false`', () => {
const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner');
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 235a17d13b0..87ef0885d8c 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -69,7 +69,7 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
-window.gon.ee = process.env.EE;
+window.gon.ee = process.env.IS_GITLAB_EE;
gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
@@ -124,7 +124,7 @@ const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
const testContexts = [require.context('spec', true, /_spec$/)];
-if (process.env.EE) {
+if (process.env.IS_GITLAB_EE) {
testContexts.push(require.context('ee_spec', true, /_spec$/));
}
@@ -213,7 +213,7 @@ if (process.env.BABEL_ENV === 'coverage') {
describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
- if (process.env.EE) {
+ if (process.env.IS_GITLAB_EE) {
sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
}
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 02c476f2871..cd77b0ab815 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -15,6 +15,16 @@ describe('MRWidgetHeader', () => {
gon.relative_url_root = '';
});
+ const expectDownloadDropdownItems = () => {
+ const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches');
+ const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff');
+
+ expect(downloadEmailPatchesEl.textContent.trim()).toEqual('Email patches');
+ expect(downloadEmailPatchesEl.getAttribute('href')).toEqual('/mr/email-patches');
+ expect(downloadPlainDiffEl.textContent.trim()).toEqual('Plain diff');
+ expect(downloadPlainDiffEl.getAttribute('href')).toEqual('/mr/plainDiffPath');
+ };
+
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
@@ -207,21 +217,7 @@ describe('MRWidgetHeader', () => {
});
it('renders download dropdown with links', () => {
- expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
- 'Email patches',
- );
-
- expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
- '/mr/email-patches',
- );
-
- expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
- 'Plain diff',
- );
-
- expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
- '/mr/plainDiffPath',
- );
+ expectDownloadDropdownItems();
});
});
@@ -250,10 +246,8 @@ describe('MRWidgetHeader', () => {
expect(button).toEqual(null);
});
- it('does not render download dropdown with links', () => {
- expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
-
- expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
+ it('renders download dropdown with links', () => {
+ expectDownloadDropdownItems();
});
});
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index d1fd899c1a8..7da69e3fa84 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import FileRow from '~/vue_shared/components/file_row.vue';
+import FileRowExtra from '~/ide/components/file_row_extra.vue';
import { file } from 'spec/ide/helpers';
import mountComponent from '../../helpers/vue_mount_component_helper';
@@ -16,6 +17,10 @@ describe('File row component', () => {
vm.$destroy();
});
+ const findNewDropdown = () => vm.$el.querySelector('.ide-new-btn .dropdown');
+ const findNewDropdownButton = () => vm.$el.querySelector('.ide-new-btn .dropdown button');
+ const findFileRow = () => vm.$el.querySelector('.file-row');
+
it('renders name', () => {
createComponent({
file: file('t4'),
@@ -84,4 +89,59 @@ describe('File row component', () => {
expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null);
});
+
+ describe('new dropdown', () => {
+ beforeEach(() => {
+ createComponent({
+ file: file('t5'),
+ level: 1,
+ extraComponent: FileRowExtra,
+ });
+ });
+
+ it('renders in extra component', () => {
+ expect(findNewDropdown()).not.toBe(null);
+ });
+
+ it('is hidden at start', () => {
+ expect(findNewDropdown()).not.toHaveClass('show');
+ });
+
+ it('is opened when button is clicked', done => {
+ expect(vm.dropdownOpen).toBe(false);
+ findNewDropdownButton().dispatchEvent(new Event('click'));
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.dropdownOpen).toBe(true);
+ expect(findNewDropdown()).toHaveClass('show');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('when opened', () => {
+ beforeEach(() => {
+ vm.dropdownOpen = true;
+ });
+
+ it('stays open when button triggers mouseout', () => {
+ findNewDropdownButton().dispatchEvent(new Event('mouseout'));
+
+ expect(vm.dropdownOpen).toBe(true);
+ });
+
+ it('stays open when button triggers mouseleave', () => {
+ findNewDropdownButton().dispatchEvent(new Event('mouseleave'));
+
+ expect(vm.dropdownOpen).toBe(true);
+ });
+
+ it('closes when row triggers mouseleave', () => {
+ findFileRow().dispatchEvent(new Event('mouseleave'));
+
+ expect(vm.dropdownOpen).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index e733a95288e..d4be2451f0b 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -98,7 +98,7 @@ describe('Markdown field header component', () => {
it('renders suggestion template', () => {
vm.lineContent = 'Some content';
- expect(vm.mdSuggestion).toEqual('```suggestion\n{text}\n```');
+ expect(vm.mdSuggestion).toEqual('```suggestion:-0+0\n{text}\n```');
});
it('does not render suggestion button if `canSuggest` is set to false', () => {
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js
index f87c2a92f47..ea74cb9eb21 100644
--- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -1,21 +1,50 @@
import Vue from 'vue';
import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue';
+import { selectDiffLines } from '~/vue_shared/components/lib/utils/diff_utils';
const MOCK_DATA = {
canApply: true,
- newLines: [
- { content: 'Line 1\n', lineNumber: 1 },
- { content: 'Line 2\n', lineNumber: 2 },
- { content: 'Line 3\n', lineNumber: 3 },
- ],
- fromLine: 1,
- fromContent: 'Old content',
suggestion: {
id: 1,
+ diff_lines: [
+ {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-test',
+ text: '-test',
+ type: 'old',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test',
+ text: '+new test',
+ type: 'new',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test2',
+ text: '+new test2',
+ type: 'new',
+ },
+ ],
},
helpPagePath: 'path_to_docs',
};
+const lines = selectDiffLines(MOCK_DATA.suggestion.diff_lines);
+const newLines = lines.filter(line => line.type === 'new');
+
describe('Suggestion Diff component', () => {
let vm;
@@ -39,30 +68,23 @@ describe('Suggestion Diff component', () => {
});
it('renders the oldLineNumber', () => {
- const fromLine = vm.$el.querySelector('.qa-old-diff-line-number').innerHTML;
+ const fromLine = vm.$el.querySelector('.old_line').innerHTML;
- expect(parseInt(fromLine, 10)).toBe(vm.fromLine);
+ expect(parseInt(fromLine, 10)).toBe(lines[0].old_line);
});
it('renders the oldLineContent', () => {
const fromContent = vm.$el.querySelector('.line_content.old').innerHTML;
- expect(fromContent.includes(vm.fromContent)).toBe(true);
- });
-
- it('renders the contents of newLines', () => {
- const newLines = vm.$el.querySelectorAll('.line_holder.new');
-
- newLines.forEach((line, i) => {
- expect(newLines[i].innerHTML.includes(vm.newLines[i].content)).toBe(true);
- });
+ expect(fromContent.includes(lines[0].text)).toBe(true);
});
- it('renders a line number for each line', () => {
- const newLineNumbers = vm.$el.querySelectorAll('.qa-new-diff-line-number');
+ it('renders new lines', () => {
+ const newLinesElements = vm.$el.querySelectorAll('.line_holder.new');
- newLineNumbers.forEach((line, i) => {
- expect(newLineNumbers[i].innerHTML.includes(vm.newLines[i].lineNumber)).toBe(true);
+ newLinesElements.forEach((line, i) => {
+ expect(newLinesElements[i].innerHTML.includes(newLines[i].new_line)).toBe(true);
+ expect(newLinesElements[i].innerHTML.includes(newLines[i].text)).toBe(true);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
index 33be63a3a1e..b7de40b4831 100644
--- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
@@ -2,46 +2,52 @@ import Vue from 'vue';
import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
const MOCK_DATA = {
- fromLine: 1,
- fromContent: 'Old content',
- suggestions: [],
+ suggestions: [
+ {
+ id: 1,
+ appliable: true,
+ applied: false,
+ current_user: {
+ can_apply: true,
+ },
+ diff_lines: [
+ {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-test',
+ text: '-test',
+ type: 'old',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test',
+ text: '+new test',
+ type: 'new',
+ },
+ ],
+ },
+ ],
noteHtml: `
+ <div class="suggestion">
+ <div class="line">-oldtest</div>
+ </div>
<div class="suggestion">
- <div class="line">Suggestion 1</div>
+ <div class="line">+newtest</div>
</div>
-
- <div class="suggestion">
- <div class="line">Suggestion 2</div>
- </div>
`,
isApplied: false,
helpPagePath: 'path_to_docs',
};
-const generateLine = content => {
- const line = document.createElement('div');
- line.className = 'line';
- line.innerHTML = content;
-
- return line;
-};
-
-const generateMockLines = () => {
- const line1 = generateLine('Line 1');
- const line2 = generateLine('Line 2');
- const line3 = generateLine('- Line 3');
- const container = document.createElement('div');
-
- container.appendChild(line1);
- container.appendChild(line2);
- container.appendChild(line3);
-
- return container;
-};
-
describe('Suggestion component', () => {
let vm;
- let extractedLines;
let diffTable;
beforeEach(done => {
@@ -51,8 +57,7 @@ describe('Suggestion component', () => {
propsData: MOCK_DATA,
}).$mount();
- extractedLines = vm.extractNewLines(generateMockLines());
- diffTable = vm.generateDiff(extractedLines).$mount().$el;
+ diffTable = vm.generateDiff(0).$mount().$el;
spyOn(vm, 'renderSuggestions');
vm.renderSuggestions();
@@ -70,32 +75,8 @@ describe('Suggestion component', () => {
it('renders suggestions', () => {
expect(vm.renderSuggestions).toHaveBeenCalled();
- expect(vm.$el.innerHTML.includes('Suggestion 1')).toBe(true);
- expect(vm.$el.innerHTML.includes('Suggestion 2')).toBe(true);
- });
- });
-
- describe('extractNewLines', () => {
- it('extracts suggested lines', () => {
- const expectedReturn = [
- { content: 'Line 1\n', lineNumber: 1 },
- { content: 'Line 2\n', lineNumber: 2 },
- { content: '- Line 3\n', lineNumber: 3 },
- ];
-
- expect(vm.extractNewLines(generateMockLines())).toEqual(expectedReturn);
- });
-
- it('increments line number for each extracted line', () => {
- expect(extractedLines[0].lineNumber).toEqual(1);
- expect(extractedLines[1].lineNumber).toEqual(2);
- expect(extractedLines[2].lineNumber).toEqual(3);
- });
-
- it('returns empty array if no lines are found', () => {
- const el = document.createElement('div');
-
- expect(vm.extractNewLines(el)).toEqual([]);
+ expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
+ expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
});
});
@@ -109,17 +90,17 @@ describe('Suggestion component', () => {
});
it('generates a diff table that contains contents the suggested lines', () => {
- extractedLines.forEach((line, i) => {
- expect(diffTable.innerHTML.includes(extractedLines[i].content)).toBe(true);
+ MOCK_DATA.suggestions[0].diff_lines.forEach(line => {
+ const text = line.text.substring(1);
+
+ expect(diffTable.innerHTML.includes(text)).toBe(true);
});
});
it('generates a diff table with the correct line number for each suggested line', () => {
- const lines = diffTable.getElementsByClassName('qa-new-diff-line-number');
+ const lines = diffTable.querySelectorAll('.old_line');
- expect([...lines][0].innerHTML).toBe('1');
- expect([...lines][1].innerHTML).toBe('2');
- expect([...lines][2].innerHTML).toBe('3');
+ expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
new file mode 100644
index 00000000000..b95183747bb
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
@@ -0,0 +1,110 @@
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { trimText } from 'spec/helpers/vue_component_helper';
+
+const localVue = createLocalVue();
+
+describe('ProjectListItem component', () => {
+ const Component = localVue.extend(ProjectListItem);
+ let wrapper;
+ let vm;
+ let options;
+ loadJSONFixtures('projects.json');
+ const project = getJSONFixture('projects.json')[0];
+
+ beforeEach(() => {
+ options = {
+ propsData: {
+ project,
+ selected: false,
+ },
+ sync: false,
+ localVue,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ });
+
+ it('does not render a check mark icon if selected === false', () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
+ });
+
+ it('renders a check mark icon if selected === true', () => {
+ options.propsData.selected = true;
+
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
+ });
+
+ it(`emits a "clicked" event when clicked`, () => {
+ wrapper = shallowMount(Component, options);
+ ({ vm } = wrapper);
+
+ spyOn(vm, '$emit');
+ wrapper.vm.onClick();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it(`renders the project avatar`, () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-project-avatar')).toBe(true);
+ });
+
+ it(`renders a simple namespace name with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a /');
+ });
+
+ it(`renders a properly truncated namespace with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a / ... / e /');
+ });
+
+ it(`renders the project name`, () => {
+ options.propsData.project.name = 'my-test-project';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').text());
+
+ expect(renderedName).toBe('my-test-project');
+ });
+
+ it(`renders the project name with highlighting in the case of a search query match`, () => {
+ options.propsData.project.name = 'my-test-project';
+ options.propsData.matcher = 'pro';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
+
+ expect(renderedName).toContain(expected);
+ });
+
+ it('prevents search query and project name XSS', () => {
+ const alertSpy = spyOn(window, 'alert');
+ options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
+ options.propsData.matcher = "pro<script>alert('XSS');</script>";
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-xss-project';
+
+ expect(renderedName).toContain(expected);
+ expect(alertSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
new file mode 100644
index 00000000000..ba9ec8f2f19
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -0,0 +1,132 @@
+import Vue from 'vue';
+import _ from 'underscore';
+import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'spec/helpers/vue_component_helper';
+
+describe('ProjectSelector component', () => {
+ let wrapper;
+ let vm;
+ loadJSONFixtures('projects.json');
+ const allProjects = getJSONFixture('projects.json');
+ const searchResults = allProjects.slice(0, 5);
+ let selected = [];
+ selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
+
+ beforeEach(() => {
+ jasmine.clock().install();
+
+ wrapper = shallowMount(Vue.extend(ProjectSelector), {
+ propsData: {
+ projectSearchResults: searchResults,
+ selectedProjects: selected,
+ showNoResultsMessage: false,
+ showMinimumSearchQueryMessage: false,
+ showLoadingIndicator: false,
+ showSearchErrorMessage: false,
+ },
+ attachToDocument: true,
+ });
+
+ ({ vm } = wrapper);
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ vm.$destroy();
+ });
+
+ it('renders the search results', () => {
+ expect(wrapper.findAll('.js-project-list-item').length).toBe(5);
+ });
+
+ it(`triggers a (debounced) search when the search input value changes`, () => {
+ spyOn(vm, '$emit');
+ const query = 'my test query!';
+ const searchInput = wrapper.find('.js-project-selector-input');
+ searchInput.setValue(query);
+ searchInput.trigger('input');
+
+ expect(vm.$emit).not.toHaveBeenCalledWith();
+ jasmine.clock().tick(501);
+
+ expect(vm.$emit).toHaveBeenCalledWith('searched', query);
+ });
+
+ it(`debounces the search input`, () => {
+ spyOn(vm, '$emit');
+ const searchInput = wrapper.find('.js-project-selector-input');
+
+ const updateSearchQuery = (count = 0) => {
+ if (count === 10) {
+ jasmine.clock().tick(101);
+
+ expect(vm.$emit).toHaveBeenCalledTimes(1);
+ expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`);
+ } else {
+ searchInput.setValue(`search query #${count}`);
+ searchInput.trigger('input');
+
+ jasmine.clock().tick(400);
+ updateSearchQuery(count + 1);
+ }
+ };
+
+ updateSearchQuery();
+ });
+
+ it(`includes a placeholder in the search box`, () => {
+ expect(wrapper.find('.js-project-selector-input').attributes('placeholder')).toBe(
+ 'Search your projects',
+ );
+ });
+
+ it(`triggers a "projectClicked" event when a project is clicked`, () => {
+ spyOn(vm, '$emit');
+ wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults));
+
+ expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults));
+ });
+
+ it(`shows a "no results" message if showNoResultsMessage === true`, () => {
+ wrapper.setProps({ showNoResultsMessage: true });
+
+ expect(wrapper.contains('.js-no-results-message')).toBe(true);
+
+ const noResultsEl = wrapper.find('.js-no-results-message');
+
+ expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
+ });
+
+ it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, () => {
+ wrapper.setProps({ showMinimumSearchQueryMessage: true });
+
+ expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true);
+
+ const minimumSearchEl = wrapper.find('.js-minimum-search-query-message');
+
+ expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search');
+ });
+
+ it(`shows a error message if showSearchErrorMessage === true`, () => {
+ wrapper.setProps({ showSearchErrorMessage: true });
+
+ expect(wrapper.contains('.js-search-error-message')).toBe(true);
+
+ const errorMessageEl = wrapper.find('.js-search-error-message');
+
+ expect(trimText(errorMessageEl.text())).toEqual(
+ 'Something went wrong, unable to search projects',
+ );
+ });
+
+ it(`focuses the input element when the focusSearchInput() method is called`, () => {
+ const input = wrapper.find('.js-project-selector-input');
+
+ expect(document.activeElement).not.toBe(input.element);
+ vm.focusSearchInput();
+
+ expect(document.activeElement).toBe(input.element);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index 5cf6afebd7e..0689fc1cf1f 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -45,12 +45,21 @@ describe('DropdownButtonComponent', () => {
});
const vmMoreLabels = createComponent(mockMoreLabels);
- expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more');
+ expect(vmMoreLabels.dropdownToggleText).toBe(
+ `Foo Label +${mockMoreLabels.labels.length - 1} more`,
+ );
vmMoreLabels.$destroy();
});
it('returns first label name when `labels` prop has only one item present', () => {
- expect(vm.dropdownToggleText).toBe('Foo Label');
+ const singleLabel = Object.assign({}, componentConfig, {
+ labels: [mockLabels[0]],
+ });
+ const vmSingleLabel = createComponent(singleLabel);
+
+ expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
+
+ vmSingleLabel.$destroy();
});
});
});
@@ -73,7 +82,7 @@ describe('DropdownButtonComponent', () => {
const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
expect(dropdownToggleTextEl).not.toBeNull();
- expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label');
+ expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
});
it('renders dropdown button icon', () => {
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 804b33422bd..cb49fa31d20 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -35,9 +35,12 @@ describe('DropdownValueCollapsedComponent', () => {
});
it('returns labels names separated by coma when `labels` prop has more than one item', () => {
- const vmMoreLabels = createComponent(mockLabels.concat(mockLabels));
+ const labels = mockLabels.concat(mockLabels);
+ const vmMoreLabels = createComponent(labels);
- expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label');
+ const expectedText = labels.map(label => label.title).join(', ');
+
+ expect(vmMoreLabels.labelsList).toBe(expectedText);
vmMoreLabels.$destroy();
});
@@ -49,14 +52,19 @@ describe('DropdownValueCollapsedComponent', () => {
const vmMoreLabels = createComponent(mockMoreLabels);
- expect(vmMoreLabels.labelsList).toBe(
- 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
- );
+ const expectedText = `${mockMoreLabels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ')}, and ${mockMoreLabels.length - 5} more`;
+
+ expect(vmMoreLabels.labelsList).toBe(expectedText);
vmMoreLabels.$destroy();
});
it('returns first label name when `labels` prop has only one item present', () => {
- expect(vm.labelsList).toBe('Foo Label');
+ const text = mockLabels.map(label => label.title).join(', ');
+
+ expect(vm.labelsList).toBe(text);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 3fff781594f..35a9c300953 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import $ from 'jquery';
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
@@ -15,6 +16,7 @@ const createComponent = (
return mountComponent(Component, {
labels,
labelFilterBasePath,
+ enableScopedLabels: true,
});
};
@@ -67,6 +69,26 @@ describe('DropdownValueComponent', () => {
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);
+ });
+ });
+
+ 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);
+ });
+ });
});
describe('template', () => {
@@ -91,15 +113,25 @@ describe('DropdownValueComponent', () => {
);
});
- it('renders label element with tooltip and styles based on label details', () => {
+ it('renders label element and styles based on label details', () => {
const labelEl = vm.$el.querySelector('a span.badge.color-label');
expect(labelEl).not.toBeNull();
- expect(labelEl.dataset.placement).toBe('bottom');
- expect(labelEl.dataset.container).toBe('body');
- expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description);
expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
expect(labelEl.innerText.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();
+ });
+
+ it('renders anchor tag containing question icon', () => {
+ const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label');
+
+ expect(anchor).not.toBeNull();
+ expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull();
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
index 3fcb91b6f5e..70025f041a7 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -6,6 +6,13 @@ export const mockLabels = [
color: '#BADA55',
text_color: '#FFFFFF',
},
+ {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#0033CC',
+ text_color: '#FFFFFF',
+ },
];
export const mockSuggestedColors = [
diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb
index 9945d598a14..aed86b21cb7 100644
--- a/spec/lib/api/helpers/custom_validators_spec.rb
+++ b/spec/lib/api/helpers/custom_validators_spec.rb
@@ -21,7 +21,7 @@ describe API::Helpers::CustomValidators do
end
context 'invalid parameters' do
- it 'should raise a validation error' do
+ it 'raises a validation error' do
expect_validation_error({ 'test' => 'some_value' })
end
end
@@ -44,7 +44,7 @@ describe API::Helpers::CustomValidators do
end
context 'invalid parameters' do
- it 'should raise a validation error' do
+ it 'raises a validation error' do
expect_validation_error({ 'test' => 'some_other_string' })
end
end
@@ -67,7 +67,7 @@ describe API::Helpers::CustomValidators do
end
context 'invalid parameters' do
- it 'should raise a validation error' do
+ it 'raises a validation error' do
expect_validation_error({ 'test' => 'some_other_string' })
end
end
diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
index b645e49bd43..5b3f679084e 100644
--- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
+++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
@@ -13,6 +13,6 @@ describe Banzai::Filter::BlockquoteFenceFilter do
end
it 'allows trailing whitespace on blockquote fence lines' do
- expect(filter(">>> \ntest\n>>> ")).to eq("> test")
+ expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n")
end
end
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index 8235c411eb7..6f7acfe7072 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper
- it 'should replace plantuml pre tag with img tag' do
+ it 'replaces plantuml pre tag with img tag' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
@@ -12,7 +12,7 @@ describe Banzai::Filter::PlantumlFilter do
expect(doc.to_s).to eq output
end
- it 'should not replace plantuml pre tag with img tag if disabled' do
+ it 'does not replace plantuml pre tag with img tag if disabled' do
stub_application_setting(plantuml_enabled: false)
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<pre><code lang="plantuml">Bob -&gt; Sara : Hello</code></pre>'
@@ -21,7 +21,7 @@ describe Banzai::Filter::PlantumlFilter do
expect(doc.to_s).to eq output
end
- it 'should not replace plantuml pre tag with img tag if url is invalid' do
+ it 'does not replace plantuml pre tag with img tag if url is invalid' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
input = '<pre><code lang="plantuml">Bob -> Sara : Hello</code></pre>'
output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
diff --git a/spec/lib/banzai/suggestions_parser_spec.rb b/spec/lib/banzai/suggestions_parser_spec.rb
deleted file mode 100644
index 79658d710ce..00000000000
--- a/spec/lib/banzai/suggestions_parser_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Banzai::SuggestionsParser do
- describe '.parse' do
- it 'returns a list of suggestion contents' do
- markdown = <<-MARKDOWN.strip_heredoc
- ```suggestion
- foo
- bar
- ```
-
- ```
- nothing
- ```
-
- ```suggestion
- xpto
- baz
- ```
-
- ```thing
- this is not a suggestion, it's a thing
- ```
- MARKDOWN
-
- expect(described_class.parse(markdown)).to eq([" foo\n bar",
- " xpto\n baz"])
- end
- end
-end
diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb
index 494c0561975..b9ffe895bf0 100644
--- a/spec/lib/forever_spec.rb
+++ b/spec/lib/forever_spec.rb
@@ -5,7 +5,7 @@ describe Forever do
subject { described_class.date }
context 'when using PostgreSQL' do
- it 'should return Postgresql future date' do
+ it 'returns Postgresql future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(subject).to eq(described_class::POSTGRESQL_DATE)
@@ -13,7 +13,7 @@ describe Forever do
end
context 'when using MySQL' do
- it 'should return MySQL future date' do
+ it 'returns MySQL future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(subject).to eq(described_class::MYSQL_DATE)
diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
index 812e0cc6947..128e118ac17 100644
--- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
end
shared_examples 'consistent kubernetes namespace attributes' do
- it 'should populate namespace and service account information' do
+ it 'populates namespace and service account information' do
migration.perform
clusters_with_namespace.each do |cluster|
@@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do
let(:cluster_projects) { cluster_projects_table.all }
- it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do
+ it 'creates a Clusters::KubernetesNamespace per Clusters::Project' do
expect do
migration.perform
end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects_table.count)
@@ -57,7 +57,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
create_kubernetes_namespace(clusters_table.all)
end
- it 'should not create any Clusters::KubernetesNamespace' do
+ it 'does not create any Clusters::KubernetesNamespace' do
expect do
migration.perform
end.not_to change(Clusters::KubernetesNamespace, :count)
@@ -78,7 +78,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count)
end
- it 'should not modify clusters with Clusters::KubernetesNamespace' do
+ it 'does not modify clusters with Clusters::KubernetesNamespace' do
migration.perform
with_kubernetes_namespace.each do |cluster|
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index 8582af96199..0e73c8c59c9 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
migration.perform(1, 3)
end
- it 'it creates the fork network' do
+ it 'creates the fork network' do
expect(fork_network1).not_to be_nil
expect(fork_network2).not_to be_nil
end
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
index ec0450643c3..22ca681cfd3 100644
--- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -101,6 +101,32 @@ describe Gitlab::Ci::Build::Policy::Refs do
expect(described_class.new(['/fix-.*/']))
.not_to be_satisfied_by(pipeline)
end
+
+ context 'when unsafe regexp is used' do
+ let(:subject) { described_class.new(['/^(?!master).+/']) }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
+ it 'ignores invalid regexp' do
+ expect(subject)
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when allow_unsafe_ruby_regexp is enabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+ end
+
+ it 'is satisfied by regexp' do
+ expect(subject)
+ .to be_satisfied_by(pipeline)
+ end
+ end
+ end
end
context 'malicious regexp' do
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 1c987e13a9a..fba5671594d 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -1,4 +1,5 @@
require 'fast_spec_helper'
+require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do
@@ -33,6 +34,44 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
+ context 'when config is an empty regexp' do
+ let(:config) { ['//'] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when using unsafe regexp' do
+ include StubFeatureFlags
+
+ let(:config) { ['/^(?!master).+/'] }
+
+ subject { described_class.new([regexp]) }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ context 'when allow_unsafe_ruby_regexp is enabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
context 'when config is a special keyword' do
let(:config) { %w[tags triggers branches] }
@@ -67,6 +106,34 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
+ context 'when using unsafe regexp' do
+ include StubFeatureFlags
+
+ let(:config) { { refs: ['/^(?!master).+/'] } }
+
+ subject { described_class.new([regexp]) }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ context 'when allow_unsafe_ruby_regexp is enabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
context 'when specifying kubernetes policy' do
let(:config) { { kubernetes: 'active' } }
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index fa39b32d7ab..dd536a241bd 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::Ci::Config::External::File::Base do
context 'when a location is present' do
let(:location) { 'some-location' }
- it 'should return true' do
+ it 'returns true' do
expect(subject).to be_matching
end
end
@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::File::Base do
context 'with a location is missing' do
let(:location) { nil }
- it 'should return false' do
+ it 'returns false' do
expect(subject).not_to be_matching
end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
index dc14b07287e..9451db9522a 100644
--- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'when a local is specified' do
let(:params) { { local: 'file' } }
- it 'should return true' do
+ it 'returns true' do
expect(local_file).to be_matching
end
end
@@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'with a missing local' do
let(:params) { { local: nil } }
- it 'should return false' do
+ it 'returns false' do
expect(local_file).not_to be_matching
end
end
@@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'with a missing local key' do
let(:params) { {} }
- it 'should return false' do
+ it 'returns false' do
expect(local_file).not_to be_matching
end
end
@@ -45,7 +45,7 @@ describe Gitlab::Ci::Config::External::File::Local do
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("image: 'ruby2:2'")
end
- it 'should return true' do
+ it 'returns true' do
expect(local_file.valid?).to be_truthy
end
end
@@ -53,7 +53,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'when is not a valid local path' do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
- it 'should return false' do
+ it 'returns false' do
expect(local_file.valid?).to be_falsy
end
end
@@ -61,7 +61,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'when is not a yaml file' do
let(:location) { '/config/application.rb' }
- it 'should return false' do
+ it 'returns false' do
expect(local_file.valid?).to be_falsy
end
end
@@ -84,7 +84,7 @@ describe Gitlab::Ci::Config::External::File::Local do
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return(local_file_content)
end
- it 'should return the content of the file' do
+ it 'returns the content of the file' do
expect(local_file.content).to eq(local_file_content)
end
end
@@ -92,7 +92,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'with an invalid file' do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
- it 'should be nil' do
+ it 'is nil' do
expect(local_file.content).to be_nil
end
end
@@ -101,7 +101,7 @@ describe Gitlab::Ci::Config::External::File::Local do
describe '#error_message' do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
- it 'should return an error message' do
+ it 'returns an error message' do
expect(local_file.error_message).to eq("Local file `#{location}` does not exist!")
end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
index 6e89bb1b30f..4acb4f324d3 100644
--- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Ci::Config::External::File::Project do
context 'when a file and project is specified' do
let(:params) { { file: 'file.yml', project: 'project' } }
- it 'should return true' do
+ it 'returns true' do
expect(project_file).to be_matching
end
end
@@ -27,7 +27,7 @@ describe Gitlab::Ci::Config::External::File::Project do
context 'with only file is specified' do
let(:params) { { file: 'file.yml' } }
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_matching
end
end
@@ -35,7 +35,7 @@ describe Gitlab::Ci::Config::External::File::Project do
context 'with only project is specified' do
let(:params) { { project: 'project' } }
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_matching
end
end
@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::External::File::Project do
context 'with a missing local key' do
let(:params) { {} }
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_matching
end
end
@@ -61,14 +61,14 @@ describe Gitlab::Ci::Config::External::File::Project do
stub_project_blob(root_ref_sha, '/file.yml') { 'image: ruby:2.1' }
end
- it 'should return true' do
+ it 'returns true' do
expect(project_file).to be_valid
end
context 'when user does not have permission to access file' do
let(:context_user) { create(:user) }
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_valid
expect(project_file.error_message).to include("Project `#{project.full_path}` not found or access denied!")
end
@@ -86,7 +86,7 @@ describe Gitlab::Ci::Config::External::File::Project do
stub_project_blob(ref_sha, '/file.yml') { 'image: ruby:2.1' }
end
- it 'should return true' do
+ it 'returns true' do
expect(project_file).to be_valid
end
end
@@ -102,7 +102,7 @@ describe Gitlab::Ci::Config::External::File::Project do
stub_project_blob(root_ref_sha, '/file.yml') { '' }
end
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_valid
expect(project_file.error_message).to include("Project `#{project.full_path}` file `/file.yml` is empty!")
end
@@ -113,7 +113,7 @@ describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, ref: 'I-Do-Not-Exist', file: '/file.yml' }
end
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_valid
expect(project_file.error_message).to include("Project `#{project.full_path}` reference `I-Do-Not-Exist` does not exist!")
end
@@ -124,7 +124,7 @@ describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, file: '/invalid-file.yml' }
end
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_valid
expect(project_file.error_message).to include("Project `#{project.full_path}` file `/invalid-file.yml` does not exist!")
end
@@ -135,7 +135,7 @@ describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, file: '/invalid-file' }
end
- it 'should return false' do
+ it 'returns false' do
expect(project_file).not_to be_valid
expect(project_file.error_message).to include('Included file `/invalid-file` does not have YAML extension!')
end
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index c5b32c29759..d8a61618e77 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when a remote is specified' do
let(:params) { { remote: 'http://remote' } }
- it 'should return true' do
+ it 'returns true' do
expect(remote_file).to be_matching
end
end
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with a missing remote' do
let(:params) { { remote: nil } }
- it 'should return false' do
+ it 'returns false' do
expect(remote_file).not_to be_matching
end
end
@@ -37,7 +37,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with a missing remote key' do
let(:params) { {} }
- it 'should return false' do
+ it 'returns false' do
expect(remote_file).not_to be_matching
end
end
@@ -49,7 +49,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
WebMock.stub_request(:get, location).to_return(body: remote_file_content)
end
- it 'should return true' do
+ it 'returns true' do
expect(remote_file.valid?).to be_truthy
end
end
@@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with an irregular url' do
let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
- it 'should return false' do
+ it 'returns false' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -67,7 +67,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error)
end
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -75,7 +75,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when is not a yaml file' do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -83,7 +83,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with an internal url' do
let(:location) { 'http://localhost:8080' }
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -95,7 +95,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
WebMock.stub_request(:get, location).to_return(body: remote_file_content)
end
- it 'should return the content of the file' do
+ it 'returns the content of the file' do
expect(remote_file.content).to eql(remote_file_content)
end
end
@@ -105,7 +105,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error)
end
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.content).to be_falsy
end
end
@@ -117,7 +117,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error'))
end
- it 'should be nil' do
+ it 'is nil' do
expect(remote_file.content).to be_nil
end
end
@@ -125,7 +125,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with an internal url' do
let(:location) { 'http://localhost:8080' }
- it 'should be nil' do
+ it 'is nil' do
expect(remote_file.content).to be_nil
end
end
@@ -147,7 +147,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
WebMock.stub_request(:get, location).to_timeout
end
- it 'should returns error message about a timeout' do
+ it 'returns error message about a timeout' do
expect(subject).to match /could not be fetched because of a timeout error!/
end
end
@@ -157,7 +157,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error)
end
- it 'should returns error message about a HTTP error' do
+ it 'returns error message about a HTTP error' do
expect(subject).to match /could not be fetched because of HTTP error!/
end
end
@@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404)
end
- it 'should returns error message about a timeout' do
+ it 'returns error message about a timeout' do
expect(subject).to match /could not be fetched because of HTTP code `404` error!/
end
end
@@ -175,7 +175,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when the URL is blocked' do
let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' }
- it 'should include details about blocked URL' do
+ it 'includes details about blocked URL' do
expect(subject).to eq "Remote file could not be fetched because URL '#{location}' " \
'is blocked: Requests to localhost are not allowed!'
end
diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
index 8ecaf4800f8..1609b8fd66b 100644
--- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'when a template is specified' do
let(:params) { { template: 'some-template' } }
- it 'should return true' do
+ it 'returns true' do
expect(template_file).to be_matching
end
end
@@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'with a missing template' do
let(:params) { { template: nil } }
- it 'should return false' do
+ it 'returns false' do
expect(template_file).not_to be_matching
end
end
@@ -32,7 +32,7 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'with a missing template key' do
let(:params) { {} }
- it 'should return false' do
+ it 'returns false' do
expect(template_file).not_to be_matching
end
end
@@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'when is a valid template name' do
let(:template) { 'Auto-DevOps.gitlab-ci.yml' }
- it 'should return true' do
+ it 'returns true' do
expect(template_file).to be_valid
end
end
@@ -50,7 +50,7 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'with invalid template name' do
let(:template) { 'Template.yml' }
- it 'should return false' do
+ it 'returns false' do
expect(template_file).not_to be_valid
expect(template_file.error_message).to include('Template file `Template.yml` is not a valid location!')
end
@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'with a non-existing template' do
let(:template) { 'I-Do-Not-Have-This-Template.gitlab-ci.yml' }
- it 'should return false' do
+ it 'returns false' do
expect(template_file).not_to be_valid
expect(template_file.error_message).to include('Included file `I-Do-Not-Have-This-Template.gitlab-ci.yml` is empty or does not exist!')
end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 3f6f6d7c5d9..e94bb44f990 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when no external files defined' do
let(:values) { { image: 'ruby:2.2' } }
- it 'should return the same values' do
+ it 'returns the same values' do
expect(processor.perform).to eq(values)
end
end
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when an invalid local file is defined' do
let(:values) { { include: '/lib/gitlab/ci/templates/non-existent-file.yml', image: 'ruby:2.2' } }
- it 'should raise an error' do
+ it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
@@ -45,7 +45,7 @@ describe Gitlab::Ci::Config::External::Processor do
WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error'))
end
- it 'should raise an error' do
+ it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Remote file `#{remote_file}` could not be fetched because of a socket error!"
@@ -78,12 +78,12 @@ describe Gitlab::Ci::Config::External::Processor do
WebMock.stub_request(:get, remote_file).to_return(body: external_file_content)
end
- it 'should append the file to the values' do
+ it 'appends the file to the values' do
output = processor.perform
expect(output.keys).to match_array([:image, :before_script, :rspec, :rubocop])
end
- it "should remove the 'include' keyword" do
+ it "removes the 'include' keyword" do
expect(processor.perform[:include]).to be_nil
end
end
@@ -105,12 +105,12 @@ describe Gitlab::Ci::Config::External::Processor do
.to receive(:fetch_local_content).and_return(local_file_content)
end
- it 'should append the file to the values' do
+ it 'appends the file to the values' do
output = processor.perform
expect(output.keys).to match_array([:image, :before_script])
end
- it "should remove the 'include' keyword" do
+ it "removes the 'include' keyword" do
expect(processor.perform[:include]).to be_nil
end
end
@@ -148,11 +148,11 @@ describe Gitlab::Ci::Config::External::Processor do
WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
end
- it 'should append the files to the values' do
+ it 'appends the files to the values' do
expect(processor.perform.keys).to match_array([:image, :stages, :before_script, :rspec])
end
- it "should remove the 'include' keyword" do
+ it "removes the 'include' keyword" do
expect(processor.perform[:include]).to be_nil
end
end
@@ -167,7 +167,7 @@ describe Gitlab::Ci::Config::External::Processor do
.to receive(:fetch_local_content).and_return(local_file_content)
end
- it 'should raise an error' do
+ it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
@@ -190,7 +190,7 @@ describe Gitlab::Ci::Config::External::Processor do
HEREDOC
end
- it 'should take precedence' do
+ it 'takes precedence' do
WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
expect(processor.perform[:image]).to eq('ruby:2.2')
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 00b2753c5fc..fd2a29e4ddb 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -225,7 +225,7 @@ describe Gitlab::Ci::Config do
end
context "when gitlab_ci_yml has valid 'include' defined" do
- it 'should return a composed hash' do
+ it 'returns a composed hash' do
before_script_values = [
"apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v",
"which ruby",
@@ -316,7 +316,7 @@ describe Gitlab::Ci::Config do
HEREDOC
end
- it 'should take precedence' do
+ it 'takes precedence' do
expect(config.to_hash).to eq({ image: 'ruby:2.2' })
end
end
@@ -341,7 +341,7 @@ describe Gitlab::Ci::Config do
HEREDOC
end
- it 'should merge the variables dictionaries' do
+ it 'merges the variables dictionaries' do
expect(config.to_hash).to eq({ variables: { A: 'alpha', B: 'beta', C: 'gamma', D: 'delta' } })
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
index dc13cae961c..c7f4fc98ca3 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
step.perform!
end
- it 'should break the chain' do
+ it 'breaks the chain' do
expect(step.break?).to be true
end
@@ -37,11 +37,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
step.perform!
end
- it 'should not break the chain' do
+ it 'does not break the chain' do
expect(step.break?).to be false
end
- it 'should not skip a pipeline chain' do
+ it 'does not skip a pipeline chain' do
expect(pipeline.reload).not_to be_skipped
end
end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index b379b08ad62..b6231510b91 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -123,6 +123,35 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.action_path).to include 'retry'
end
end
+
+ context 'when build has unmet prerequisites' do
+ let(:build) { create(:ci_build, :prerequisite_failure) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable,
+ Gitlab::Ci::Status::Build::FailedUnmetPrerequisites]
+ end
+
+ it 'fabricates a failed with unmet prerequisites build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::FailedUnmetPrerequisites
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'status_failed'
+ expect(status.favicon).to eq 'favicon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ expect(status.action_title).to include 'Retry'
+ expect(status.action_path).to include 'retry'
+ end
+ end
end
context 'when build is a canceled' do
diff --git a/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb
new file mode 100644
index 00000000000..a4854bdc6b9
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Build::FailedUnmetPrerequisites do
+ describe '#illustration' do
+ subject { described_class.new(double).illustration }
+
+ it { is_expected.to include(:image, :size, :title, :content) }
+ end
+
+ describe '.matches?' do
+ let(:build) { create(:ci_build, :created) }
+
+ subject { described_class.matches?(build, double) }
+
+ context 'when build has not failed' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when build has failed' do
+ before do
+ build.drop!(failure_reason)
+ end
+
+ context 'with unmet prerequisites' do
+ let(:failure_reason) { :unmet_prerequisites }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with a different error' do
+ let(:failure_reason) { :runner_system_failure }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb
index e20a621168c..4e3681cd943 100644
--- a/spec/lib/gitlab/ci/templates/templates_spec.rb
+++ b/spec/lib/gitlab/ci/templates/templates_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
describe "CI YML Templates" do
ABSTRACT_TEMPLATES = %w[Serverless].freeze
- PROJECT_DEPENDENT_TEMPLATES = %w[Auto-DevOps].freeze
+ # These templates depend on the presence of the `project`
+ # param to enable processing of `include:` within CI config.
+ PROJECT_DEPENDENT_TEMPLATES = %w[Auto-DevOps DAST].freeze
def self.concrete_templates
Gitlab::Template::GitlabCiYmlTemplate.all.reject do |template|
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 63a0d54dcfc..8b39c4e4dd0 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -615,6 +615,14 @@ module Gitlab
subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) }
context "when validating a ci config file with no project context" do
+ context "when a single string is provided" do
+ let(:include_content) { "/local.gitlab-ci.yml" }
+
+ it "does not return any error" do
+ expect { subject }.not_to raise_error
+ end
+ end
+
context "when an array is provided" do
let(:include_content) { ["/local.gitlab-ci.yml"] }
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index b7924302014..51e5bdc6307 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -150,13 +150,13 @@ describe Gitlab::ContributionsCalendar do
end
describe '#starting_year' do
- it "should be the start of last year" do
+ it "is the start of last year" do
expect(calendar.starting_year).to eq(last_year.year)
end
end
describe '#starting_month' do
- it "should be the start of this month" do
+ it "is the start of this month" do
expect(calendar.starting_month).to eq(today.month)
end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
index 1d31f96159c..ddd54a669a3 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do
describe '#rename_wildcard_paths' do
it_behaves_like 'renames child namespaces'
- it 'should rename projects' do
+ it 'renames projects' do
rename_projects = double
expect(described_class::RenameProjects)
.to receive(:new).with(['the-path'], subject)
@@ -40,7 +40,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do
end
describe '#rename_root_paths' do
- it 'should rename namespaces' do
+ it 'renames namespaces' do
rename_namespaces = double
expect(described_class::RenameNamespaces)
.to receive(:new).with(['the-path'], subject)
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 256166dbad3..0697594c725 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:diffable) { merge_request.merge_request_diff }
end
- it 'it uses a different cache key if diff line keys change' do
+ it 'uses a different cache key if diff line keys change' do
mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil)
key = mr_diff.cache_key
diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb
index 71fd25df698..d7ca0e0a522 100644
--- a/spec/lib/gitlab/diff/suggestion_spec.rb
+++ b/spec/lib/gitlab/diff/suggestion_spec.rb
@@ -10,6 +10,16 @@ describe Gitlab::Diff::Suggestion do
lines_above: above,
lines_below: below)
end
+
+ it 'returns diff lines with correct line numbers' do
+ diff_lines = suggestion.diff_lines
+
+ expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
+
+ expected_diff_lines.each_with_index do |expected_line, index|
+ expect(diff_lines[index].to_hash).to include(expected_line)
+ end
+ end
end
let(:merge_request) { create(:merge_request) }
@@ -48,6 +58,18 @@ describe Gitlab::Diff::Suggestion do
let(:expected_above) { line - 1 }
let(:expected_below) { below }
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 1, new_pos: 1, type: 'old', text: "-require 'fileutils'" },
+ { old_pos: 2, new_pos: 1, type: 'old', text: "-require 'open3'" },
+ { old_pos: 3, new_pos: 1, type: 'old', text: "-" },
+ { old_pos: 4, new_pos: 1, type: 'old', text: "-module Popen" },
+ { old_pos: 5, new_pos: 1, type: 'old', text: "- extend self" },
+ { old_pos: 6, new_pos: 1, type: 'old', text: "-" },
+ { old_pos: 7, new_pos: 1, type: 'new', text: "+# parsed suggestion content" },
+ { old_pos: 7, new_pos: 2, type: 'new', text: "+# with comments" }
+ ]
+ end
it_behaves_like 'correct suggestion raw content'
end
@@ -59,6 +81,47 @@ describe Gitlab::Diff::Suggestion do
let(:expected_below) { below }
let(:expected_above) { above }
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 4, new_pos: 4, type: "match", text: "@@ -4 +4" },
+ { old_pos: 4, new_pos: 4, type: "old", text: "-module Popen" },
+ { old_pos: 5, new_pos: 4, type: "old", text: "- extend self" },
+ { old_pos: 6, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 7, new_pos: 4, type: "old", text: "- def popen(cmd, path=nil)" },
+ { old_pos: 8, new_pos: 4, type: "old", text: "- unless cmd.is_a?(Array)" },
+ { old_pos: 9, new_pos: 4, type: "old", text: "- raise RuntimeError, \"System commands must be given as an array of strings\"" },
+ { old_pos: 10, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 11, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 12, new_pos: 4, type: "old", text: "- path ||= Dir.pwd" },
+ { old_pos: 13, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 14, new_pos: 4, type: "old", text: "- vars = {" },
+ { old_pos: 15, new_pos: 4, type: "old", text: "- \"PWD\" => path" },
+ { old_pos: 16, new_pos: 4, type: "old", text: "- }" },
+ { old_pos: 17, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 18, new_pos: 4, type: "old", text: "- options = {" },
+ { old_pos: 19, new_pos: 4, type: "old", text: "- chdir: path" },
+ { old_pos: 20, new_pos: 4, type: "old", text: "- }" },
+ { old_pos: 21, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 22, new_pos: 4, type: "old", text: "- unless File.directory?(path)" },
+ { old_pos: 23, new_pos: 4, type: "old", text: "- FileUtils.mkdir_p(path)" },
+ { old_pos: 24, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 25, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 26, new_pos: 4, type: "old", text: "- @cmd_output = \"\"" },
+ { old_pos: 27, new_pos: 4, type: "old", text: "- @cmd_status = 0" },
+ { old_pos: 28, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 29, new_pos: 4, type: "old", text: "- Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|" },
+ { old_pos: 30, new_pos: 4, type: "old", text: "- @cmd_output << stdout.read" },
+ { old_pos: 31, new_pos: 4, type: "old", text: "- @cmd_output << stderr.read" },
+ { old_pos: 32, new_pos: 4, type: "old", text: "- @cmd_status = wait_thr.value.exitstatus" },
+ { old_pos: 33, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 34, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 35, new_pos: 4, type: "old", text: "- return @cmd_output, @cmd_status" },
+ { old_pos: 36, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 37, new_pos: 4, type: "old", text: "-end" },
+ { old_pos: 38, new_pos: 4, type: "new", text: "+# parsed suggestion content" },
+ { old_pos: 38, new_pos: 5, type: "new", text: "+# with comments" }
+ ]
+ end
it_behaves_like 'correct suggestion raw content'
end
@@ -70,17 +133,19 @@ describe Gitlab::Diff::Suggestion do
let(:expected_below) { below }
let(:expected_above) { above }
let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
-
- it_behaves_like 'correct suggestion raw content'
- end
-
- context 'when no extra lines (single-line suggestion)' do
- let(:line) { 5 }
- let(:above) { 0 }
- let(:below) { 0 }
- let(:expected_below) { below }
- let(:expected_above) { above }
- let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 3, new_pos: 3, type: "match", text: "@@ -3 +3" },
+ { old_pos: 3, new_pos: 3, type: "old", text: "-" },
+ { old_pos: 4, new_pos: 3, type: "old", text: "-module Popen" },
+ { old_pos: 5, new_pos: 3, type: "old", text: "- extend self" },
+ { old_pos: 6, new_pos: 3, type: "old", text: "-" },
+ { old_pos: 7, new_pos: 3, type: "old", text: "- def popen(cmd, path=nil)" },
+ { old_pos: 8, new_pos: 3, type: "old", text: "- unless cmd.is_a?(Array)" },
+ { old_pos: 9, new_pos: 3, type: "new", text: "+# parsed suggestion content" },
+ { old_pos: 9, new_pos: 4, type: "new", text: "+# with comments" }
+ ]
+ end
it_behaves_like 'correct suggestion raw content'
end
diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
index 1119ea04995..1f2af42f6e7 100644
--- a/spec/lib/gitlab/diff/suggestions_parser_spec.rb
+++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
@@ -69,5 +69,66 @@ describe Gitlab::Diff::SuggestionsParser do
lines_below: 0)
end
end
+
+ context 'multi-line suggestions' do
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ ```suggestion:-2+1
+ # above and below
+ ```
+
+ ```
+ nothing
+ ```
+
+ ```suggestion:-3
+ # only above
+ ```
+
+ ```suggestion:+3
+ # only below
+ ```
+
+ ```thing
+ this is not a suggestion, it's a thing
+ ```
+ MARKDOWN
+ end
+
+ it 'returns a list of Gitlab::Diff::Suggestion' do
+ expect(subject).to all(be_a(Gitlab::Diff::Suggestion))
+ expect(subject.size).to eq(3)
+ end
+
+ it 'suggestion with above and below param has correct data' do
+ from_line = position.new_line - 2
+ to_line = position.new_line + 1
+
+ expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
+ to_content: " # above and below\n",
+ lines_above: 2,
+ lines_below: 1)
+ end
+
+ it 'suggestion with above param has correct data' do
+ from_line = position.new_line - 3
+ to_line = position.new_line
+
+ expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only above\n",
+ lines_above: 3,
+ lines_below: 0)
+ end
+
+ it 'suggestion with below param has correct data' do
+ from_line = position.new_line
+ to_line = position.new_line + 3
+
+ expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only below\n",
+ lines_above: 0,
+ lines_below: 3)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index f69cb502ca6..a7cb0bb2a87 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -19,6 +19,24 @@ describe Gitlab::EtagCaching::Router do
expect(result.name).to eq 'issue_title'
end
+ it 'matches with a project name that includes a suffix of create' do
+ result = described_class.match(
+ '/group/test-create/issues/123/realtime_changes'
+ )
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_title'
+ end
+
+ it 'matches with a project name that includes a prefix of create' do
+ result = described_class.match(
+ '/group/create-test/issues/123/realtime_changes'
+ )
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_title'
+ end
+
it 'matches project pipelines endpoint' do
result = described_class.match(
'/my-group/my-project/pipelines.json'
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 4a4ac833e39..507bf222810 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -113,13 +113,13 @@ describe Gitlab::Git::Commit, :seed_helper do
context 'Class methods' do
shared_examples '.find' do
- it "should return first head commit if without params" do
+ it "returns first head commit if without params" do
expect(described_class.last(repository).id).to eq(
rugged_repo.head.target.oid
)
end
- it "should return valid commit" do
+ it "returns valid commit" do
expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
end
@@ -127,21 +127,21 @@ describe Gitlab::Git::Commit, :seed_helper do
expect(described_class.find(repository, SeedRepo::Commit::ID).parent_ids).to be_an(Array)
end
- it "should return valid commit for tag" do
+ it "returns valid commit for tag" do
expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
end
- it "should return nil for non-commit ids" do
+ it "returns nil for non-commit ids" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
expect(described_class.find(repository, blob.id)).to be_nil
end
- it "should return nil for parent of non-commit object" do
+ it "returns nil for parent of non-commit object" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
expect(described_class.find(repository, "#{blob.id}^")).to be_nil
end
- it "should return nil for nonexisting ids" do
+ it "returns nil for nonexisting ids" do
expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
@@ -328,7 +328,7 @@ describe Gitlab::Git::Commit, :seed_helper do
end
describe '.find_all' do
- it 'should return a return a collection of commits' do
+ it 'returns a return a collection of commits' do
commits = described_class.find_all(repository)
expect(commits).to all( be_a_kind_of(described_class) )
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 1d22329b670..9ab669ad488 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -182,7 +182,7 @@ EOT
context "without default options" do
let(:filtered_options) { described_class.filter_diff_options(options) }
- it "should filter invalid options" do
+ it "filters invalid options" do
expect(filtered_options).not_to have_key(:invalid_opt)
end
end
@@ -193,16 +193,16 @@ EOT
described_class.filter_diff_options(options, default_options)
end
- it "should filter invalid options" do
+ it "filters invalid options" do
expect(filtered_options).not_to have_key(:invalid_opt)
expect(filtered_options).not_to have_key(:bad_opt)
end
- it "should merge with default options" do
+ it "merges with default options" do
expect(filtered_options).to have_key(:ignore_whitespace_change)
end
- it "should override default options" do
+ it "overrides default options" do
expect(filtered_options).to have_key(:max_files)
expect(filtered_options[:max_files]).to eq(100)
end
diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
index 6fd2b33486b..de81dcd227d 100644
--- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb
+++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::GitmodulesParser do
- it 'should parse a .gitmodules file correctly' do
+ it 'parses a .gitmodules file correctly' do
data = <<~GITMODULES
[submodule "vendor/libgit2"]
path = vendor/libgit2
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 8ba6862392c..fdb43d1221a 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -152,13 +152,14 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:append_sha) { true }
let(:ref) { 'master' }
let(:format) { nil }
+ let(:path) { nil }
let(:expected_extension) { 'tar.gz' }
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
- subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) }
+ subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
it 'sets CommitId to the commit SHA' do
expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
@@ -176,6 +177,14 @@ describe Gitlab::Git::Repository, :seed_helper do
expect(metadata['ArchivePath']).to eq(expected_path)
end
+ context 'path is set' do
+ let(:path) { 'foo/bar' }
+
+ it 'appends the path to the prefix' do
+ expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar")
+ end
+ end
+
context 'append_sha varies archive path and filename' do
where(:append_sha, :ref, :expected_prefix) do
sha = SeedRepo::LastCommit::ID
@@ -441,20 +450,20 @@ describe Gitlab::Git::Repository, :seed_helper do
ensure_seeds
end
- it "should create a new branch" do
+ it "creates a new branch" do
expect(repository.create_branch('new_branch', 'master')).not_to be_nil
end
- it "should create a new branch with the right name" do
+ it "creates a new branch with the right name" do
expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch')
end
- it "should fail if we create an existing branch" do
+ it "fails if we create an existing branch" do
repository.create_branch('duplicated_branch', 'master')
expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
end
- it "should fail if we create a branch from a non existing ref" do
+ it "fails if we create a branch from a non existing ref" do
expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
end
end
@@ -513,7 +522,7 @@ describe Gitlab::Git::Repository, :seed_helper do
describe "#refs_hash" do
subject { repository.refs_hash }
- it "should have as many entries as branches and tags" do
+ it "has as many entries as branches and tags" do
expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
# We flatten in case a commit is pointed at by more than one branch and/or tag
expect(subject.values.flatten.size).to eq(expected_refs.size)
@@ -604,11 +613,11 @@ describe Gitlab::Git::Repository, :seed_helper do
end
shared_examples 'search files by content' do
- it 'should have 2 items' do
+ it 'has 2 items' do
expect(search_results.size).to eq(2)
end
- it 'should have the correct matching line' do
+ it 'has the correct matching line' do
expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n",
"search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n")
end
@@ -841,7 +850,7 @@ describe Gitlab::Git::Repository, :seed_helper do
context "where provides 'after' timestamp" do
options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
- it "should returns commits on or after that timestamp" do
+ it "returns commits on or after that timestamp" do
commits = repository.log(options)
expect(commits.size).to be > 0
@@ -854,7 +863,7 @@ describe Gitlab::Git::Repository, :seed_helper do
context "where provides 'before' timestamp" do
options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
- it "should returns commits on or before that timestamp" do
+ it "returns commits on or before that timestamp" do
commits = repository.log(options)
expect(commits.size).to be > 0
@@ -1055,14 +1064,14 @@ describe Gitlab::Git::Repository, :seed_helper do
end
describe '#find_branch' do
- it 'should return a Branch for master' do
+ it 'returns a Branch for master' do
branch = repository.find_branch('master')
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
- it 'should handle non-existent branch' do
+ it 'handles non-existent branch' do
branch = repository.find_branch('this-is-garbage')
expect(branch).to eq(nil)
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
new file mode 100644
index 00000000000..6114aca0616
--- /dev/null
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Also see spec/graphql/features/authorization_spec.rb for
+# integration tests of AuthorizeFieldService
+describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
+ describe '#build_checker' do
+ let(:current_user) { double(:current_user) }
+ let(:abilities) { [double(:first_ability), double(:last_ability)] }
+
+ context 'when authorizing against the object' do
+ let(:checker) do
+ service = described_class.new(double(resolve_proc: proc {}))
+ allow(service).to receive(:authorizations).and_return(abilities)
+ service.__send__(:build_checker, current_user, nil)
+ end
+
+ it 'returns a checker which checks for a single object' do
+ object = double(:object)
+
+ abilities.each do |ability|
+ spy_ability_check_for(ability, object, passed: true)
+ end
+
+ expect(checker.call(object)).to eq(object)
+ end
+
+ it 'returns a checker which checks for all objects' do
+ objects = [double(:first), double(:last)]
+
+ abilities.each do |ability|
+ objects.each do |object|
+ spy_ability_check_for(ability, object, passed: true)
+ end
+ end
+
+ expect(checker.call(objects)).to eq(objects)
+ end
+
+ context 'when some objects would not pass the check' do
+ it 'returns nil when it is single object' do
+ disallowed = double(:object)
+
+ spy_ability_check_for(abilities.first, disallowed, passed: false)
+
+ expect(checker.call(disallowed)).to be_nil
+ end
+
+ it 'returns only objects which passed when there are more than one' do
+ allowed = double(:allowed)
+ disallowed = double(:disallowed)
+
+ spy_ability_check_for(abilities.first, disallowed, passed: false)
+
+ abilities.each do |ability|
+ spy_ability_check_for(ability, allowed, passed: true)
+ end
+
+ expect(checker.call([disallowed, allowed])).to contain_exactly(allowed)
+ end
+ end
+ end
+
+ context 'when authorizing against another object' do
+ let(:authorizing_obj) { double(:object) }
+
+ let(:checker) do
+ service = described_class.new(double(resolve_proc: proc {}))
+ allow(service).to receive(:authorizations).and_return(abilities)
+ service.__send__(:build_checker, current_user, authorizing_obj)
+ end
+
+ it 'returns a checker which checks for a single object' do
+ object = double(:object)
+
+ abilities.each do |ability|
+ spy_ability_check_for(ability, authorizing_obj, passed: true)
+ end
+
+ expect(checker.call(object)).to eq(object)
+ end
+
+ it 'returns a checker which checks for all objects' do
+ objects = [double(:first), double(:last)]
+
+ abilities.each do |ability|
+ objects.each do |object|
+ spy_ability_check_for(ability, authorizing_obj, passed: true)
+ end
+ end
+
+ expect(checker.call(objects)).to eq(objects)
+ end
+ end
+ end
+
+ private
+
+ def spy_ability_check_for(ability, object, passed: true)
+ expect(Ability)
+ .to receive(:allowed?)
+ .with(current_user, ability, object)
+ .and_return(passed)
+ end
+end
diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb
deleted file mode 100644
index cf3a8bcc8b4..00000000000
--- a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Graphql::Authorize::Instrumentation do
- describe '#build_checker' do
- let(:current_user) { double(:current_user) }
- let(:abilities) { [double(:first_ability), double(:last_ability)] }
-
- let(:checker) do
- described_class.new.__send__(:build_checker, current_user, abilities)
- end
-
- it 'returns a checker which checks for a single object' do
- object = double(:object)
-
- abilities.each do |ability|
- spy_ability_check_for(ability, object, passed: true)
- end
-
- expect(checker.call(object)).to eq(object)
- end
-
- it 'returns a checker which checks for all objects' do
- objects = [double(:first), double(:last)]
-
- abilities.each do |ability|
- objects.each do |object|
- spy_ability_check_for(ability, object, passed: true)
- end
- end
-
- expect(checker.call(objects)).to eq(objects)
- end
-
- context 'when some objects would not pass the check' do
- it 'returns nil when it is single object' do
- disallowed = double(:object)
-
- spy_ability_check_for(abilities.first, disallowed, passed: false)
-
- expect(checker.call(disallowed)).to be_nil
- end
-
- it 'returns only objects which passed when there are more than one' do
- allowed = double(:allowed)
- disallowed = double(:disallowed)
-
- spy_ability_check_for(abilities.first, disallowed, passed: false)
-
- abilities.each do |ability|
- spy_ability_check_for(ability, allowed, passed: true)
- end
-
- expect(checker.call([disallowed, allowed]))
- .to contain_exactly(allowed)
- end
- end
-
- def spy_ability_check_for(ability, object, passed: true)
- expect(Ability)
- .to receive(:allowed?)
- .with(current_user, ability, object)
- .and_return(passed)
- end
- end
-end
diff --git a/spec/lib/gitlab/graphql/tracing_spec.rb b/spec/lib/gitlab/graphql/tracing_spec.rb
new file mode 100644
index 00000000000..7300a9a572e
--- /dev/null
+++ b/spec/lib/gitlab/graphql/tracing_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Tracing do
+ let(:graphql_duration_seconds_histogram) { double('Gitlab::Metrics::NullMetric') }
+
+ it 'updates graphql histogram with expected labels' do
+ query = 'query { users { id } }'
+ tracer = described_class.new
+
+ allow(tracer)
+ .to receive(:graphql_duration_seconds)
+ .and_return(graphql_duration_seconds_histogram)
+
+ expect_metric('graphql.lex', 'lex')
+ expect_metric('graphql.parse', 'parse')
+ expect_metric('graphql.validate', 'validate')
+ expect_metric('graphql.analyze', 'analyze_multiplex')
+ expect_metric('graphql.execute', 'execute_query_lazy')
+ expect_metric('graphql.execute', 'execute_multiplex')
+
+ GitlabSchema.execute(query, context: { tracers: [tracer] })
+ end
+
+ private
+
+ def expect_metric(platform_key, key)
+ expect(graphql_duration_seconds_histogram)
+ .to receive(:observe)
+ .with({ platform_key: platform_key, key: key }, be > 0.0)
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb
index 4a669408025..e1106f7496a 100644
--- a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb
+++ b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ClusterRoleBinding do
subject { cluster_role_binding.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb
index fe65d03875f..911d6024804 100644
--- a/spec/lib/gitlab/kubernetes/config_map_spec.rb
+++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Kubernetes::ConfigMap do
let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) }
subject { config_map.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
index aacae78be43..78a4eb44e38 100644
--- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
describe '#pod_resource' do
subject { base_command.pod_resource }
- it 'should returns a kubeclient resoure with pod content for application' do
+ it 'returns a kubeclient resoure with pod content for application' do
is_expected.to be_an_instance_of ::Kubeclient::Resource
end
diff --git a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb
index 167bee22fc3..04649353976 100644
--- a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Kubernetes::Helm::Certificate do
describe '.generate_root' do
subject { described_class.generate_root }
- it 'should generate a root CA that expires a long way in the future' do
+ it 'generates a root CA that expires a long way in the future' do
expect(subject.cert.not_after).to be > 999.years.from_now
end
end
@@ -13,14 +13,14 @@ describe Gitlab::Kubernetes::Helm::Certificate do
describe '#issue' do
subject { described_class.generate_root.issue }
- it 'should generate a cert that expires soon' do
+ it 'generates a cert that expires soon' do
expect(subject.cert.not_after).to be < 60.minutes.from_now
end
context 'passing in INFINITE_EXPIRY' do
subject { described_class.generate_root.issue(expires_in: described_class::INFINITE_EXPIRY) }
- it 'should generate a cert that expires a long way in the future' do
+ it 'generates a cert that expires a long way in the future' do
expect(subject.cert.not_after).to be > 999.years.from_now
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 95b6b3fd953..06c8d127951 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -10,11 +10,11 @@ describe Gitlab::Kubernetes::Helm::Pod do
subject { described_class.new(command, namespace, service_account_name: service_account_name) }
context 'with a command' do
- it 'should generate a Kubeclient::Resource' do
+ it 'generates a Kubeclient::Resource' do
expect(subject.generate).to be_a_kind_of(Kubeclient::Resource)
end
- it 'should generate the appropriate metadata' do
+ it 'generates the appropriate metadata' do
metadata = subject.generate.metadata
expect(metadata.name).to eq("install-#{app.name}")
expect(metadata.namespace).to eq('gitlab-managed-apps')
@@ -22,12 +22,12 @@ describe Gitlab::Kubernetes::Helm::Pod do
expect(metadata.labels['gitlab.org/application']).to eq(app.name)
end
- it 'should generate a container spec' do
+ it 'generates a container spec' do
spec = subject.generate.spec
expect(spec.containers.count).to eq(1)
end
- it 'should generate the appropriate specifications for the container' do
+ it 'generates the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7')
@@ -37,30 +37,30 @@ describe Gitlab::Kubernetes::Helm::Pod do
expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"])
end
- it 'should include a never restart policy' do
+ it 'includes a never restart policy' do
spec = subject.generate.spec
expect(spec.restartPolicy).to eq('Never')
end
- it 'should include volumes for the container' do
+ it 'includes volumes for the container' do
container = subject.generate.spec.containers.first
expect(container.volumeMounts.first['name']).to eq('configuration-volume')
expect(container.volumeMounts.first['mountPath']).to eq("/data/helm/#{app.name}/config")
end
- it 'should include a volume inside the specification' do
+ it 'includes a volume inside the specification' do
spec = subject.generate.spec
expect(spec.volumes.first['name']).to eq('configuration-volume')
end
- it 'should mount configMap specification in the volume' do
+ it 'mounts configMap specification in the volume' do
volume = subject.generate.spec.volumes.first
expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}")
expect(volume.configMap['items'].first['key']).to eq(:'values.yaml')
expect(volume.configMap['items'].first['path']).to eq(:'values.yaml')
end
- it 'should have no serviceAccountName' do
+ it 'has no serviceAccountName' do
spec = subject.generate.spec
expect(spec.serviceAccountName).to be_nil
end
@@ -68,7 +68,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
context 'with a service_account_name' do
let(:service_account_name) { 'sa' }
- it 'should use the serviceAccountName provided' do
+ it 'uses the serviceAccountName provided' do
spec = subject.generate.spec
expect(spec.serviceAccountName).to eq(service_account_name)
end
diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
index a1a59533bfb..50acee254cb 100644
--- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb
+++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do
).generate
end
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/service_account_spec.rb b/spec/lib/gitlab/kubernetes/service_account_spec.rb
index 8da9e932dc3..0d525966d18 100644
--- a/spec/lib/gitlab/kubernetes/service_account_spec.rb
+++ b/spec/lib/gitlab/kubernetes/service_account_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Kubernetes::ServiceAccount do
subject { service_account.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb
index 0773d3d9aec..0d334bed45f 100644
--- a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb
+++ b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ServiceAccountToken do
subject { service_account_token.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
new file mode 100644
index 00000000000..e70fde09edd
--- /dev/null
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -0,0 +1,229 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Transaction do
+ let(:transaction) { described_class.new }
+ let(:metric) { transaction.metrics[0] }
+
+ let(:sensitive_tags) do
+ {
+ path: 'private',
+ branch: 'sensitive'
+ }
+ end
+
+ shared_examples 'tag filter' do |sane_tags|
+ it 'filters potentially sensitive tags' do
+ expect(metric.tags).to eq(sane_tags)
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the duration of a transaction in seconds' do
+ transaction.run { }
+
+ expect(transaction.duration).to be > 0
+ end
+ end
+
+ describe '#allocated_memory' do
+ it 'returns the allocated memory in bytes' do
+ transaction.run { 'a' * 32 }
+
+ expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
+ end
+ end
+
+ describe '#run' do
+ it 'yields the supplied block' do
+ expect { |b| transaction.run(&b) }.to yield_control
+ end
+
+ it 'stores the transaction in the current thread' do
+ transaction.run do
+ expect(described_class.current).to eq(transaction)
+ end
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ transaction.run { }
+
+ expect(described_class.current).to be_nil
+ end
+ end
+
+ describe '#add_metric' do
+ it 'adds a metric to the transaction' do
+ transaction.add_metric('foo', value: 1)
+
+ expect(metric.series).to eq('rails_foo')
+ expect(metric.tags).to eq({})
+ expect(metric.values).to eq(value: 1)
+ end
+
+ context 'with sensitive tags' do
+ before do
+ transaction
+ .add_metric('foo', { value: 1 }, **sensitive_tags.merge(sane: 'yes'))
+ end
+
+ it_behaves_like 'tag filter', sane: 'yes'
+ end
+ end
+
+ describe '#method_call_for' do
+ it 'returns a MethodCall' do
+ method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
+
+ expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
+ end
+ end
+
+ describe '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ values = metric_values(time: 3)
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#set' do
+ it 'sets a value' do
+ transaction.set(:number, 10)
+
+ values = metric_values(number: 10)
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#finish' do
+ it 'tracks the transaction details and submits them to Sidekiq' do
+ expect(transaction).to receive(:track_self)
+ expect(transaction).to receive(:submit)
+
+ transaction.finish
+ end
+ end
+
+ describe '#track_self' do
+ it 'adds a metric for the transaction itself' do
+ values = metric_values
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#submit' do
+ it 'submits the metrics to Sidekiq' do
+ transaction.track_self
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([an_instance_of(Hash)])
+
+ transaction.submit
+ end
+
+ it 'adds the action as a tag for every metric' do
+ allow(transaction)
+ .to receive(:labels)
+ .and_return(controller: 'Foo', action: 'bar')
+
+ transaction.track_self
+
+ hash = {
+ series: 'rails_transactions',
+ tags: { action: 'Foo#bar' },
+ values: metric_values,
+ timestamp: a_kind_of(Integer)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([hash])
+
+ transaction.submit
+ end
+
+ it 'does not add an action tag for events' do
+ allow(transaction)
+ .to receive(:labels)
+ .and_return(controller: 'Foo', action: 'bar')
+
+ transaction.add_event(:meow)
+
+ hash = {
+ series: 'events',
+ tags: { event: :meow },
+ values: { count: 1 },
+ timestamp: a_kind_of(Integer)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([hash])
+
+ transaction.submit
+ end
+ end
+
+ describe '#add_event' do
+ it 'adds a metric' do
+ transaction.add_event(:meow)
+
+ expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
+ end
+
+ it "does not prefix the metric's series name" do
+ transaction.add_event(:meow)
+
+ expect(metric.series).to eq(described_class::EVENT_SERIES)
+ end
+
+ it 'tracks a counter for every event' do
+ transaction.add_event(:meow)
+
+ expect(metric.values).to eq(count: 1)
+ end
+
+ it 'tracks the event name' do
+ transaction.add_event(:meow)
+
+ expect(metric.tags).to eq(event: :meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ transaction.add_event(:meow, animal: 'cat')
+
+ expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ end
+
+ context 'with sensitive tags' do
+ before do
+ transaction.add_event(:meow, **sensitive_tags.merge(sane: 'yes'))
+ end
+
+ it_behaves_like 'tag filter', event: :meow, sane: 'yes'
+ end
+ end
+
+ private
+
+ def metric_values(opts = {})
+ {
+ duration: 0.0,
+ allocated_memory: a_kind_of(Numeric)
+ }.merge(opts)
+ end
+end
diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb
index 4700a7ad2e1..e6e9ae3223e 100644
--- a/spec/lib/gitlab/object_hierarchy_spec.rb
+++ b/spec/lib/gitlab/object_hierarchy_spec.rb
@@ -81,6 +81,24 @@ describe Gitlab::ObjectHierarchy, :postgresql do
expect { relation.update_all(share_with_group_lock: false) }
.to raise_error(ActiveRecord::ReadOnlyRecord)
end
+
+ context 'when with_depth is true' do
+ let(:relation) do
+ described_class.new(Group.where(id: parent.id)).base_and_descendants(with_depth: true)
+ end
+
+ it 'includes depth in the results' do
+ object_depths = {
+ parent.id => 1,
+ child1.id => 2,
+ child2.id => 3
+ }
+
+ relation.each do |object|
+ expect(object.depth).to eq(object_depths[object.id])
+ end
+ end
+ end
end
describe '#descendants' do
@@ -91,6 +109,28 @@ describe Gitlab::ObjectHierarchy, :postgresql do
end
end
+ describe '#max_descendants_depth' do
+ subject { described_class.new(base_relation).max_descendants_depth }
+
+ context 'when base relation is empty' do
+ let(:base_relation) { Group.where(id: nil) }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'when base has no children' do
+ let(:base_relation) { Group.where(id: child2) }
+
+ it { expect(subject).to eq(1) }
+ end
+
+ context 'when base has grandchildren' do
+ let(:base_relation) { Group.where(id: parent) }
+
+ it { expect(subject).to eq(3) }
+ end
+ end
+
describe '#ancestors' do
it 'includes only the ancestors' do
relation = described_class.new(Group.where(id: child2)).ancestors
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 312e5e55af8..71e69a0d418 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -100,7 +100,7 @@ describe Gitlab::PathRegex do
end
let(:ee_top_level_words) do
- ['unsubscribes']
+ %w(unsubscribes v2)
end
let(:files_in_public) do
diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb
index 5a88b23aa82..a6589f0c0a3 100644
--- a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb
@@ -9,9 +9,35 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery do
let(:query_params) { [environment.id] }
it 'queries using specific time' do
- expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f)
-
+ expect(client).to receive(:query_range)
+ .with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f)
expect(query_result).not_to be_nil
end
+
+ context 'when start and end time parameters are provided' do
+ let(:query_params) { [environment.id, start_time, end_time] }
+
+ context 'as unix timestamps' do
+ let(:start_time) { 4.hours.ago.to_f }
+ let(:end_time) { 2.hours.ago.to_f }
+
+ it 'queries using the provided times' do
+ expect(client).to receive(:query_range)
+ .with(anything, start: start_time, stop: end_time)
+ expect(query_result).not_to be_nil
+ end
+ end
+
+ context 'as Date/Time objects' do
+ let(:start_time) { 4.hours.ago }
+ let(:end_time) { 2.hours.ago }
+
+ it 'queries using the provided times converted to unix' do
+ expect(client).to receive(:query_range)
+ .with(anything, start: start_time.to_f, stop: end_time.to_f)
+ expect(query_result).not_to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
new file mode 100644
index 00000000000..7f6283715f2
--- /dev/null
+++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
+ include PrometheusHelpers
+
+ let(:project) { create(:project) }
+ let(:serverless_func) { Serverless::Function.new(project, 'test-name', 'test-ns') }
+
+ let(:client) { double('prometheus_client') }
+ subject { described_class.new(client) }
+
+ context 'verify queries' do
+ before do
+ allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns')))
+ allow(client).to receive(:query_range)
+ end
+
+ it 'has the query, but no data' do
+ results = subject.query(serverless_func.id)
+
+ expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index 4c3b8deefb9..f15ae83a02c 100644
--- a/spec/lib/gitlab/prometheus_client_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -60,15 +60,13 @@ describe Gitlab::PrometheusClient do
end
describe 'failure to reach a provided prometheus url' do
- let(:prometheus_url) {"https://prometheus.invalid.example.com"}
+ let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
- subject { described_class.new(RestClient::Resource.new(prometheus_url)) }
-
- context 'exceptions are raised' do
+ shared_examples 'exceptions are raised' do
it 'raises a Gitlab::PrometheusClient::Error error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested
end
@@ -76,7 +74,7 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusClient::Error error when a SSLError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested
end
@@ -84,11 +82,23 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Network connection error")
expect(req_stub).to have_been_requested
end
end
+
+ context 'ping' do
+ subject { described_class.new(RestClient::Resource.new(prometheus_url)).ping }
+
+ it_behaves_like 'exceptions are raised'
+ end
+
+ context 'proxy' do
+ subject { described_class.new(RestClient::Resource.new(prometheus_url)).proxy('query', { query: '1' }) }
+
+ it_behaves_like 'exceptions are raised'
+ end
end
describe '#query' do
@@ -230,4 +240,87 @@ describe Gitlab::PrometheusClient do
let(:execute_query) { subject.query_range(prometheus_query) }
end
end
+
+ describe '.compute_step' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:now) { Time.now.utc }
+
+ subject { described_class.compute_step(start, stop) }
+
+ where(:time_interval_in_seconds, :step) do
+ 0 | 60
+ 10.hours | 60
+ 10.hours + 1 | 61
+ # frontend options
+ 30.minutes | 60
+ 3.hours | 60
+ 8.hours | 60
+ 1.day | 144
+ 3.days | 432
+ 1.week | 1008
+ end
+
+ with_them do
+ let(:start) { now - time_interval_in_seconds }
+ let(:stop) { now }
+
+ it { is_expected.to eq(step) }
+ end
+ end
+
+ describe 'proxy' do
+ context 'get API' do
+ let(:prometheus_query) { prometheus_cpu_query('env-slug') }
+ let(:query_url) { prometheus_query_url(prometheus_query) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'when response status code is 200' do
+ it 'returns response object' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
+
+ response = subject.proxy('query', { query: prometheus_query })
+ json_response = JSON.parse(response.body)
+
+ expect(response.code).to eq(200)
+ expect(json_response).to eq({
+ 'status' => 'success',
+ 'data' => {
+ 'resultType' => 'vector',
+ 'result' => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }]
+ }
+ })
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when response status code is not 200' do
+ it 'returns response object' do
+ req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'error' })
+
+ response = subject.proxy('query', { query: prometheus_query })
+ json_response = JSON.parse(response.body)
+
+ expect(req_stub).to have_been_requested
+ expect(response.code).to eq(400)
+ expect(json_response).to eq('error' => 'error')
+ end
+ end
+
+ context 'when RestClient::Exception is raised' do
+ before do
+ stub_prometheus_request_with_exception(query_url, RestClient::Exception)
+ end
+
+ it 'raises PrometheusClient::Error' do
+ expect { subject.proxy('query', { query: prometheus_query }) }.to(
+ raise_error(Gitlab::PrometheusClient::Error, 'Network connection error')
+ )
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index 4c7ca4e2b57..8fbda929064 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -44,8 +44,10 @@ describe ::Gitlab::RepoPath do
end
end
- it "returns nil for non existent paths" do
- expect(described_class.parse("path/non-existent.git")).to eq(nil)
+ it "returns the default type for non existent paths" do
+ _project, type, _redirected = described_class.parse("path/non-existent.git")
+
+ expect(type).to eq(Gitlab::GlRepository.default_type)
end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 4b57eecff93..312aa3be490 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -97,7 +97,7 @@ describe Gitlab::SearchResults do
results.objects('merge_requests')
end
- it 'it skips project filter if default project context is used' do
+ it 'skips project filter if default project context is used' do
allow(results).to receive(:default_project_filter).and_return(true)
expect(results).not_to receive(:project_ids_relation)
@@ -113,7 +113,7 @@ describe Gitlab::SearchResults do
results.objects('issues')
end
- it 'it skips project filter if default project context is used' do
+ it 'skips project filter if default project context is used' do
allow(results).to receive(:default_project_filter).and_return(true)
expect(results).not_to receive(:project_ids_relation)
diff --git a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
index c9d1a06b3e6..0bbaf5968ed 100644
--- a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
+++ b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
@@ -7,19 +7,19 @@ describe Gitlab::Tracing::Rails::ActionViewSubscriber do
using RSpec::Parameterized::TableSyntax
shared_examples 'an actionview notification' do
- it 'should notify the tracer when the hash contains null values' do
+ it 'notifies the tracer when the hash contains null values' do
expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
subject.public_send(notify_method, start, finish, payload)
end
- it 'should notify the tracer when the payload is missing values' do
+ it 'notifies the tracer when the payload is missing values' do
expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
subject.public_send(notify_method, start, finish, payload.compact)
end
- it 'should not throw exceptions when with the default tracer' do
+ it 'does not throw exceptions when with the default tracer' do
expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
index 3d066843148..7bd0875fa68 100644
--- a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
+++ b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
@@ -53,19 +53,19 @@ describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do
}
end
- it 'should notify the tracer when the hash contains null values' do
+ it 'notifies the tracer when the hash contains null values' do
expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
subject.notify(start, finish, payload)
end
- it 'should notify the tracer when the payload is missing values' do
+ it 'notifies the tracer when the payload is missing values' do
expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
subject.notify(start, finish, payload.compact)
end
- it 'should not throw exceptions when with the default tracer' do
+ it 'does not throw exceptions when with the default tracer' do
expect { subject.notify(start, finish, payload) }.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb
index 566b5050e47..db75ce2a998 100644
--- a/spec/lib/gitlab/tracing_spec.rb
+++ b/spec/lib/gitlab/tracing_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Tracing do
end
with_them do
- it 'should return the correct state for .enabled?' do
+ it 'returns the correct state for .enabled?' do
expect(described_class).to receive(:connection_string).and_return(connection_string)
expect(described_class.enabled?).to eq(enabled_state)
@@ -33,7 +33,7 @@ describe Gitlab::Tracing do
end
with_them do
- it 'should return the correct state for .tracing_url_enabled?' do
+ it 'returns the correct state for .tracing_url_enabled?' do
expect(described_class).to receive(:enabled?).and_return(enabled?)
allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template)
@@ -56,7 +56,7 @@ describe Gitlab::Tracing do
end
with_them do
- it 'should return the correct state for .tracing_url' do
+ it 'returns the correct state for .tracing_url' do
expect(described_class).to receive(:tracing_url_enabled?).and_return(tracing_url_enabled?)
allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template)
allow(Gitlab::CorrelationId).to receive(:current_id).and_return(correlation_id)
diff --git a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
index 005d41580de..f1882e03581 100644
--- a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
@@ -1,5 +1,6 @@
require 'fast_spec_helper'
require 'support/shared_examples/malicious_regexp_shared_examples'
+require 'support/helpers/stub_feature_flags'
describe Gitlab::UntrustedRegexp::RubySyntax do
describe '.matches_syntax?' do
@@ -33,6 +34,12 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
end
end
+ context 'when regexp is empty' do
+ it 'fabricates regexp correctly' do
+ expect(described_class.fabricate('//')).not_to be_nil
+ end
+ end
+
context 'when regexp is a raw pattern' do
it 'returns error' do
expect(described_class.fabricate('some .* thing')).to be_nil
@@ -41,24 +48,63 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
end
describe '.fabricate!' do
- context 'when regexp is using /regexp/ scheme with flags' do
- it 'fabricates regexp with a single flag' do
- regexp = described_class.fabricate!('/something/i')
+ context 'safe regexp is used' do
+ context 'when regexp is using /regexp/ scheme with flags' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate!('/something/i')
+
+ expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?i)something')
+ expect(regexp.scan('SOMETHING')).to be_one
+ end
- expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?i)something')
- expect(regexp.scan('SOMETHING')).to be_one
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate!('/something/im')
+
+ expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?im)something')
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate!('/something/')
+
+ expect(regexp).to eq Gitlab::UntrustedRegexp.new('something')
+ end
end
+ end
- it 'fabricates regexp with multiple flags' do
- regexp = described_class.fabricate!('/something/im')
+ context 'when unsafe regexp is used' do
+ include StubFeatureFlags
- expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?im)something')
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+
+ allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError)
end
- it 'fabricates regexp without flags' do
- regexp = described_class.fabricate!('/something/')
+ context 'when no fallback is enabled' do
+ it 'raises an exception' do
+ expect { described_class.fabricate!('/something/') }
+ .to raise_error(RegexpError)
+ end
+ end
+
+ context 'when fallback is used' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate!('/something/i', fallback: true)
+
+ expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE)
+ end
+
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate!('/something/im', fallback: true)
+
+ expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE | Regexp::MULTILINE)
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate!('/something/', fallback: true)
- expect(regexp).to eq Gitlab::UntrustedRegexp.new('something')
+ expect(regexp).to eq Regexp.new('something')
+ end
end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 6e98a999766..5861e6955a6 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -161,7 +161,7 @@ describe Gitlab::UrlSanitizer do
end
context 'when credentials contains special chars' do
- it 'should parse the URL without errors' do
+ it 'parses the URL without errors' do
url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git")
expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git")
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d88086b01b1..f8ce399287a 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -16,20 +16,12 @@ describe Gitlab::Workhorse do
let(:ref) { 'master' }
let(:format) { 'zip' }
let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
- let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) }
- let(:gitaly_params) do
- base_params.merge(
- 'GitalyServer' => {
- 'address' => Gitlab::GitalyClient.address(project.repository_storage),
- 'token' => Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys
- )
- end
+ let(:path) { 'some/path' }
+ let(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: nil, path: path) }
let(:cache_disabled) { false }
subject do
- described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil)
+ described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil, path: path)
end
before do
@@ -41,7 +33,22 @@ describe Gitlab::Workhorse do
expect(key).to eq('Gitlab-Workhorse-Send-Data')
expect(command).to eq('git-archive')
- expect(params).to include(gitaly_params)
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => Base64.urlsafe_encode64(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: Gitaly::GetArchiveRequest::Format::ZIP,
+ path: path
+ ).to_proto
+ )
+ }.deep_stringify_keys)
end
context 'when archive caching is disabled' do
@@ -87,7 +94,7 @@ describe Gitlab::Workhorse do
end
end
- describe '.terminal_websocket' do
+ describe '.channel_websocket' do
def terminal(ca_pem: nil)
out = {
subprotocols: ['foo'],
@@ -101,25 +108,25 @@ describe Gitlab::Workhorse do
def workhorse(ca_pem: nil)
out = {
- 'Terminal' => {
+ 'Channel' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
'Header' => { 'Authorization' => ['Token x'] },
'MaxSessionTime' => 600
}
}
- out['Terminal']['CAPem'] = ca_pem if ca_pem
+ out['Channel']['CAPem'] = ca_pem if ca_pem
out
end
context 'without ca_pem' do
- subject { described_class.terminal_websocket(terminal) }
+ subject { described_class.channel_websocket(terminal) }
it { is_expected.to eq(workhorse) }
end
context 'with ca_pem' do
- subject { described_class.terminal_websocket(terminal(ca_pem: "foo")) }
+ subject { described_class.channel_websocket(terminal(ca_pem: "foo")) }
it { is_expected.to eq(workhorse(ca_pem: "foo")) }
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 3c8897ed37c..5fa1369c00a 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -30,6 +30,19 @@ describe Notify do
description: 'My awesome description!')
end
+ describe 'with HTML-encoded entities' do
+ before do
+ described_class.test_email('test@test.com', 'Subject', 'Some body with &mdash;').deliver
+ end
+
+ subject { ActionMailer::Base.deliveries.last }
+
+ it 'retains 7bit encoding' do
+ expect(subject.body.ascii_only?).to eq(true)
+ expect(subject.body.encoding).to eq('7bit')
+ end
+ end
+
context 'for a project' do
shared_examples 'an assignee email' do
it 'is sent to the assignee as the author' do
diff --git a/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
new file mode 100644
index 00000000000..572b7dfd0c8
--- /dev/null
+++ b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190313092516_clean_up_noteable_id_for_notes_on_commits.rb')
+
+describe CleanUpNoteableIdForNotesOnCommits, :migration do
+ let(:notes) { table(:notes) }
+
+ before do
+ notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test')
+ notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test')
+ notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test')
+
+ notes.create!(noteable_type: 'Issue', noteable_id: 1, note: 'Test')
+ notes.create!(noteable_type: 'MergeRequest', noteable_id: 1, note: 'Test')
+ notes.create!(noteable_type: 'Snippet', noteable_id: 1, note: 'Test')
+ end
+
+ it 'clears noteable_id for notes on commits' do
+ expect { migrate! }.to change { dirty_notes_on_commits.count }.from(3).to(0)
+ end
+
+ it 'does not clear noteable_id for other notes' do
+ expect { migrate! }.not_to change { other_notes.count }
+ end
+
+ def dirty_notes_on_commits
+ notes.where(noteable_type: 'Commit').where('noteable_id IS NOT NULL')
+ end
+
+ def other_notes
+ notes.where("noteable_type != 'Commit' AND noteable_id IS NOT NULL")
+ end
+end
diff --git a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
index b1ff3cfd355..349cffea70e 100644
--- a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
+++ b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
@@ -25,7 +25,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
context 'with ProjectAutoDevOps with no domain' do
let(:domain) { nil }
- it 'should not update cluster project' do
+ it 'does not update cluster project' do
migrate!
expect(clusters_without_domain.count).to eq(clusters_table.count)
@@ -35,7 +35,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
context 'with ProjectAutoDevOps with domain' do
let(:domain) { 'example-domain.com' }
- it 'should update all cluster projects' do
+ it 'updates all cluster projects' do
migrate!
expect(clusters_with_domain.count).to eq(clusters_table.count)
@@ -49,7 +49,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
setup_cluster_projects_with_domain(quantity: 25, domain: nil)
end
- it 'should only update specific cluster projects' do
+ it 'only updates specific cluster projects' do
migrate!
expect(clusters_with_domain.count).to eq(20)
diff --git a/spec/migrations/schedule_runners_token_encryption_spec.rb b/spec/migrations/schedule_runners_token_encryption_spec.rb
index 376d2795277..97ff6c128f3 100644
--- a/spec/migrations/schedule_runners_token_encryption_spec.rb
+++ b/spec/migrations/schedule_runners_token_encryption_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20181121111200_schedule_runners_token_encryption')
-describe ScheduleRunnersTokenEncryption, :migration do
+describe ScheduleRunnersTokenEncryption, :migration, :sidekiq do
let(:settings) { table(:application_settings) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
index bf974d60b24..bc94f8820bd 100644
--- a/spec/migrations/schedule_sync_issuables_state_id_spec.rb
+++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190214112022_schedule_sync_issuables_state_id.rb')
-describe ScheduleSyncIssuablesStateId, :migration do
+describe ScheduleSyncIssuablesStateId, :migration, :sidekiq do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:merge_requests) { table(:merge_requests) }
diff --git a/spec/migrations/truncate_user_fullname_spec.rb b/spec/migrations/truncate_user_fullname_spec.rb
new file mode 100644
index 00000000000..17fd4d9f688
--- /dev/null
+++ b/spec/migrations/truncate_user_fullname_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190325080727_truncate_user_fullname.rb')
+
+describe TruncateUserFullname, :migration do
+ let(:users) { table(:users) }
+
+ let(:user_short) { create_user(name: 'abc', email: 'test_short@example.com') }
+ let(:user_long) { create_user(name: 'a' * 200 + 'z', email: 'test_long@example.com') }
+
+ def create_user(params)
+ users.create!(params.merge(projects_limit: 0))
+ end
+
+ it 'truncates user full name to the first 128 characters' do
+ expect { migrate! }.to change { user_long.reload.name }.to('a' * 128)
+ end
+
+ it 'does not truncate short names' do
+ expect { migrate! }.not_to change { user_short.reload.name.length }
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c5579dafb4a..c81572d739e 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -6,6 +6,7 @@ describe ApplicationSetting do
let(:setting) { described_class.create_from_defaults }
it { include(CacheableAttributes) }
+ it { include(ApplicationSettingImplementation) }
it { expect(described_class.current_without_cache).to eq(described_class.last) }
it { expect(setting).to be_valid }
@@ -286,12 +287,10 @@ describe ApplicationSetting do
end
context 'restrict creating duplicates' do
- before do
- described_class.create_from_defaults
- end
+ let!(:current_settings) { described_class.create_from_defaults }
- it 'raises an record creation violation if already created' do
- expect { described_class.create_from_defaults }.to raise_error(ActiveRecord::RecordNotUnique)
+ it 'returns the current settings' do
+ expect(described_class.create_from_defaults).to eq(current_settings)
end
end
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
index 314d7d1e9f4..c661f5384ea 100644
--- a/spec/models/badge_spec.rb
+++ b/spec/models/badge_spec.rb
@@ -61,7 +61,7 @@ describe Badge do
end
shared_examples 'rendered_links' do
- it 'should use the project information to populate the url placeholders' do
+ it 'uses the project information to populate the url placeholders' do
stub_project_commit_info(project)
expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb
index e683d110252..d41c5cf2ca1 100644
--- a/spec/models/badges/project_badge_spec.rb
+++ b/spec/models/badges/project_badge_spec.rb
@@ -14,7 +14,7 @@ describe ProjectBadge do
end
shared_examples 'rendered_links' do
- it 'should use the badge project information to populate the url placeholders' do
+ it 'uses the badge project information to populate the url placeholders' do
stub_project_commit_info(project)
expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index a52c10019e6..e51fd009f50 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -13,25 +13,33 @@ describe Ci::BuildRunnerSession, model: true do
it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') }
describe '#terminal_specification' do
- let(:terminal_specification) { subject.terminal_specification }
+ let(:specification) { subject.terminal_specification }
+
+ it 'returns terminal.gitlab.com protocol' do
+ expect(specification[:subprotocols]).to eq ['terminal.gitlab.com']
+ end
+
+ it 'returns a wss url' do
+ expect(specification[:url]).to start_with('wss://')
+ end
it 'returns empty hash if no url' do
subject.url = ''
- expect(terminal_specification).to be_empty
+ expect(specification).to be_empty
end
context 'when url is present' do
it 'returns ca_pem nil if empty certificate' do
subject.certificate = ''
- expect(terminal_specification[:ca_pem]).to be_nil
+ expect(specification[:ca_pem]).to be_nil
end
it 'adds Authorization header if authorization is present' do
subject.authorization = 'whatever'
- expect(terminal_specification[:headers]).to include(Authorization: ['whatever'])
+ expect(specification[:headers]).to include(Authorization: ['whatever'])
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2c41dfa65cd..1352a2de2d7 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -117,6 +117,16 @@ describe Ci::Build do
it 'returns the job' do
is_expected.to include(job)
end
+
+ context 'when ci_enable_legacy_artifacts feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_enable_legacy_artifacts: false)
+ end
+
+ it 'does not return the job' do
+ is_expected.not_to include(job)
+ end
+ end
end
context 'when job has a job artifact archive' do
@@ -471,6 +481,14 @@ describe Ci::Build do
let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_truthy }
+
+ context 'when ci_enable_legacy_artifacts feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_enable_legacy_artifacts: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
end
end
end
@@ -2708,13 +2726,13 @@ describe Ci::Build do
project.deploy_tokens << deploy_token
end
- it 'should include deploy token variables' do
+ it 'includes deploy token variables' do
is_expected.to include(*deploy_token_variables)
end
end
context 'when gitlab-deploy-token does not exist' do
- it 'should not include deploy token variables' do
+ it 'does not include deploy token variables' do
expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil
expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil
end
@@ -3198,7 +3216,7 @@ describe Ci::Build do
it 'does not try to create a todo' do
project.add_developer(user)
- expect(service).not_to receive(:commit_status_merge_requests)
+ expect(service).not_to receive(:pipeline_merge_requests)
subject.drop!
end
@@ -3234,7 +3252,23 @@ describe Ci::Build do
end
context 'when build is not configured to be retried' do
- subject { create(:ci_build, :running, project: project, user: user) }
+ subject { create(:ci_build, :running, project: project, user: user, pipeline: pipeline) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let(:merge_request) do
+ create(:merge_request, :opened,
+ source_branch: 'feature',
+ source_project: project,
+ target_branch: 'master',
+ target_project: project)
+ end
it 'does not retry build' do
expect(described_class).not_to receive(:retry)
@@ -3253,7 +3287,10 @@ describe Ci::Build do
it 'creates a todo' do
project.add_developer(user)
- expect(service).to receive(:commit_status_merge_requests)
+ expect_next_instance_of(TodoService) do |todo_service|
+ expect(todo_service)
+ .to receive(:merge_request_build_failed).with(merge_request)
+ end
subject.drop!
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index b3ab63925dd..2cb581696a0 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -72,7 +72,7 @@ describe Ci::Runner do
expect(instance_runner.errors.full_messages).to include('Runner cannot have projects assigned')
end
- it 'should fail to save a group assigned to a project runner even if the runner is already saved' do
+ it 'fails to save a group assigned to a project runner even if the runner is already saved' do
group_runner
expect { create(:group, runners: [project_runner]) }
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index af7eadfc74c..5cd80edb3a1 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -36,7 +36,7 @@ describe Clusters::Applications::CertManager do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with cert_manager arguments' do
+ it 'is initialized with cert_manager arguments' do
expect(subject.name).to eq('certmanager')
expect(subject.chart).to eq('stable/cert-manager')
expect(subject.version).to eq('v0.5.2')
@@ -52,7 +52,7 @@ describe Clusters::Applications::CertManager do
cert_manager.email = cert_email
end
- it 'should use his/her email to register issuer with certificate provider' do
+ it 'uses his/her email to register issuer with certificate provider' do
expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
end
end
@@ -68,7 +68,7 @@ describe Clusters::Applications::CertManager do
context 'application failed to install previously' do
let(:cert_manager) { create(:clusters_applications_cert_managers, :errored, version: '0.0.1') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('v0.5.2')
end
end
@@ -80,7 +80,7 @@ describe Clusters::Applications::CertManager do
subject { application.files }
- it 'should include cert_manager specific keys in the values.yaml file' do
+ it 'includes cert_manager specific keys in the values.yaml file' do
expect(values).to include('ingressShim')
end
end
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index f97d126d918..f177d493a2e 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -36,11 +36,11 @@ describe Clusters::Applications::Helm do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) }
- it 'should be initialized with 1 arguments' do
+ it 'is initialized with 1 arguments' do
expect(subject.name).to eq('helm')
end
- it 'should have cert files' do
+ it 'has cert files' do
expect(subject.files[:'ca.pem']).to be_present
expect(subject.files[:'ca.pem']).to eq(helm.ca_cert)
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 09e60b9a206..113d29b5551 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -73,7 +73,7 @@ describe Clusters::Applications::Ingress do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with ingress arguments' do
+ it 'is initialized with ingress arguments' do
expect(subject.name).to eq('ingress')
expect(subject.chart).to eq('stable/nginx-ingress')
expect(subject.version).to eq('1.1.2')
@@ -92,7 +92,7 @@ describe Clusters::Applications::Ingress do
context 'application failed to install previously' do
let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('1.1.2')
end
end
@@ -104,7 +104,7 @@ describe Clusters::Applications::Ingress do
subject { application.files }
- it 'should include ingress valid keys in values' do
+ it 'includes ingress valid keys in values' do
expect(values).to include('image')
expect(values).to include('repository')
expect(values).to include('stats')
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 5970a1959b5..1a7363b64f9 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -45,7 +45,7 @@ describe Clusters::Applications::Jupyter do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with 4 arguments' do
+ it 'is initialized with 4 arguments' do
expect(subject.name).to eq('jupyter')
expect(subject.chart).to eq('jupyter/jupyterhub')
expect(subject.version).to eq('0.9-174bbd5')
@@ -65,7 +65,7 @@ describe Clusters::Applications::Jupyter do
context 'application failed to install previously' do
let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('0.9-174bbd5')
end
end
@@ -77,7 +77,7 @@ describe Clusters::Applications::Jupyter do
subject { application.files }
- it 'should include valid values' do
+ it 'includes valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
expect(values).to include('rbac')
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index 25493689fbc..5e68f2634da 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -77,17 +77,17 @@ describe Clusters::Applications::Knative do
end
shared_examples 'a command' do
- it 'should be an instance of Helm::InstallCommand' do
+ it 'is an instance of Helm::InstallCommand' do
expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
end
- it 'should be initialized with knative arguments' do
+ it 'is initialized with knative arguments' do
expect(subject.name).to eq('knative')
expect(subject.chart).to eq('knative/knative')
expect(subject.files).to eq(knative.files)
end
- it 'should not install metrics for prometheus' do
+ it 'does not install metrics for prometheus' do
expect(subject.postinstall).to be_nil
end
@@ -97,7 +97,7 @@ describe Clusters::Applications::Knative do
subject { knative.install_command }
- it 'should install metrics' do
+ it 'installs metrics' do
expect(subject.postinstall).not_to be_nil
expect(subject.postinstall.length).to be(1)
expect(subject.postinstall[0]).to eql("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
@@ -108,7 +108,7 @@ describe Clusters::Applications::Knative do
describe '#install_command' do
subject { knative.install_command }
- it 'should be initialized with latest version' do
+ it 'is initialized with latest version' do
expect(subject.version).to eq('0.3.0')
end
@@ -119,7 +119,7 @@ describe Clusters::Applications::Knative do
let!(:current_installed_version) { knative.version = '0.1.0' }
subject { knative.update_command }
- it 'should be initialized with current version' do
+ it 'is initialized with current version' do
expect(subject.version).to eq(current_installed_version)
end
@@ -132,7 +132,7 @@ describe Clusters::Applications::Knative do
subject { application.files }
- it 'should include knative specific keys in the values.yaml file' do
+ it 'includes knative specific keys in the values.yaml file' do
expect(values).to include('domain')
end
end
@@ -165,7 +165,7 @@ describe Clusters::Applications::Knative do
synchronous_reactive_cache(knative)
end
- it 'should be able k8s core for pod details' do
+ it 'is able k8s core for pod details' do
expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
end
end
@@ -190,7 +190,7 @@ describe Clusters::Applications::Knative do
stub_kubeclient_service_pods
end
- it 'should have an unintialized cache' do
+ it 'has an unintialized cache' do
is_expected.to be_nil
end
@@ -204,11 +204,11 @@ describe Clusters::Applications::Knative do
synchronous_reactive_cache(knative)
end
- it 'should have cached services' do
+ it 'has cached services' do
is_expected.not_to be_nil
end
- it 'should match our namespace' do
+ it 'matches our namespace' do
expect(knative.services_for(ns: namespace)).not_to be_nil
end
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 82a502addd4..e8ba9737c23 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -94,7 +94,7 @@ describe Clusters::Applications::Prometheus do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with 3 arguments' do
+ it 'is initialized with 3 arguments' do
expect(subject.name).to eq('prometheus')
expect(subject.chart).to eq('stable/prometheus')
expect(subject.version).to eq('6.7.3')
@@ -113,12 +113,12 @@ describe Clusters::Applications::Prometheus do
context 'application failed to install previously' do
let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('6.7.3')
end
end
- it 'should not install knative metrics' do
+ it 'does not install knative metrics' do
expect(subject.postinstall).to be_nil
end
@@ -128,7 +128,7 @@ describe Clusters::Applications::Prometheus do
subject { prometheus.install_command }
- it 'should install knative metrics' do
+ it 'installs knative metrics' do
expect(subject.postinstall).to include("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
end
end
@@ -142,7 +142,7 @@ describe Clusters::Applications::Prometheus do
expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::InstallCommand)
end
- it 'should be initialized with 3 arguments' do
+ it 'is initialized with 3 arguments' do
command = prometheus.upgrade_command(values)
expect(command.name).to eq('prometheus')
@@ -180,7 +180,7 @@ describe Clusters::Applications::Prometheus do
subject { application.files }
- it 'should include prometheus valid values' do
+ it 'includes prometheus valid values' do
expect(values).to include('alertmanager')
expect(values).to include('kubeStateMetrics')
expect(values).to include('nodeExporter')
@@ -204,7 +204,7 @@ describe Clusters::Applications::Prometheus do
expect(subject[:'values.yaml']).to eq({ hello: :world })
end
- it 'should include cert files' do
+ it 'includes cert files' do
expect(subject[:'ca.pem']).to be_present
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
@@ -220,7 +220,7 @@ describe Clusters::Applications::Prometheus do
application.cluster.application_helm.ca_cert = nil
end
- it 'should not include cert files' do
+ it 'does not include cert files' do
expect(subject[:'ca.pem']).not_to be_present
expect(subject[:'cert.pem']).not_to be_present
expect(subject[:'key.pem']).not_to be_present
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 7e2f5835279..399a13f82cb 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -21,7 +21,7 @@ describe Clusters::Applications::Runner do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with 4 arguments' do
+ it 'is initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
expect(subject.version).to eq('0.3.0')
@@ -41,7 +41,7 @@ describe Clusters::Applications::Runner do
context 'application failed to install previously' do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('0.3.0')
end
end
@@ -53,7 +53,7 @@ describe Clusters::Applications::Runner do
subject { application.files }
- it 'should include runner valid values' do
+ it 'includes runner valid values' do
expect(values).to include('concurrent')
expect(values).to include('checkInterval')
expect(values).to include('rbac')
@@ -131,7 +131,7 @@ describe Clusters::Applications::Runner do
allow(application).to receive(:chart_values).and_return(stub_values)
end
- it 'should overwrite values.yaml' do
+ it 'overwrites values.yaml' do
expect(values).to match(/privileged: '?#{application.privileged}/)
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index fabd2806d9a..894ef3fb956 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -269,7 +269,7 @@ describe Clusters::Cluster do
context 'when cluster is not a valid hostname' do
let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') }
- it 'should add an error on domain' do
+ it 'adds an error on domain' do
expect(subject).not_to be_valid
expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])')
end
@@ -599,7 +599,7 @@ describe Clusters::Cluster do
stub_application_setting(auto_devops_domain: 'global_domain.com')
end
- it 'should include KUBE_INGRESS_BASE_DOMAIN' do
+ it 'includes KUBE_INGRESS_BASE_DOMAIN' do
expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'global_domain.com')
end
end
@@ -607,7 +607,7 @@ describe Clusters::Cluster do
context 'with a cluster domain' do
let(:cluster) { create(:cluster, :provided_by_gcp, domain: 'example.com') }
- it 'should include KUBE_INGRESS_BASE_DOMAIN' do
+ it 'includes KUBE_INGRESS_BASE_DOMAIN' do
expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'example.com')
end
end
@@ -615,7 +615,7 @@ describe Clusters::Cluster do
context 'with no domain' do
let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
- it 'should return an empty array' do
+ it 'returns an empty array' do
expect(subject.to_hash).to be_empty
end
end
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index 579f486f99f..b5cba80b806 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
context 'when platform has a namespace assigned' do
let(:namespace) { 'platform-namespace' }
- it 'should copy the namespace' do
+ it 'copies the namespace' do
subject
expect(kubernetes_namespace.namespace).to eq('platform-namespace')
@@ -72,7 +72,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
let(:namespace) { nil }
let(:project_slug) { "#{project.path}-#{project.id}" }
- it 'should fallback to project namespace' do
+ it 'fallbacks to project namespace' do
subject
expect(kubernetes_namespace.namespace).to eq(project_slug)
@@ -83,7 +83,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
describe '#service_account_name' do
let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" }
- it 'should set a service account name based on namespace' do
+ it 'sets a service account name based on namespace' do
subject
expect(kubernetes_namespace.service_account_name).to eq(service_account_name)
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 14bec17a2bd..0281dd2c303 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -447,7 +447,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
let(:platform) { cluster.platform }
context 'when namespace is updated' do
- it 'should call ConfigureWorker' do
+ it 'calls ConfigureWorker' do
expect(ClusterConfigureWorker).to receive(:perform_async).with(cluster.id).once
platform.namespace = 'new-namespace'
@@ -456,7 +456,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
context 'when namespace is not updated' do
- it 'should not call ConfigureWorker' do
+ it 'does not call ConfigureWorker' do
expect(ClusterConfigureWorker).not_to receive(:perform_async)
platform.username = "new-username"
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 259ac6852a8..27ed298ae08 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -659,7 +659,7 @@ describe Issuable do
end
context 'adding time' do
- it 'should update the total time spent' do
+ it 'updates the total time spent' do
spend_time(1800)
expect(issue.total_time_spent).to eq(1800)
@@ -679,7 +679,7 @@ describe Issuable do
spend_time(1800)
end
- it 'should update the total time spent' do
+ it 'updates the total time spent' do
spend_time(-900)
expect(issue.total_time_spent).to eq(900)
diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb
index db20a8b4701..25a2d290f76 100644
--- a/spec/models/concerns/prometheus_adapter_spec.rb
+++ b/spec/models/concerns/prometheus_adapter_spec.rb
@@ -77,6 +77,28 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe 'additional_metrics' do
+ let(:additional_metrics_environment_query) { Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery }
+ let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ let(:time_window) { [1552642245.067, 1552642095.831] }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with valid data' do
+ subject { service.query(:additional_metrics_environment, environment, *time_window) }
+
+ before do
+ stub_reactive_cache(service, prometheus_data, additional_metrics_environment_query, environment.id, *time_window)
+ end
+
+ it 'returns reactive data' do
+ expect(subject).to eq(prometheus_data)
+ end
+ end
+ end
end
describe '#calculate_reactive_cache' do
@@ -121,4 +143,24 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#build_query_args' do
+ subject { service.build_query_args(*args) }
+
+ context 'when active record models are included' do
+ let(:args) { [double(:environment, id: 12)] }
+
+ it 'serializes by id' do
+ is_expected.to eq [12]
+ end
+ end
+
+ context 'when args are safe for serialization' do
+ let(:args) { ['stringy arg', 5, 6.0, :symbolic_arg] }
+
+ it 'does nothing' do
+ is_expected.to eq args
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 32e13d5abed..53df9e0bc05 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -16,6 +16,10 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
attr_reader :id
+ def self.primary_key
+ :id
+ end
+
def initialize(id, &blk)
@id = id
@calculator = blk
@@ -106,6 +110,46 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
end
+ describe '.reactive_cache_worker_finder' do
+ context 'with default reactive_cache_worker_finder' do
+ let(:args) { %w(other args) }
+
+ before do
+ allow(instance.class).to receive(:find_by).with(id: instance.id)
+ .and_return(instance)
+ end
+
+ it 'calls the activerecord find_by method' do
+ result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
+
+ expect(result).to eq(instance)
+ expect(instance.class).to have_received(:find_by).with(id: instance.id)
+ end
+ end
+
+ context 'with custom reactive_cache_worker_finder' do
+ let(:args) { %w(arg1 arg2) }
+ let(:instance) { CustomFinderCacheTest.new(666, &calculation) }
+
+ class CustomFinderCacheTest < CacheTest
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ def self.from_cache(*args); end
+ end
+
+ before do
+ allow(instance.class).to receive(:from_cache).with(*args).and_return(instance)
+ end
+
+ it 'overrides the default reactive_cache_worker_finder' do
+ result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
+
+ expect(result).to eq(instance)
+ expect(instance.class).to have_received(:from_cache).with(*args)
+ end
+ end
+ end
+
describe '#clear_reactive_cache!' do
before do
stub_reactive_cache(instance, 4)
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index 650d49e41a1..67353475251 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -12,7 +12,7 @@ describe Spammable do
end
describe 'ClassMethods' do
- it 'should return correct attr_spammable' do
+ it 'returns correct attr_spammable' do
expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
end
end
@@ -20,7 +20,7 @@ describe Spammable do
describe 'InstanceMethods' do
let(:issue) { build(:issue, spam: true) }
- it 'should be invalid if spam' do
+ it 'is invalid if spam' do
expect(issue.valid?).to be_falsey
end
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 05320703e25..2fe82eaa778 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -9,7 +9,7 @@ describe DeployToken do
it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
describe '#ensure_token' do
- it 'should ensure a token' do
+ it 'ensures a token' do
deploy_token.token = nil
deploy_token.save
@@ -19,13 +19,13 @@ describe DeployToken do
describe '#ensure_at_least_one_scope' do
context 'with at least one scope' do
- it 'should be valid' do
+ it 'is valid' do
is_expected.to be_valid
end
end
context 'with no scopes' do
- it 'should be invalid' do
+ it 'is invalid' do
deploy_token = build(:deploy_token, read_repository: false, read_registry: false)
expect(deploy_token).not_to be_valid
@@ -36,13 +36,13 @@ describe DeployToken do
describe '#scopes' do
context 'with all the scopes' do
- it 'should return scopes assigned to DeployToken' do
+ it 'returns scopes assigned to DeployToken' do
expect(deploy_token.scopes).to eq([:read_repository, :read_registry])
end
end
context 'with only one scope' do
- it 'should return scopes assigned to DeployToken' do
+ it 'returns scopes assigned to DeployToken' do
deploy_token = create(:deploy_token, read_registry: false)
expect(deploy_token.scopes).to eq([:read_repository])
end
@@ -50,7 +50,7 @@ describe DeployToken do
end
describe '#revoke!' do
- it 'should update revoke attribute' do
+ it 'updates revoke attribute' do
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
end
@@ -58,20 +58,20 @@ describe DeployToken do
describe "#active?" do
context "when it has been revoked" do
- it 'should return false' do
+ it 'returns false' do
deploy_token.revoke!
expect(deploy_token.active?).to be_falsy
end
end
context "when it hasn't been revoked and is not expired" do
- it 'should return true' do
+ it 'returns true' do
expect(deploy_token.active?).to be_truthy
end
end
context "when it hasn't been revoked and is expired" do
- it 'should return true' do
+ it 'returns true' do
deploy_token.update_attribute(:expires_at, Date.today - 5.days)
expect(deploy_token.active?).to be_falsy
end
@@ -80,7 +80,7 @@ describe DeployToken do
context "when it hasn't been revoked and has no expiry" do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
- it 'should return true' do
+ it 'returns true' do
expect(deploy_token.active?).to be_truthy
end
end
@@ -126,7 +126,7 @@ describe DeployToken do
context 'when using Forever.date' do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
- it 'should return nil' do
+ it 'returns nil' do
expect(deploy_token.expires_at).to be_nil
end
end
@@ -135,7 +135,7 @@ describe DeployToken do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
- it 'should return the personalized date' do
+ it 'returns the personalized date' do
expect(deploy_token.expires_at).to eq(expires_at)
end
end
@@ -145,7 +145,7 @@ describe DeployToken do
context 'when passing nil' do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
- it 'should assign Forever.date' do
+ it 'assigns Forever.date' do
expect(deploy_token.read_attribute(:expires_at)).to eq(Forever.date)
end
end
@@ -154,7 +154,7 @@ describe DeployToken do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
- it 'should respect the value' do
+ it 'respects the value' do
expect(deploy_token.read_attribute(:expires_at)).to eq(expires_at)
end
end
@@ -166,14 +166,14 @@ describe DeployToken do
subject { project.deploy_tokens.gitlab_deploy_token }
context 'with a gitlab deploy token associated' do
- it 'should return the gitlab deploy token' do
+ it 'returns the gitlab deploy token' do
deploy_token = create(:deploy_token, :gitlab_deploy_token, projects: [project])
is_expected.to eq(deploy_token)
end
end
context 'with no gitlab deploy token associated' do
- it 'should return nil' do
+ it 'returns nil' do
is_expected.to be_nil
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index ca5eed60b56..cfe7c7ef0b0 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -687,7 +687,8 @@ describe Environment do
describe '#additional_metrics' do
let(:project) { create(:prometheus_project) }
- subject { environment.additional_metrics }
+ let(:metric_params) { [] }
+ subject { environment.additional_metrics(*metric_params) }
context 'when the environment has additional metrics' do
before do
@@ -695,12 +696,26 @@ describe Environment do
end
it 'returns the additional metrics from the deployment service' do
- expect(environment.prometheus_adapter).to receive(:query)
- .with(:additional_metrics_environment, environment)
- .and_return(:fake_metrics)
+ expect(environment.prometheus_adapter)
+ .to receive(:query)
+ .with(:additional_metrics_environment, environment)
+ .and_return(:fake_metrics)
is_expected.to eq(:fake_metrics)
end
+
+ context 'when time window arguments are provided' do
+ let(:metric_params) { [1552642245.067, Time.now] }
+
+ it 'queries with the expected parameters' do
+ expect(environment.prometheus_adapter)
+ .to receive(:query)
+ .with(:additional_metrics_environment, environment, *metric_params.map(&:to_f))
+ .and_return(:fake_metrics)
+
+ is_expected.to eq(:fake_metrics)
+ end
+ end
end
context 'when the environment does not have metrics' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 2c6abddca17..e6e7298a043 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -364,6 +364,32 @@ describe Group do
it { expect(group.has_maintainer?(nil)).to be_falsey }
end
+ describe '#last_owner?' do
+ before do
+ @members = setup_group_members(group)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+
+ context 'with two owners' do
+ before do
+ create(:group_member, :owner, group: group)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_falsy }
+ end
+
+ context 'with owners from a parent', :postgresql do
+ before do
+ parent_group = create(:group)
+ create(:group_member, :owner, group: parent_group)
+ group.update(parent: parent_group)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_falsy }
+ end
+ end
+
describe '#lfs_enabled?' do
context 'LFS enabled globally' do
before do
@@ -762,14 +788,14 @@ describe Group do
describe '#has_parent?' do
context 'when the group has a parent' do
- it 'should be truthy' do
+ it 'is truthy' do
group = create(:group, :nested)
expect(group.has_parent?).to be_truthy
end
end
context 'when the group has no parent' do
- it 'should be falsy' do
+ it 'is falsy' do
group = create(:group, parent: nil)
expect(group.has_parent?).to be_falsy
end
@@ -933,4 +959,12 @@ describe Group do
end
end
end
+
+ describe 'project_creation_level' do
+ it 'outputs the default one if it is nil' do
+ group = create(:group, project_creation_level: nil)
+
+ expect(group.project_creation_level).to eq(Gitlab::CurrentSettings.default_project_creation)
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 62e7dd3231b..387d1221c76 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -740,14 +740,14 @@ describe Namespace do
describe '#full_path_was' do
context 'when the group has no parent' do
- it 'should return the path was' do
+ it 'returns the path was' do
group = create(:group, parent: nil)
expect(group.full_path_was).to eq(group.path_was)
end
end
context 'when a parent is assigned to a group with no previous parent' do
- it 'should return the path was' do
+ it 'returns the path was' do
group = create(:group, parent: nil)
parent = create(:group)
@@ -758,7 +758,7 @@ describe Namespace do
end
context 'when a parent is removed from the group' do
- it 'should return the parent full path' do
+ it 'returns the parent full path' do
parent = create(:group)
group = create(:group, parent: parent)
group.parent = nil
@@ -768,7 +768,7 @@ describe Namespace do
end
context 'when changing parents' do
- it 'should return the previous parent full path' do
+ it 'returns the previous parent full path' do
parent = create(:group)
group = create(:group, parent: parent)
new_parent = create(:group)
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index d1a2bedf542..232172fde76 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -22,7 +22,7 @@ describe Network::Graph do
expect(commits).to all( be_kind_of(Network::Commit) )
end
- it 'it the commits by commit date (descending)' do
+ it 'sorts commits by commit date (descending)' do
# Remove duplicate timestamps because they make it harder to
# assert that the commits are sorted as expected.
commits = graph.commits.uniq(&:date)
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 8ad28ce68cc..b81e5610e2c 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -117,7 +117,7 @@ describe ProjectAutoDevops do
context 'when the project is public' do
let(:project) { create(:project, :repository, :public) }
- it 'should not create a gitlab deploy token' do
+ it 'does not create a gitlab deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
@@ -127,7 +127,7 @@ describe ProjectAutoDevops do
context 'when the project is internal' do
let(:project) { create(:project, :repository, :internal) }
- it 'should create a gitlab deploy token' do
+ it 'creates a gitlab deploy token' do
expect do
auto_devops.save
end.to change { DeployToken.count }.by(1)
@@ -137,7 +137,7 @@ describe ProjectAutoDevops do
context 'when the project is private' do
let(:project) { create(:project, :repository, :private) }
- it 'should create a gitlab deploy token' do
+ it 'creates a gitlab deploy token' do
expect do
auto_devops.save
end.to change { DeployToken.count }.by(1)
@@ -148,7 +148,7 @@ describe ProjectAutoDevops do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
- it 'should create a deploy token' do
+ it 'creates a deploy token' do
expect do
auto_devops.save
end.to change { DeployToken.count }.by(1)
@@ -159,7 +159,7 @@ describe ProjectAutoDevops do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) }
- it 'should create a deploy token' do
+ it 'creates a deploy token' do
allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
expect do
@@ -172,7 +172,7 @@ describe ProjectAutoDevops do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
- it 'should not create a deploy token' do
+ it 'does not create a deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
@@ -184,7 +184,7 @@ describe ProjectAutoDevops do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
- it 'should not create a deploy token' do
+ it 'does not create a deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
@@ -196,7 +196,7 @@ describe ProjectAutoDevops do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
- it 'should not create a deploy token' do
+ it 'does not create a deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 7bf093b71e7..3a381cb405d 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -70,11 +70,11 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
kubernetes_service.properties['namespace'] = "foo"
end
- it 'should not update attributes' do
+ it 'does not update attributes' do
expect(kubernetes_service.save).to be_falsy
end
- it 'should include an error with a deprecation message' do
+ it 'includes an error with a deprecation message' do
kubernetes_service.valid?
expect(kubernetes_service.errors[:base].first).to match(/Kubernetes service integration has been deprecated/)
end
@@ -83,7 +83,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'with a non-deprecated service' do
let(:kubernetes_service) { create(:kubernetes_service) }
- it 'should update attributes' do
+ it 'updates attributes' do
kubernetes_service.properties['namespace'] = 'foo'
expect(kubernetes_service.save).to be_truthy
end
@@ -98,15 +98,15 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
kubernetes_service.save
end
- it 'should deactive the service' do
+ it 'deactivates the service' do
expect(kubernetes_service.active?).to be_falsy
end
- it 'should not include a deprecation message as error' do
+ it 'does not include a deprecation message as error' do
expect(kubernetes_service.errors.messages.count).to eq(0)
end
- it 'should update attributes' do
+ it 'updates attributes' do
expect(kubernetes_service.properties['namespace']).to eq("foo")
end
end
@@ -118,7 +118,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
kubernetes_service.properties['namespace'] = 'foo'
end
- it 'should update attributes' do
+ it 'updates attributes' do
expect(kubernetes_service.save).to be_truthy
expect(kubernetes_service.properties['namespace']).to eq('foo')
end
@@ -392,13 +392,13 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:kubernetes_service) { create(:kubernetes_service) }
context 'with an active kubernetes service' do
- it 'should return false' do
+ it 'returns false' do
expect(kubernetes_service.deprecated?).to be_falsy
end
end
context 'with a inactive kubernetes service' do
- it 'should return true' do
+ it 'returns true' do
kubernetes_service.update_attribute(:active, false)
expect(kubernetes_service.deprecated?).to be_truthy
end
@@ -408,18 +408,18 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe "#deprecation_message" do
let(:kubernetes_service) { create(:kubernetes_service) }
- it 'should indicate the service is deprecated' do
+ it 'indicates the service is deprecated' do
expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/)
end
context 'if the services is active' do
- it 'should return a message' do
+ it 'returns a message' do
expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/)
end
end
context 'if the service is not active' do
- it 'should return a message' do
+ it 'returns a message' do
kubernetes_service.update_attribute(:active, false)
expect(kubernetes_service.deprecation_message).to match(/Fields on this page are now uneditable/)
end
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index de2c8790405..773b8b7890f 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -56,7 +56,7 @@ describe PivotaltrackerService do
WebMock.stub_request(:post, url)
end
- it 'should post correct message' do
+ it 'posts correct message' do
service.execute(push_data)
expect(WebMock).to have_requested(:post, url).with(
body: {
@@ -81,14 +81,14 @@ describe PivotaltrackerService do
end
end
- it 'should post message if branch is in the list' do
+ it 'posts message if branch is in the list' do
service.execute(push_data(branch: 'master'))
service.execute(push_data(branch: 'v10'))
expect(WebMock).to have_requested(:post, url).twice
end
- it 'should not post message if branch is not in the list' do
+ it 'does not post message if branch is not in the list' do
service.execute(push_data(branch: 'mas'))
service.execute(push_data(branch: 'v11'))
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 33e514cd7b9..5eb31430ccd 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -415,7 +415,7 @@ describe Project do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
- it 'should return .external pipelines' do
+ it 'returns .external pipelines' do
expect(project.all_pipelines).to all(have_attributes(source: 'external'))
expect(project.all_pipelines.size).to eq(1)
end
@@ -439,7 +439,7 @@ describe Project do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
- it 'should return .external pipelines' do
+ it 'returns .external pipelines' do
expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
expect(project.ci_pipelines.size).to eq(1)
end
@@ -1910,7 +1910,7 @@ describe Project do
tags: %w[latest rc1])
end
- it 'should have image tags' do
+ it 'has image tags' do
expect(project).to have_container_registry_tags
end
end
@@ -1921,7 +1921,7 @@ describe Project do
tags: %w[latest rc1 pre1])
end
- it 'should have image tags' do
+ it 'has image tags' do
expect(project).to have_container_registry_tags
end
end
@@ -1931,7 +1931,7 @@ describe Project do
stub_container_registry_tags(repository: :any, tags: [])
end
- it 'should not have image tags' do
+ it 'does not have image tags' do
expect(project).not_to have_container_registry_tags
end
end
@@ -1942,16 +1942,16 @@ describe Project do
stub_container_registry_config(enabled: false)
end
- it 'should not have image tags' do
+ it 'does not have image tags' do
expect(project).not_to have_container_registry_tags
end
- it 'should not check root repository tags' do
+ it 'does not check root repository tags' do
expect(project).not_to receive(:full_path)
expect(project).not_to have_container_registry_tags
end
- it 'should iterate through container repositories' do
+ it 'iterates through container repositories' do
expect(project).to receive(:container_repositories)
expect(project).not_to have_container_registry_tags
end
@@ -2638,7 +2638,7 @@ describe Project do
let!(:cluster) { kubernetes_namespace.cluster }
let(:project) { kubernetes_namespace.project }
- it 'should return token from kubernetes namespace' do
+ it 'returns token from kubernetes namespace' do
expect(project.deployment_variables).to include(
{ key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 0478094034a..f743dfed31f 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -7,14 +7,14 @@ describe RemoteMirror, :mailer do
describe 'URL validation' do
context 'with a valid URL' do
- it 'should be valid' do
+ it 'is valid' do
remote_mirror = build(:remote_mirror)
expect(remote_mirror).to be_valid
end
end
context 'with an invalid URL' do
- it 'should not be valid' do
+ it 'is not valid' do
remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
expect(remote_mirror).not_to be_valid
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 2578208659a..3f5d285bc2c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -50,7 +50,7 @@ describe Repository do
it { is_expected.not_to include('fix') }
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.branch_names_contains(sample_commit.id)
end
@@ -225,7 +225,7 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
end
@@ -249,7 +249,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
end
@@ -390,7 +390,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
end
@@ -726,7 +726,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.search_files_by_content('feature', 'master')
end
@@ -775,7 +775,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
end
end
@@ -817,7 +817,7 @@ describe Repository do
let(:broken_repository) { create(:project, :broken_storage).repository }
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
end
@@ -1018,7 +1018,7 @@ describe Repository do
repository.add_branch(project.creator, ref, 'master')
end
- it 'should be true' do
+ it 'is true' do
is_expected.to eq(true)
end
end
@@ -1028,7 +1028,7 @@ describe Repository do
repository.add_tag(project.creator, ref, 'master')
end
- it 'should be false' do
+ it 'is false' do
is_expected.to eq(false)
end
end
@@ -1152,7 +1152,7 @@ describe Repository do
end
context 'with broken storage', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error { broken_repository.exists? }
end
end
@@ -2249,11 +2249,11 @@ describe Repository do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
- it 'it is an ancestor' do
+ it 'is an ancestor' do
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
- it 'it is not an ancestor' do
+ it 'is not an ancestor' do
expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
end
diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb
new file mode 100644
index 00000000000..1854d5f9415
--- /dev/null
+++ b/spec/models/serverless/function_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Serverless::Function do
+ let(:project) { create(:project) }
+ let(:func) { described_class.new(project, 'test', 'test-ns') }
+
+ it 'has a proper id' do
+ expect(func.id).to eql("#{project.id}/test/test-ns")
+ expect(func.name).to eql("test")
+ expect(func.namespace).to eql("test-ns")
+ end
+
+ it 'can decode an identifier' do
+ f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns")
+
+ expect(f.name).to eql("testfunc")
+ expect(f.namespace).to eql("dummy-ns")
+ end
+end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 2f025038bab..64db32781fe 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -291,7 +291,7 @@ describe Service do
describe "#deprecated?" do
let(:project) { create(:project, :repository) }
- it 'should return false by default' do
+ it 'returns false by default' do
service = create(:service, project: project)
expect(service.deprecated?).to be_falsy
end
@@ -300,7 +300,7 @@ describe Service do
describe "#deprecation_message" do
let(:project) { create(:project, :repository) }
- it 'should be empty by default' do
+ it 'is empty by default' do
service = create(:service, project: project)
expect(service.deprecation_message).to be_nil
end
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index cafc725dddb..8d4e9070b19 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -21,6 +21,22 @@ describe Suggestion do
end
end
+ describe '#diff_lines' do
+ let(:suggestion) { create(:suggestion, :content_from_repo) }
+
+ it 'returns parsed diff lines' do
+ expected_diff_lines = Gitlab::Diff::SuggestionDiff.new(suggestion).diff_lines
+ diff_lines = suggestion.diff_lines
+
+ expect(diff_lines.size).to eq(expected_diff_lines.size)
+ expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
+
+ expected_diff_lines.each_with_index do |expected_line, index|
+ expect(diff_lines[index].to_hash).to eq(expected_line.to_hash)
+ end
+ end
+ end
+
describe '#appliable?' do
context 'when note does not support suggestions' do
it 'returns false' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b7e36748fa2..a45a2737b13 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -98,6 +98,11 @@ describe User do
end
describe 'validations' do
+ describe 'name' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(128) }
+ end
+
describe 'username' do
it 'validates presence' do
expect(subject).to validate_presence_of(:username)
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index e68da67818a..cacdb0e0595 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -20,12 +20,16 @@ describe WikiPage do
context 'when there are pages' do
before do
create_page('dir_1/dir_1_1/page_3', 'content')
+ create_page('page_1', 'content')
create_page('dir_1/page_2', 'content')
create_page('dir_2/page_5', 'content')
+ create_page('page_6', 'content')
create_page('dir_2/page_4', 'content')
- create_page('page_1', 'content')
end
+
let(:page_1) { wiki.find_page('page_1') }
+ let(:page_6) { wiki.find_page('page_6') }
+
let(:dir_1) do
WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
end
@@ -38,25 +42,38 @@ describe WikiPage do
WikiDirectory.new('dir_2', pages)
end
- it 'returns an array with pages and directories' do
- expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2]
+ context 'sort by title' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.pages) }
+ let(:expected_grouped_entries) { [dir_1_1, dir_1, dir_2, page_1, page_6] }
- grouped_entries = described_class.group_by_directory(wiki.pages)
+ it 'returns an array with pages and directories' do
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
- grouped_entries.each_with_index do |page_or_dir, i|
- expected_page_or_dir = expected_grouped_entries[i]
- expected_slugs = get_slugs(expected_page_or_dir)
- slugs = get_slugs(page_or_dir)
+ expect(slugs).to match_array(expected_slugs)
+ end
+ end
+ end
+
+ context 'sort by created_at' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.pages(sort: 'created_at')) }
+ let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, dir_2, page_6] }
- expect(slugs).to match_array(expected_slugs)
+ it 'returns an array with pages and directories' do
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
+
+ expect(slugs).to match_array(expected_slugs)
+ end
end
end
- it 'returns an array sorted by alphabetical position' do
- # Directories and pages within directories are sorted alphabetically.
- # Pages at root come before everything.
- expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3',
- 'dir_2/page_4', 'dir_2/page_5']
+ it 'returns an array with retained order with directories at the top' do
+ expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
grouped_entries = described_class.group_by_directory(wiki.pages)
diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb
new file mode 100644
index 00000000000..7bd7184cffe
--- /dev/null
+++ b/spec/policies/group_member_policy_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupMemberPolicy do
+ let(:guest) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :private) }
+
+ before do
+ group.add_guest(guest)
+ group.add_owner(owner)
+ end
+
+ let(:member_related_permissions) do
+ [:update_group_member, :destroy_group_member]
+ end
+
+ let(:membership) { current_user.members.first }
+
+ subject { described_class.new(current_user, membership) }
+
+ def expect_allowed(*permissions)
+ permissions.each { |p| is_expected.to be_allowed(p) }
+ end
+
+ def expect_disallowed(*permissions)
+ permissions.each { |p| is_expected.not_to be_allowed(p) }
+ end
+
+ context 'with guest user' do
+ let(:current_user) { guest }
+
+ it do
+ expect_disallowed(:member_related_permissions)
+ end
+ end
+
+ context 'with one owner' do
+ let(:current_user) { owner }
+
+ it do
+ expect_disallowed(:destroy_group_member)
+ expect_disallowed(:update_group_member)
+ end
+ end
+
+ context 'with more than one owner' do
+ let(:current_user) { owner }
+
+ before do
+ group.add_owner(create(:user))
+ end
+
+ it do
+ expect_allowed(:destroy_group_member)
+ expect_allowed(:update_group_member)
+ end
+ end
+
+ context 'with the group parent', :postgresql do
+ let(:current_user) { create :user }
+ let(:subgroup) { create(:group, :private, parent: group)}
+
+ before do
+ group.add_owner(owner)
+ subgroup.add_owner(current_user)
+ end
+
+ it do
+ expect_allowed(:destroy_group_member)
+ expect_allowed(:update_group_member)
+ end
+ end
+
+ context 'without group parent' do
+ let(:current_user) { create :user }
+ let(:subgroup) { create(:group, :private)}
+
+ before do
+ subgroup.add_owner(current_user)
+ end
+
+ it do
+ expect_disallowed(:destroy_group_member)
+ expect_disallowed(:update_group_member)
+ end
+ end
+
+ context 'without group parent with two owners' do
+ let(:current_user) { create :user }
+ let(:other_user) { create :user }
+ let(:subgroup) { create(:group, :private)}
+
+ before do
+ subgroup.add_owner(current_user)
+ subgroup.add_owner(other_user)
+ end
+
+ it do
+ expect_allowed(:destroy_group_member)
+ expect_allowed(:update_group_member)
+ end
+ end
+end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index dc98baca6dc..59f3a961d50 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -347,6 +347,120 @@ describe GroupPolicy do
end
end
+ context "create_projects" do
+ context 'when group has no project creation level set' do
+ let(:group) { create(:group, project_creation_level: nil) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+ end
+
+ context 'when group has project creation level set to no one' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+ end
+
+ context 'when group has project creation level set to maintainer only' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+ end
+
+ context 'when group has project creation level set to developers + maintainer' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+ end
+ end
+
it_behaves_like 'clusterable policies' do
let(:clusterable) { create(:group) }
let(:cluster) do
diff --git a/spec/presenters/ci/bridge_presenter_spec.rb b/spec/presenters/ci/bridge_presenter_spec.rb
new file mode 100644
index 00000000000..986818a7b9e
--- /dev/null
+++ b/spec/presenters/ci/bridge_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Ci::BridgePresenter do
+ set(:project) { create(:project) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+ set(:bridge) { create(:ci_bridge, pipeline: pipeline, status: :failed) }
+
+ subject(:presenter) do
+ described_class.new(bridge)
+ end
+
+ it 'presents information about recoverable state' do
+ expect(presenter).to be_recoverable
+ end
+end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index 676835b3880..e202f7a9b5f 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -180,7 +180,7 @@ describe Ci::BuildPresenter do
context 'When build has failed and retried' do
let(:build) { create(:ci_build, :script_failure, :retried, pipeline: pipeline) }
- it 'should include the reason of failure and the retried title' do
+ it 'includes the reason of failure and the retried title' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - failed - (script failure) (retried)")
@@ -190,7 +190,7 @@ describe Ci::BuildPresenter do
context 'When build has failed and is allowed to' do
let(:build) { create(:ci_build, :script_failure, :allowed_to_fail, pipeline: pipeline) }
- it 'should include the reason of failure' do
+ it 'includes the reason of failure' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - failed - (script failure) (allowed to fail)")
@@ -200,7 +200,7 @@ describe Ci::BuildPresenter do
context 'For any other build (no retried)' do
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
- it 'should include build name and status' do
+ it 'includes build name and status' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - passed")
@@ -210,7 +210,7 @@ describe Ci::BuildPresenter do
context 'For any other build (retried)' do
let(:build) { create(:ci_build, :success, :retried, pipeline: pipeline) }
- it 'should include build name and status' do
+ it 'includes build name and status' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - passed (retried)")
@@ -269,7 +269,7 @@ describe Ci::BuildPresenter do
context 'when is a script or missing dependency failure' do
let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) }
- it 'should return false' do
+ it 'returns false' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_falsy
@@ -280,7 +280,7 @@ describe Ci::BuildPresenter do
context 'when is any other failure type' do
let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
- it 'should return true' do
+ it 'returns true' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_truthy
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
new file mode 100644
index 00000000000..708a000532b
--- /dev/null
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'GitlabSchema configurations' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+ let!(:query) { graphql_query_for('project', 'fullPath' => project.full_path) }
+
+ it 'shows an error if complexity it too high' do
+ allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
+
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1')
+ end
+end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 537194b8e11..0919540e4ba 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -498,6 +498,40 @@ describe API::Internal do
end
end
+ context "console message" do
+ before do
+ project.add_developer(user)
+ end
+
+ context "git pull" do
+ context "with no console message" do
+ it "has the correct payload" do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['gl_console_messages']).to eq([])
+ end
+ end
+
+ context "with a console message" do
+ let(:console_messages) { ['message for the console'] }
+
+ it "has the correct payload" do
+ expect_next_instance_of(Gitlab::GitAccess) do |access|
+ expect(access).to receive(:check_for_console_messages)
+ .with('git-upload-pack')
+ .and_return(console_messages)
+ end
+
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['gl_console_messages']).to eq(console_messages)
+ end
+ end
+ end
+ end
+
context "blocked user" do
let(:personal_project) { create(:project, namespace: user.namespace) }
@@ -610,6 +644,22 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(404)
expect(json_response["status"]).to be_falsey
end
+
+ it 'returns a 200 response when using a project path that does not exist' do
+ post(
+ api("/internal/allowed"),
+ params: {
+ key_id: key.id,
+ project: 'project/does-not-exist.git',
+ action: 'git-upload-pack',
+ secret_token: secret_token,
+ protocol: 'ssh'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response["status"]).to be_falsey
+ end
end
context 'user does not exist' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index a5434d3ea80..86484ce62f8 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -2189,6 +2189,18 @@ describe API::Issues do
expect_paginated_array_response(related_mr.id)
end
+ context 'merge request closes an issue' do
+ let!(:closing_issue_mr_rel) do
+ create(:merge_requests_closing_issues, issue: issue, merge_request: related_mr)
+ end
+
+ it 'returns closing MR only once' do
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response([related_mr.id])
+ end
+ end
+
context 'no merge request mentioned a issue' do
it 'returns empty array' do
get_related_merge_requests(project.id, closed_issue.iid, user)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4259fda7f04..7ffa365c651 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -729,6 +729,14 @@ describe API::MergeRequests do
end
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
+ it 'matches json schema' do
+ merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/merge_request')
+ end
+
it 'exposes known attributes' do
create(:award_emoji, :downvote, awardable: merge_request)
create(:award_emoji, :upvote, awardable: merge_request)
@@ -1353,7 +1361,12 @@ describe API::MergeRequests do
end
it 'returns 405 if the build failed for a merge request that requires success' do
- allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+ project.update!(only_allow_merge_if_pipeline_succeeds: true)
+
+ create(:ci_pipeline,
+ :failed,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 52599db9a9e..9fed07cae82 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -399,6 +399,13 @@ describe API::Pipelines do
describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
+ it 'exposes known attributes' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pipeline/detail')
+ end
+
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
@@ -428,7 +435,7 @@ describe API::Pipelines do
end
context 'unauthorized user' do
- it 'should not return a project pipeline' do
+ it 'does not return a project pipeline' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -474,7 +481,7 @@ describe API::Pipelines do
context 'unauthorized user' do
context 'when user is not member' do
- it 'should return a 404' do
+ it 'returns a 404' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -489,7 +496,7 @@ describe API::Pipelines do
project.add_developer(developer)
end
- it 'should return a 403' do
+ it 'returns a 403' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer)
expect(response).to have_gitlab_http_status(403)
@@ -519,7 +526,7 @@ describe API::Pipelines do
end
context 'unauthorized user' do
- it 'should not return a project pipeline' do
+ it 'does not return a project pipeline' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
expect(response).to have_gitlab_http_status(404)
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 81442125a1c..94e6ca2c07c 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -22,7 +22,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
get api("/projects/#{project.id}/clusters", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -34,15 +34,15 @@ describe API::ProjectClusters do
get api("/projects/#{project.id}/clusters", current_user)
end
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
- it 'should include pagination headers' do
+ it 'includes pagination headers' do
expect(response).to include_pagination_headers
end
- it 'should only include authorized clusters' do
+ it 'onlies include authorized clusters' do
cluster_ids = json_response.map { |cluster| cluster['id'] }
expect(cluster_ids).to match_array(clusters.pluck(:id))
@@ -67,7 +67,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
get api("/projects/#{project.id}/clusters/#{cluster_id}", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -132,7 +132,7 @@ describe API::ProjectClusters do
projects: [project])
end
- it 'should not include GCP provider info' do
+ it 'does not include GCP provider info' do
expect(json_response['provider_gcp']).not_to be_present
end
end
@@ -194,7 +194,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
post api("/projects/#{project.id}/clusters/user", non_member), params: cluster_params
expect(response).to have_gitlab_http_status(404)
@@ -207,11 +207,11 @@ describe API::ProjectClusters do
end
context 'with valid params' do
- it 'should respond with 201' do
+ it 'responds with 201' do
expect(response).to have_gitlab_http_status(201)
end
- it 'should create a new Cluster::Cluster' do
+ it 'creates a new Cluster::Cluster' do
cluster_result = Clusters::Cluster.find(json_response["id"])
platform_kubernetes = cluster_result.platform
@@ -246,7 +246,7 @@ describe API::ProjectClusters do
context 'when user sets authorization type as ABAC' do
let(:authorization_type) { 'abac' }
- it 'should create an ABAC cluster' do
+ it 'creates an ABAC cluster' do
cluster_result = Clusters::Cluster.find(json_response['id'])
expect(cluster_result.platform.abac?).to be_truthy
@@ -256,15 +256,15 @@ describe API::ProjectClusters do
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
- it 'should respond with 400' do
+ it 'responds with 400' do
expect(response).to have_gitlab_http_status(400)
end
- it 'should not create a new Clusters::Cluster' do
+ it 'does not create a new Clusters::Cluster' do
expect(project.reload.clusters).to be_empty
end
- it 'should return validation errors' do
+ it 'returns validation errors' do
expect(json_response['message']['platform_kubernetes.namespace'].first).to be_present
end
end
@@ -278,11 +278,11 @@ describe API::ProjectClusters do
post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
end
- it 'should respond with 403' do
+ it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
- it 'should return an appropriate message' do
+ it 'returns an appropriate message' do
expect(json_response['message']).to include('Instance does not support multiple Kubernetes clusters')
end
end
@@ -314,7 +314,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
put api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: update_params
expect(response).to have_gitlab_http_status(404)
@@ -329,11 +329,11 @@ describe API::ProjectClusters do
end
context 'with valid params' do
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
- it 'should update cluster attributes' do
+ it 'updates cluster attributes' do
expect(cluster.domain).to eq('new-domain.com')
expect(cluster.platform_kubernetes.namespace).to eq('new-namespace')
end
@@ -342,17 +342,17 @@ describe API::ProjectClusters do
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
- it 'should respond with 400' do
+ it 'responds with 400' do
expect(response).to have_gitlab_http_status(400)
end
- it 'should not update cluster attributes' do
+ it 'does not update cluster attributes' do
expect(cluster.domain).not_to eq('new_domain.com')
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
expect(cluster.kubernetes_namespace.namespace).not_to eq('invalid_namespace')
end
- it 'should return validation errors' do
+ it 'returns validation errors' do
expect(json_response['message']['platform_kubernetes.namespace'].first).to match('can contain only lowercase letters')
end
end
@@ -366,11 +366,11 @@ describe API::ProjectClusters do
}
end
- it 'should respond with 400' do
+ it 'responds with 400' do
expect(response).to have_gitlab_http_status(400)
end
- it 'should return validation error' do
+ it 'returns validation error' do
expect(json_response['message']['platform_kubernetes.base'].first).to eq('Cannot modify managed Kubernetes cluster')
end
end
@@ -378,7 +378,7 @@ describe API::ProjectClusters do
context 'when user tries to change namespace' do
let(:namespace) { 'new-namespace' }
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
end
@@ -407,11 +407,11 @@ describe API::ProjectClusters do
}
end
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
- it 'should update platform kubernetes attributes' do
+ it 'updates platform kubernetes attributes' do
platform_kubernetes = cluster.platform_kubernetes
expect(cluster.name).to eq('new-name')
@@ -424,7 +424,7 @@ describe API::ProjectClusters do
context 'with a cluster that does not belong to user' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
@@ -440,7 +440,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: cluster_params
expect(response).to have_gitlab_http_status(404)
@@ -452,18 +452,18 @@ describe API::ProjectClusters do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: cluster_params
end
- it 'should respond with 204' do
+ it 'responds with 204' do
expect(response).to have_gitlab_http_status(204)
end
- it 'should delete the cluster' do
+ it 'deletes the cluster' do
expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy
end
context 'with a cluster that does not belong to user' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index f33eb5b9e02..f869325e892 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -44,6 +44,7 @@ describe API::Settings, 'Settings' do
put api("/application/settings", admin),
params: {
default_projects_limit: 3,
+ default_project_creation: 2,
password_authentication_enabled_for_web: false,
repository_storages: ['custom'],
plantuml_enabled: true,
@@ -64,12 +65,13 @@ describe API::Settings, 'Settings' do
performance_bar_allowed_group_path: group.full_path,
instance_statistics_visibility_private: true,
diff_max_patch_bytes: 150_000,
- default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
+ default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3
}
expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
+ expect(json_response['default_project_creation']).to eq(::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
expect(json_response['password_authentication_enabled_for_web']).to be_falsey
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['plantuml_enabled']).to be_truthy
diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb
index be6aa7c65c3..dbfb3eace83 100644
--- a/spec/serializers/analytics_stage_serializer_spec.rb
+++ b/spec/serializers/analytics_stage_serializer_spec.rb
@@ -14,7 +14,7 @@ describe AnalyticsStageSerializer do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
end
- it 'it generates payload for single object' do
+ it 'generates payload for single object' do
expect(subject).to be_kind_of Hash
end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
index 236c244b402..8fa0574bfd6 100644
--- a/spec/serializers/analytics_summary_serializer_spec.rb
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -18,7 +18,7 @@ describe AnalyticsSummarySerializer do
.to receive(:value).and_return(1.12)
end
- it 'it generates payload for single object' do
+ it 'generates payload for single object' do
expect(subject).to be_kind_of Hash
end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index f6bd6e9ede4..1edf69dc290 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -112,5 +112,15 @@ describe BuildDetailsEntity do
expect(subject['merge_request_path']).to be_nil
end
end
+
+ context 'when the build has failed' do
+ let(:build) { create(:ci_build, :created) }
+
+ before do
+ build.drop!(:unmet_prerequisites)
+ end
+
+ it { is_expected.to include(failure_reason: 'unmet_prerequisites') }
+ end
end
end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 791b64dc356..c2312734042 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -54,7 +54,7 @@ describe EnvironmentEntity do
projects: [project])
end
- it 'should include cluster_type' do
+ it 'includes cluster_type' do
expect(subject).to include(:cluster_type)
expect(subject[:cluster_type]).to eq('project_type')
end
@@ -65,7 +65,7 @@ describe EnvironmentEntity do
create(:kubernetes_service, project: project)
end
- it 'should not include cluster_type' do
+ it 'does not include cluster_type' do
expect(subject).not_to include(:cluster_type)
end
end
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 851b41a7f7e..8de61d4d466 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -154,15 +154,15 @@ describe JobEntity do
expect(subject[:status][:label]).to eq('failed')
end
- it 'should indicate the failure reason on tooltip' do
+ it 'indicates the failure reason on tooltip' do
expect(subject[:status][:tooltip]).to eq('failed - (API failure)')
end
- it 'should include a callout message with a verbose output' do
+ it 'includes a callout message with a verbose output' do
expect(subject[:callout_message]).to eq('There has been an API failure, please try again')
end
- it 'should state that it is not recoverable' do
+ it 'states that it is not recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
@@ -178,15 +178,15 @@ describe JobEntity do
expect(subject[:status][:label]).to eq('failed (allowed to fail)')
end
- it 'should indicate the failure reason on tooltip' do
+ it 'indicates the failure reason on tooltip' do
expect(subject[:status][:tooltip]).to eq('failed - (API failure) (allowed to fail)')
end
- it 'should include a callout message with a verbose output' do
+ it 'includes a callout message with a verbose output' do
expect(subject[:callout_message]).to eq('There has been an API failure, please try again')
end
- it 'should state that it is not recoverable' do
+ it 'states that it is not recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
@@ -194,7 +194,7 @@ describe JobEntity do
context 'when the job failed with a script failure' do
let(:job) { create(:ci_build, :failed, :script_failure) }
- it 'should not include callout message or recoverable keys' do
+ it 'does not include callout message or recoverable keys' do
expect(subject).not_to include('callout_message')
expect(subject).not_to include('recoverable')
end
@@ -203,7 +203,7 @@ describe JobEntity do
context 'when job failed and is recoverable' do
let(:job) { create(:ci_build, :api_failure) }
- it 'should state it is recoverable' do
+ it 'states it is recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
@@ -211,7 +211,7 @@ describe JobEntity do
context 'when job passed' do
let(:job) { create(:ci_build, :success) }
- it 'should not include callout message or recoverable keys' do
+ it 'does not include callout message or recoverable keys' do
expect(subject).not_to include('callout_message')
expect(subject).not_to include('recoverable')
end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 0e99ef38d2f..b89898f26f7 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -287,7 +287,7 @@ describe MergeRequestWidgetEntity do
resource.commits.find { |c| c.short_id == short_id }
end
- it 'should not include merge commits' do
+ it 'does not include merge commits' do
commits_in_widget = subject[:commits_without_merge_commits]
expect(commits_in_widget.length).to be < resource.commits.length
diff --git a/spec/serializers/suggestion_entity_spec.rb b/spec/serializers/suggestion_entity_spec.rb
index d38fc2b132b..d282a7f9c7a 100644
--- a/spec/serializers/suggestion_entity_spec.rb
+++ b/spec/serializers/suggestion_entity_spec.rb
@@ -13,8 +13,7 @@ describe SuggestionEntity do
subject { entity.as_json }
it 'exposes correct attributes' do
- expect(subject).to include(:id, :from_line, :to_line, :appliable,
- :applied, :from_content, :to_content)
+ expect(subject.keys).to match_array([:id, :appliable, :applied, :diff_lines, :current_user])
end
it 'exposes current user abilities' do
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
index 19446ce1cf8..1cca89d31d7 100644
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -33,14 +33,22 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
end
- shared_examples 'error logging' do
+ shared_examples 'error handling' do
context 'when installation raises a Kubeclient::HttpError' do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
+ let(:logger) { service.send(:logger) }
+ let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
before do
application.update!(cluster: cluster)
- expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ expect(service).to receive(:installation_phase).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'Unauthorized' }
+ let(:error_code) { 401 }
end
it 'shows the response code from the error' do
@@ -49,12 +57,6 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
expect(application).to be_errored.or(be_update_errored)
expect(application.status_reason).to eq('Kubernetes error: 401')
end
-
- it 'should log error' do
- expect(service.send(:logger)).to receive(:error)
-
- service.execute
- end
end
end
@@ -66,7 +68,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
context 'when application is updating' do
let(:application) { create(:clusters_applications_helm, :updating) }
- include_examples 'error logging'
+ include_examples 'error handling'
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
@@ -127,7 +129,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when application is installing' do
- include_examples 'error logging'
+ include_examples 'error handling'
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
index 018d9822d3e..db0c33a95b2 100644
--- a/spec/services/clusters/applications/install_service_spec.rb
+++ b/spec/services/clusters/applications/install_service_spec.rb
@@ -39,51 +39,34 @@ describe Clusters::Applications::InstallService do
expect(helm_client).to receive(:install).with(install_command).and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
it 'make the application errored' do
service.execute
expect(application).to be_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- service.execute
- end
end
context 'a non kubernetes error happens' do
let(:application) { create(:clusters_applications_helm, :scheduled) }
- let(:error) { StandardError.new("something bad happened") }
+ let(:error) { StandardError.new('something bad happened') }
before do
expect(application).to receive(:make_installing!).once.and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+
it 'make the application errored' do
expect(helm_client).not_to receive(:install)
@@ -92,35 +75,6 @@ describe Clusters::Applications::InstallService do
expect(application).to be_errored
expect(application.status_reason).to eq("Can't start installation process.")
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- service.execute
- end
end
end
end
diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb
index d4ee3243b84..10b1379a127 100644
--- a/spec/services/clusters/applications/patch_service_spec.rb
+++ b/spec/services/clusters/applications/patch_service_spec.rb
@@ -41,47 +41,30 @@ describe Clusters::Applications::PatchService do
expect(helm_client).to receive(:update).with(update_command).and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::PatchService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::PatchService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- service.execute
- end
end
context 'a non kubernetes error happens' do
let(:application) { create(:clusters_applications_knative, :scheduled) }
let(:error) { StandardError.new('something bad happened') }
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+
before do
expect(application).to receive(:make_updating!).once.and_raise(error)
end
@@ -94,35 +77,6 @@ describe Clusters::Applications::PatchService do
expect(application).to be_update_errored
expect(application.status_reason).to eq("Can't start update process.")
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::PatchService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::PatchService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- service.execute
- end
end
end
end
diff --git a/spec/services/clusters/applications/upgrade_service_spec.rb b/spec/services/clusters/applications/upgrade_service_spec.rb
index 1822fc38dbd..dd2e6e94e4f 100644
--- a/spec/services/clusters/applications/upgrade_service_spec.rb
+++ b/spec/services/clusters/applications/upgrade_service_spec.rb
@@ -41,41 +41,18 @@ describe Clusters::Applications::UpgradeService do
expect(helm_client).to receive(:update).with(install_command).and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- service.execute
- end
end
context 'a non kubernetes error happens' do
@@ -86,6 +63,12 @@ describe Clusters::Applications::UpgradeService do
expect(application).to receive(:make_updating!).once.and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+
it 'make the application errored' do
expect(helm_client).not_to receive(:update)
@@ -94,35 +77,6 @@ describe Clusters::Applications::UpgradeService do
expect(application).to be_update_errored
expect(application.status_reason).to eq("Can't start upgrade process.")
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- service.execute
- end
end
end
end
diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
index 11a65d0c300..382b9043566 100644
--- a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
@@ -89,7 +89,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do
it_behaves_like 'creates service account and token'
- it 'should create a cluster role binding with cluster-admin access' do
+ it 'creates a cluster role binding with cluster-admin access' do
subject
expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with(
diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb
index 3a2bbf1ecd1..1bd0356a73b 100644
--- a/spec/services/deploy_tokens/create_service_spec.rb
+++ b/spec/services/deploy_tokens/create_service_spec.rb
@@ -9,11 +9,11 @@ describe DeployTokens::CreateService do
subject { described_class.new(project, user, deploy_token_params).execute }
context 'when the deploy token is valid' do
- it 'should create a new DeployToken' do
+ it 'creates a new DeployToken' do
expect { subject }.to change { DeployToken.count }.by(1)
end
- it 'should create a new ProjectDeployToken' do
+ it 'creates a new ProjectDeployToken' do
expect { subject }.to change { ProjectDeployToken.count }.by(1)
end
@@ -25,7 +25,7 @@ describe DeployTokens::CreateService do
context 'when expires at date is not passed' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
- it 'should set Forever.date' do
+ it 'sets Forever.date' do
expect(subject.read_attribute(:expires_at)).to eq(Forever.date)
end
end
@@ -33,11 +33,11 @@ describe DeployTokens::CreateService do
context 'when the deploy token is invalid' do
let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) }
- it 'should not create a new DeployToken' do
+ it 'does not create a new DeployToken' do
expect { subject }.not_to change { DeployToken.count }
end
- it 'should not create a new ProjectDeployToken' do
+ it 'does not create a new ProjectDeployToken' do
expect { subject }.not_to change { ProjectDeployToken.count }
end
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 79d504b9b45..0bc67dbb4a1 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -12,11 +12,11 @@ describe Groups::TransferService, :postgresql do
allow(Group).to receive(:supports_nested_objects?).and_return(false)
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Database is not supported.')
end
@@ -30,11 +30,11 @@ describe Groups::TransferService, :postgresql do
create(:group_member, :owner, group: new_parent_group, user: user)
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: namespace directory cannot be moved')
end
@@ -50,7 +50,7 @@ describe Groups::TransferService, :postgresql do
context 'when the group is already a root group' do
let(:group) { create(:group, :public) }
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(nil)
expect(transfer_service.error).to eq('Transfer failed: Group is already a root group.')
end
@@ -59,11 +59,11 @@ describe Groups::TransferService, :postgresql do
context 'when the user does not have the right policies' do
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
- it "should return false" do
+ it "returns false" do
expect(transfer_service.execute(nil)).to be_falsy
end
- it "should add an error on group" do
+ it "adds an error on group" do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.")
end
@@ -76,11 +76,11 @@ describe Groups::TransferService, :postgresql do
create(:group, path: 'not-unique')
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(nil)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(nil)
expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.')
end
@@ -96,17 +96,17 @@ describe Groups::TransferService, :postgresql do
group.reload
end
- it 'should update group attributes' do
+ it 'updates group attributes' do
expect(group.parent).to be_nil
end
- it 'should update group children path' do
+ it 'updates group children path' do
group.children.each do |subgroup|
expect(subgroup.full_path).to eq("#{group.path}/#{subgroup.path}")
end
end
- it 'should update group projects path' do
+ it 'updates group projects path' do
group.projects.each do |project|
expect(project.full_path).to eq("#{group.path}/#{project.path}")
end
@@ -122,11 +122,11 @@ describe Groups::TransferService, :postgresql do
context 'when the new parent group is the same as the previous parent group' do
let(:group) { create(:group, :public, :nested, parent: new_parent_group) }
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Group is already associated to the parent group.')
end
@@ -135,11 +135,11 @@ describe Groups::TransferService, :postgresql do
context 'when the user does not have the right policies' do
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
- it "should return false" do
+ it "returns false" do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it "should add an error on group" do
+ it "adds an error on group" do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.")
end
@@ -152,11 +152,11 @@ describe Groups::TransferService, :postgresql do
create(:group, path: "not-unique", parent: new_parent_group)
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup with the same path.')
end
@@ -171,11 +171,11 @@ describe Groups::TransferService, :postgresql do
group.update_attribute(:path, 'foo')
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
@@ -191,7 +191,7 @@ describe Groups::TransferService, :postgresql do
let(:new_parent_group) { create(:group, :public) }
let(:group) { create(:group, :private, :nested) }
- it 'should not update the visibility for the group' do
+ it 'does not update the visibility for the group' do
group.reload
expect(group.private?).to be_truthy
expect(group.visibility_level).not_to eq(new_parent_group.visibility_level)
@@ -202,27 +202,27 @@ describe Groups::TransferService, :postgresql do
let(:new_parent_group) { create(:group, :private) }
let(:group) { create(:group, :public, :nested) }
- it 'should update visibility level based on the parent group' do
+ it 'updates visibility level based on the parent group' do
group.reload
expect(group.private?).to be_truthy
expect(group.visibility_level).to eq(new_parent_group.visibility_level)
end
end
- it 'should update visibility for the group based on the parent group' do
+ it 'updates visibility for the group based on the parent group' do
expect(group.visibility_level).to eq(new_parent_group.visibility_level)
end
- it 'should update parent group to the new parent ' do
+ it 'updates parent group to the new parent' do
expect(group.parent).to eq(new_parent_group)
end
- it 'should return the group as children of the new parent' do
+ it 'returns the group as children of the new parent' do
expect(new_parent_group.children.count).to eq(1)
expect(new_parent_group.children.first).to eq(group)
end
- it 'should create a redirect for the group' do
+ it 'creates a redirect for the group' do
expect(group.redirect_routes.count).to eq(1)
end
end
@@ -236,21 +236,21 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update subgroups path' do
+ it 'updates subgroups path' do
new_parent_path = new_parent_group.path
group.children.each do |subgroup|
expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}")
end
end
- it 'should create redirects for the subgroups' do
+ it 'creates redirects for the subgroups' do
expect(group.redirect_routes.count).to eq(1)
expect(subgroup1.redirect_routes.count).to eq(1)
expect(subgroup2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the children' do
- it 'should not update the children visibility' do
+ it 'does not update the children visibility' do
expect(subgroup1.private?).to be_truthy
expect(subgroup2.internal?).to be_truthy
end
@@ -261,7 +261,7 @@ describe Groups::TransferService, :postgresql do
let!(:subgroup2) { create(:group, :public, parent: group) }
let(:new_parent_group) { create(:group, :private) }
- it 'should update children visibility to match the new parent' do
+ it 'updates children visibility to match the new parent' do
group.children.each do |subgroup|
expect(subgroup.private?).to be_truthy
end
@@ -279,21 +279,21 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update projects path' do
+ it 'updates projects path' do
new_parent_path = new_parent_group.path
group.projects.each do |project|
expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}")
end
end
- it 'should create permanent redirects for the projects' do
+ it 'creates permanent redirects for the projects' do
expect(group.redirect_routes.count).to eq(1)
expect(project1.redirect_routes.count).to eq(1)
expect(project2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the projects' do
- it 'should not update projects visibility' do
+ it 'does not update projects visibility' do
expect(project1.private?).to be_truthy
expect(project2.internal?).to be_truthy
end
@@ -304,7 +304,7 @@ describe Groups::TransferService, :postgresql do
let!(:project2) { create(:project, :repository, :public, namespace: group) }
let(:new_parent_group) { create(:group, :private) }
- it 'should update projects visibility to match the new parent' do
+ it 'updates projects visibility to match the new parent' do
group.projects.each do |project|
expect(project.private?).to be_truthy
end
@@ -324,21 +324,21 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update subgroups path' do
+ it 'updates subgroups path' do
new_parent_path = new_parent_group.path
group.children.each do |subgroup|
expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}")
end
end
- it 'should update projects path' do
+ it 'updates projects path' do
new_parent_path = new_parent_group.path
group.projects.each do |project|
expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}")
end
end
- it 'should create redirect for the subgroups and projects' do
+ it 'creates redirect for the subgroups and projects' do
expect(group.redirect_routes.count).to eq(1)
expect(subgroup1.redirect_routes.count).to eq(1)
expect(subgroup2.redirect_routes.count).to eq(1)
@@ -360,7 +360,7 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update subgroups path' do
+ it 'updates subgroups path' do
new_base_path = "#{new_parent_group.path}/#{group.path}"
group.children.each do |children|
expect(children.full_path).to eq("#{new_base_path}/#{children.path}")
@@ -372,7 +372,7 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should update projects path' do
+ it 'updates projects path' do
new_parent_path = "#{new_parent_group.path}/#{group.path}"
subgroup1.projects.each do |project|
project_full_path = "#{new_parent_path}/#{project.namespace.path}/#{project.name}"
@@ -380,7 +380,7 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create redirect for the subgroups and projects' do
+ it 'creates redirect for the subgroups and projects' do
expect(group.redirect_routes.count).to eq(1)
expect(project1.redirect_routes.count).to eq(1)
expect(subgroup1.redirect_routes.count).to eq(1)
@@ -402,7 +402,7 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should restore group and projects visibility' do
+ it 'restores group and projects visibility' do
subgroup1.reload
project1.reload
expect(subgroup1.public?).to be_truthy
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 86e58fe06b9..74f1e83b362 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -58,8 +58,10 @@ describe Issues::BuildService do
"> That has a quote\n"\
">>>\n"
note_result = " > This is a string\n"\
+ " > \n"\
" > > with a blockquote\n"\
- " > > > That has a quote\n"
+ " > > > That has a quote\n"\
+ " > \n"
discussion = create(:diff_note_on_merge_request, note: note_text).to_discussion
expect(service.item_for_discussion(discussion)).to include(note_result)
end
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index af0a214c00f..39a2ef579dd 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -77,6 +77,22 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
service.execute(commit_status)
end
end
+
+ context 'when build belongs to a merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event,
+ ref: merge_request.merge_ref_path,
+ merge_request: merge_request,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let(:commit_status) { create(:ci_build, ref: merge_request.merge_ref_path, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
+ service.execute(commit_status)
+ end
+ end
end
describe '#close' do
@@ -106,6 +122,22 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
service.close(commit_status)
end
end
+
+ context 'when build belongs to a merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event,
+ ref: merge_request.merge_ref_path,
+ merge_request: merge_request,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let(:commit_status) { create(:ci_build, ref: merge_request.merge_ref_path, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_retried).with(merge_request)
+ service.close(commit_status)
+ end
+ end
end
describe '#close_all' do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 55e7b46248b..dc5d1cf2f04 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -263,19 +263,6 @@ describe MergeRequests::CreateService do
expect(merge_request.actual_head_pipeline).to be_merge_request_event
end
end
-
- context "when the 'ci_merge_request_pipeline' feature flag is disabled" do
- before do
- stub_feature_flags(ci_merge_request_pipeline: false)
- end
-
- it 'does not create a detached merge request pipeline' do
- expect(merge_request).to be_persisted
-
- merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(0)
- end
- end
end
context "when .gitlab-ci.yml does not have merge_requests keywords" do
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index 52bbd4e794d..8b7db1b2f1f 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -95,7 +95,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
sha: '1234abcdef', status: 'success')
end
- it 'it does not merge request' do
+ it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
service.trigger(old_pipeline)
end
@@ -112,6 +112,21 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
service.trigger(unrelated_pipeline)
end
end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, :success,
+ source: :merge_request_event,
+ ref: mr_merge_if_green_enabled.merge_ref_path,
+ merge_request: mr_merge_if_green_enabled,
+ merge_requests_as_head_pipeline: [mr_merge_if_green_enabled])
+ end
+
+ it 'merges the associated merge request' do
+ expect(MergeWorker).to receive(:perform_async)
+ service.trigger(pipeline)
+ end
+ end
end
describe "#cancel" do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 25cbac6d7ee..bd10523bc94 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -230,17 +230,6 @@ describe MergeRequests::RefreshService do
end.not_to change { @merge_request.merge_request_pipelines.count }
end
end
-
- context "when the 'ci_merge_request_pipeline' feature flag is disabled" do
- before do
- stub_feature_flags(ci_merge_request_pipeline: false)
- end
-
- it 'does not create a detached merge request pipeline' do
- expect { subject }
- .not_to change { @merge_request.merge_request_pipelines.count }
- end
- end
end
context "when .gitlab-ci.yml does not have merge_requests keywords" do
@@ -465,35 +454,35 @@ describe MergeRequests::RefreshService do
end
let(:force_push_commit) { @project.commit('feature').id }
- it 'should reload a new diff for a push to the forked project' do
+ it 'reloads a new diff for a push to the forked project' do
expect do
service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should reload a new diff for a force push to the source branch' do
+ it 'reloads a new diff for a force push to the source branch' do
expect do
service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should reload a new diff for a force push to the target branch' do
+ it 'reloads a new diff for a force push to the target branch' do
expect do
service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should reload a new diff for a push to the target project that contains a commit in the MR' do
+ it 'reloads a new diff for a push to the target project that contains a commit in the MR' do
expect do
service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should not increase the diff count for a new push to target branch' do
+ it 'does not increase the diff count for a new push to target branch' do
new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file',
message: 'This is a test',
branch_name: 'master')
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index af4daff336b..96fff20f7fb 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -128,37 +128,19 @@ describe Notes::BuildService do
subject { described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute }
- shared_examples 'an individual note reply' do
- it 'builds another individual note' do
- expect(subject).to be_valid
- expect(subject).to be_a(Note)
- expect(subject.discussion_id).not_to eq(note.discussion_id)
- end
+ it 'sets the note up to be in reply to that note' do
+ expect(subject).to be_valid
+ expect(subject).to be_a(DiscussionNote)
+ expect(subject.discussion_id).to eq(note.discussion_id)
end
- context 'when reply_to_individual_notes is disabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: false)
- end
-
- it_behaves_like 'an individual note reply'
- end
+ context 'when noteable does not support replies' do
+ let(:note) { create(:note_on_commit) }
- context 'when reply_to_individual_notes is enabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: true)
- end
-
- it 'sets the note up to be in reply to that note' do
+ it 'builds another individual note' do
expect(subject).to be_valid
- expect(subject).to be_a(DiscussionNote)
- expect(subject.discussion_id).to eq(note.discussion_id)
- end
-
- context 'when noteable does not support replies' do
- let(:note) { create(:note_on_commit) }
-
- it_behaves_like 'an individual note reply'
+ expect(subject).to be_a(Note)
+ expect(subject.discussion_id).not_to eq(note.discussion_id)
end
end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 8d8e81173ff..bcbb8950910 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -298,41 +298,20 @@ describe Notes::CreateService do
subject { described_class.new(project, user, reply_opts).execute }
- context 'when reply_to_individual_notes is disabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: false)
- end
-
- it 'creates an individual note' do
- expect(subject.type).to eq(nil)
- expect(subject.discussion_id).not_to eq(existing_note.discussion_id)
- end
-
- it 'does not convert existing note' do
- expect { subject }.not_to change { existing_note.reload.type }
- end
+ it 'creates a DiscussionNote in reply to existing note' do
+ expect(subject).to be_a(DiscussionNote)
+ expect(subject.discussion_id).to eq(existing_note.discussion_id)
end
- context 'when reply_to_individual_notes is enabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: true)
- end
-
- it 'creates a DiscussionNote in reply to existing note' do
- expect(subject).to be_a(DiscussionNote)
- expect(subject.discussion_id).to eq(existing_note.discussion_id)
- end
-
- it 'converts existing note to DiscussionNote' do
- expect do
- existing_note
+ it 'converts existing note to DiscussionNote' do
+ expect do
+ existing_note
- Timecop.freeze(Time.now + 1.minute) { subject }
+ Timecop.freeze(Time.now + 1.minute) { subject }
- existing_note.reload
- end.to change { existing_note.type }.from(nil).to('DiscussionNote')
- .and change { existing_note.updated_at }
- end
+ existing_note.reload
+ end.to change { existing_note.type }.from(nil).to('DiscussionNote')
+ .and change { existing_note.updated_at }
end
end
end
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index 7d2b6d5b8a7..9efdf96bc64 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -185,6 +185,7 @@ describe Notes::QuickActionsService do
end
before do
+ stub_licensed_features(multiple_issue_assignees: false)
project.add_maintainer(maintainer)
project.add_maintainer(assignee)
end
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index 85515d548a7..a1d31464e07 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe PreviewMarkdownService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
project.add_developer(user)
@@ -20,23 +20,72 @@ describe PreviewMarkdownService do
end
describe 'suggestions' do
- let(:params) { { text: "```suggestion\nfoo\n```", preview_suggestions: preview_suggestions } }
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project)
+ end
+ let(:text) { "```suggestion\nfoo\n```" }
+ let(:params) do
+ suggestion_params.merge(text: text,
+ target_type: 'MergeRequest',
+ target_id: merge_request.iid)
+ end
let(:service) { described_class.new(project, user, params) }
context 'when preview markdown param is present' do
- let(:preview_suggestions) { true }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:line) { 10 }
+ let(:diff_refs) { merge_request.diff_refs }
+
+ let(:suggestion_params) do
+ {
+ preview_suggestions: true,
+ file_path: path,
+ line: line,
+ base_sha: diff_refs.base_sha,
+ start_sha: diff_refs.start_sha,
+ head_sha: diff_refs.head_sha
+ }
+ end
+
+ it 'returns suggestions referenced in text' do
+ position = Gitlab::Diff::Position.new(new_path: path,
+ new_line: line,
+ diff_refs: diff_refs)
+
+ expect(Gitlab::Diff::SuggestionsParser)
+ .to receive(:parse)
+ .with(text, position: position, project: merge_request.project)
+ .and_call_original
- it 'returns users referenced in text' do
result = service.execute
- expect(result[:suggestions]).to eq(['foo'])
+ expect(result[:suggestions]).to all(be_a(Gitlab::Diff::Suggestion))
+ end
+
+ context 'when user is not authorized' do
+ let(:another_user) { create(:user) }
+ let(:service) { described_class.new(project, another_user, params) }
+
+ before do
+ project.add_guest(another_user)
+ end
+
+ it 'returns no suggestions' do
+ result = service.execute
+
+ expect(result[:suggestions]).to be_empty
+ end
end
end
context 'when preview markdown param is not present' do
- let(:preview_suggestions) { false }
+ let(:suggestion_params) do
+ {
+ preview_suggestions: false
+ }
+ end
- it 'returns users referenced in text' do
+ it 'returns suggestions referenced in text' do
result = service.execute
expect(result[:suggestions]).to eq([])
@@ -49,8 +98,8 @@ describe PreviewMarkdownService do
let(:params) do
{
text: "Please do it\n/assign #{user.to_reference}",
- quick_actions_target_type: 'Issue',
- quick_actions_target_id: issue.id
+ target_type: 'Issue',
+ target_id: issue.id
}
end
let(:service) { described_class.new(project, user, params) }
@@ -72,7 +121,7 @@ describe PreviewMarkdownService do
let(:params) do
{
text: "My work\n/estimate 2y",
- quick_actions_target_type: 'MergeRequest'
+ target_type: 'MergeRequest'
}
end
let(:service) { described_class.new(project, user, params) }
@@ -96,8 +145,8 @@ describe PreviewMarkdownService do
let(:params) do
{
text: "My work\n/tag v1.2.3 Stable release",
- quick_actions_target_type: 'Commit',
- quick_actions_target_id: commit.id
+ target_type: 'Commit',
+ target_id: commit.id
}
end
let(:service) { described_class.new(project, user, params) }
diff --git a/spec/services/projects/auto_devops/disable_service_spec.rb b/spec/services/projects/auto_devops/disable_service_spec.rb
index 76977d7a1a7..fb1ab3f9949 100644
--- a/spec/services/projects/auto_devops/disable_service_spec.rb
+++ b/spec/services/projects/auto_devops/disable_service_spec.rb
@@ -46,7 +46,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create(:ci_pipeline, :failed, :auto_devops_source, project: project)
end
- it 'should disable Auto DevOps for project' do
+ it 'disables Auto DevOps for project' do
subject
expect(auto_devops.enabled).to eq(false)
@@ -58,7 +58,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create_list(:ci_pipeline, 2, :failed, :auto_devops_source, project: project)
end
- it 'should explicitly disable Auto DevOps for project' do
+ it 'explicitly disables Auto DevOps for project' do
subject
expect(auto_devops.reload.enabled).to eq(false)
@@ -70,7 +70,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create(:ci_pipeline, :success, :auto_devops_source, project: project)
end
- it 'should not disable Auto DevOps for project' do
+ it 'does not disable Auto DevOps for project' do
subject
expect(auto_devops.reload.enabled).to be_nil
@@ -85,14 +85,14 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create(:ci_pipeline, :failed, :auto_devops_source, project: project)
end
- it 'should disable Auto DevOps for project' do
+ it 'disables Auto DevOps for project' do
subject
auto_devops = project.reload.auto_devops
expect(auto_devops.enabled).to eq(false)
end
- it 'should create a ProjectAutoDevops record' do
+ it 'creates a ProjectAutoDevops record' do
expect { subject }.to change { ProjectAutoDevops.count }.from(0).to(1)
end
end
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 4b6d0c51363..8455b9bc3cf 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -41,12 +41,12 @@ describe Projects::ParticipantsService do
group.add_owner(user)
end
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
expect(service.groups.size).to eq 1
expect(service.groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png")
end
- it 'should return an url for the avatar with relative url' do
+ it 'returns an url for the avatar with relative url' do
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb
new file mode 100644
index 00000000000..4bdb20de4c9
--- /dev/null
+++ b/spec/services/prometheus/proxy_service_spec.rb
@@ -0,0 +1,195 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Prometheus::ProxyService do
+ include ReactiveCachingHelpers
+
+ set(:project) { create(:project) }
+ set(:environment) { create(:environment, project: project) }
+
+ describe '#initialize' do
+ let(:params) { ActionController::Parameters.new(query: '1').permit! }
+
+ it 'initializes attributes' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.proxyable).to eq(environment)
+ expect(result.method).to eq('GET')
+ expect(result.path).to eq('query')
+ expect(result.params).to eq('query' => '1')
+ end
+
+ it 'converts ActionController::Parameters into hash' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.params).to be_an_instance_of(Hash)
+ end
+
+ context 'with unknown params' do
+ let(:params) { ActionController::Parameters.new(query: '1', other_param: 'val').permit! }
+
+ it 'filters unknown params' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.params).to eq('query' => '1')
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:prometheus_adapter) { instance_double(PrometheusService) }
+ let(:params) { ActionController::Parameters.new(query: '1').permit! }
+
+ subject { described_class.new(environment, 'GET', 'query', params) }
+
+ context 'when prometheus_adapter is nil' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(nil)
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'No prometheus server found',
+ http_status: :service_unavailable
+ )
+ end
+ end
+
+ context 'when prometheus_adapter cannot query' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(false)
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'No prometheus server found',
+ http_status: :service_unavailable
+ )
+ end
+ end
+
+ context 'cannot proxy' do
+ subject { described_class.new(environment, 'POST', 'garbage', params) }
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ message: 'Proxy support for this API is not available currently',
+ status: :error
+ )
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ let(:return_value) { { 'http_status' => 200, 'body' => 'body' } }
+
+ let(:opts) do
+ [environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }]
+ end
+
+ before do
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ end
+
+ context 'when value present in cache' do
+ before do
+ stub_reactive_cache(subject, return_value, opts)
+ end
+
+ it 'returns cached value' do
+ result = subject.execute
+
+ expect(result[:http_status]).to eq(return_value[:http_status])
+ expect(result[:body]).to eq(return_value[:body])
+ end
+ end
+
+ context 'when value not present in cache' do
+ it 'returns nil' do
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(subject.class, subject.id, *opts)
+
+ result = subject.execute
+
+ expect(result).to eq(nil)
+ end
+ end
+ end
+
+ context 'call prometheus api' do
+ let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) }
+
+ before do
+ synchronous_reactive_cache(subject)
+
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ allow(prometheus_adapter).to receive(:prometheus_client_wrapper)
+ .and_return(prometheus_client)
+ end
+
+ context 'connection to prometheus server succeeds' do
+ let(:rest_client_response) { instance_double(RestClient::Response) }
+ let(:prometheus_http_status_code) { 400 }
+
+ let(:response_body) do
+ '{"status":"error","errorType":"bad_data","error":"parse error at char 1: no expression found in input"}'
+ end
+
+ before do
+ allow(prometheus_client).to receive(:proxy).and_return(rest_client_response)
+
+ allow(rest_client_response).to receive(:code)
+ .and_return(prometheus_http_status_code)
+ allow(rest_client_response).to receive(:body).and_return(response_body)
+ end
+
+ it 'returns the http status code and body from prometheus' do
+ expect(subject.execute).to eq(
+ http_status: prometheus_http_status_code,
+ body: response_body,
+ status: :success
+ )
+ end
+ end
+
+ context 'connection to prometheus server fails' do
+ context 'prometheus client raises Gitlab::PrometheusClient::Error' do
+ before do
+ allow(prometheus_client).to receive(:proxy)
+ .and_raise(Gitlab::PrometheusClient::Error, 'Network connection error')
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'Network connection error',
+ http_status: :service_unavailable
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe '.from_cache' do
+ it 'initializes an instance of ProxyService class' do
+ result = described_class.from_cache(
+ environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }
+ )
+
+ expect(result).to be_an_instance_of(described_class)
+ expect(result.proxyable).to eq(environment)
+ expect(result.method).to eq('GET')
+ expect(result.path).to eq('query')
+ expect(result.params).to eq('query' => '1')
+ end
+ end
+end
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index 80b5dcac6c7..7732767137c 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -51,6 +51,10 @@ describe Suggestions::ApplyService do
diff_refs: merge_request.diff_refs)
end
+ let(:diff_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project)
+ end
+
let(:suggestion) do
create(:suggestion, :content_from_repo, note: diff_note,
to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n")
@@ -108,12 +112,6 @@ describe Suggestions::ApplyService do
target_project: project)
end
- let!(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position,
- project: project)
- end
-
before do
project.add_maintainer(user)
end
@@ -192,11 +190,6 @@ describe Suggestions::ApplyService do
CONTENT
end
- let(:merge_request) do
- create(:merge_request, source_project: project,
- target_project: project)
- end
-
def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:)
position = Gitlab::Diff::Position.new(old_path: path,
new_path: path,
@@ -291,6 +284,55 @@ describe Suggestions::ApplyService do
expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip)
end
end
+
+ context 'multi-line suggestion' do
+ let(:expected_content) do
+ <<~CONTENT
+ require 'fileutils'
+ require 'open3'
+
+ module Popen
+ extend self
+
+ # multi
+ # line
+
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+ end
+ CONTENT
+ end
+
+ let(:suggestion) do
+ create(:suggestion, :content_from_repo, note: diff_note,
+ lines_above: 2,
+ lines_below: 3,
+ to_content: "# multi\n# line\n")
+ end
+
+ it_behaves_like 'successfully creates commit and updates suggestion'
+ end
end
context 'fork-project' do
diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb
index b1260cf740a..9adaee6481b 100644
--- a/spec/services/task_list_toggle_service_spec.rb
+++ b/spec/services/task_list_toggle_service_spec.rb
@@ -113,4 +113,25 @@ describe TaskListToggleService do
expect(toggler.execute).to be_falsey
end
+
+ it 'properly handles a GitLab blockquote' do
+ markdown =
+ <<-EOT.strip_heredoc
+ >>>
+ gitlab blockquote
+ >>>
+
+ * [ ] Task 1
+ * [x] Task 2
+ EOT
+
+ markdown_html = Banzai::Pipeline::FullPipeline.call(markdown, project: nil)[:output].to_html
+ toggler = described_class.new(markdown, markdown_html,
+ toggle_as_checked: true,
+ line_source: '* [ ] Task 1', line_number: 5)
+
+ expect(toggler.execute).to be_truthy
+ expect(toggler.updated_markdown.lines[4]).to eq "* [x] Task 1\n"
+ expect(toggler.updated_markdown_html).to include('disabled checked> Task 1')
+ end
end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 42a086d58d2..5b79c40f27b 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -224,7 +224,7 @@ shared_examples 'discussion comments' do |resource_name|
find(toggle_selector).click
end
- it 'should have "Start discussion" selected' do
+ it 'has "Start discussion" selected' do
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
@@ -267,7 +267,7 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'should have "Comment" selected when opening the menu' do
+ it 'has "Comment" selected when opening the menu' do
find(toggle_selector).click
find("#{menu_selector} li", match: :first)
diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb
index 73156d18c1b..693b796fbdc 100644
--- a/spec/support/features/variable_list_shared_examples.rb
+++ b/spec/support/features/variable_list_shared_examples.rb
@@ -23,10 +23,13 @@ shared_examples 'variable list' do
end
end
- it 'adds empty variable' do
+ it 'adds a new protected variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
- find('.js-ci-variable-input-value').set('')
+ find('.js-ci-variable-input-value').set('key_value')
+ find('.ci-variable-protected-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
click_button('Save variables')
@@ -37,17 +40,17 @@ shared_examples 'variable list' do
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
- expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
+ expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
+ expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
end
- it 'adds new protected variable' do
+ it 'defaults to masked' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key_value')
- find('.ci-variable-protected-item .js-project-feature-toggle').click
- expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
end
click_button('Save variables')
@@ -59,7 +62,7 @@ shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
- expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
end
end
@@ -163,27 +166,6 @@ shared_examples 'variable list' do
end
end
- it 'edits variable with empty value' do
- page.within('.js-ci-variable-list-section') do
- click_button('Reveal value')
-
- page.within('.js-row:nth-child(1)') do
- find('.js-ci-variable-input-key').set('new_key')
- find('.js-ci-variable-input-value').set('')
- end
-
- click_button('Save variables')
- wait_for_requests
-
- visit page_path
-
- page.within('.js-row:nth-child(1)') do
- expect(find('.js-ci-variable-input-key').value).to eq('new_key')
- expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
- end
- end
- end
-
it 'edits variable to be protected' do
# Create the unprotected variable
page.within('.js-ci-variable-list-section .js-row:last-child') do
@@ -251,6 +233,57 @@ shared_examples 'variable list' do
end
end
+ it 'edits variable to be unmasked' do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ visit page_path
+
+ page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ end
+ end
+
+ it 'edits variable to be masked' do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ visit page_path
+
+ page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ visit page_path
+
+ page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ end
+ end
+
it 'handles multiple edits and deletion in the middle' do
page.within('.js-ci-variable-list-section') do
# Create 2 variables
@@ -297,11 +330,11 @@ shared_examples 'variable list' do
it 'shows validation error box about duplicate keys' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
- find('.js-ci-variable-input-value').set('value1')
+ find('.js-ci-variable-input-value').set('value123')
end
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
- find('.js-ci-variable-input-value').set('value2')
+ find('.js-ci-variable-input-value').set('value456')
end
click_button('Save variables')
@@ -314,4 +347,34 @@ shared_examples 'variable list' do
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/)
end
end
+
+ it 'shows validation error box about empty values' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('empty_value')
+ find('.js-ci-variable-input-value').set('')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ page.within('.js-ci-variable-list-section') do
+ expect(all('.js-ci-variable-error-box ul li').count).to eq(1)
+ expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/)
+ end
+ end
+
+ it 'shows validation error box about unmaskable values' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('unmaskable_value')
+ find('.js-ci-variable-input-value').set('???')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ page.within('.js-ci-variable-list-section') do
+ expect(all('.js-ci-variable-error-box ul li').count).to eq(1)
+ expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/)
+ end
+ end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index ca28325eab9..f59f42ee902 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -93,6 +93,8 @@ module GraphqlHelpers
end
def all_graphql_fields_for(class_name, parent_types = Set.new)
+ allow_unlimited_graphql_complexity
+
type = GitlabSchema.types[class_name.to_s]
return "" unless type
@@ -170,4 +172,10 @@ module GraphqlHelpers
field_type
end
+
+ # for most tests, we want to allow unlimited complexity
+ def allow_unlimited_graphql_complexity
+ allow_any_instance_of(GitlabSchema).to receive(:max_complexity).and_return nil
+ allow(GitlabSchema).to receive(:max_query_complexity).with(any_args).and_return nil
+ end
end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index ce1f9fce10d..87f825152cf 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -7,6 +7,10 @@ module PrometheusHelpers
%{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
end
+ def prometheus_istio_query(function_name, kube_namespace)
+ %{floor(sum(rate(istio_revision_request_count{destination_configuration=\"#{function_name}\", destination_namespace=\"#{kube_namespace}\"}[1m])*30))}
+ end
+
def prometheus_ping_url(prometheus_query)
query = { query: prometheus_query }.to_query
@@ -25,12 +29,16 @@ module PrometheusHelpers
"https://prometheus.example.com/api/v1/query?#{query}"
end
- def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f)
+ def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now, step: nil)
+ start = start.to_f
+ stop = stop.to_f
+ step ||= Gitlab::PrometheusClient.compute_step(start, stop)
+
query = {
query: prometheus_query,
- start: start.to_f,
+ start: start,
end: stop,
- step: 1.minute.to_i
+ step: step
}.to_query
"https://prometheus.example.com/api/v1/query_range?#{query}"
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index a8b00004fe7..6aa59960092 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -90,7 +90,7 @@ RSpec.shared_examples "redis_shared_examples" do
subject { described_class._raw_config }
let(:config_file_name) { '/var/empty/doesnotexist' }
- it 'should be frozen' do
+ it 'is frozen' do
expect(subject).to be_frozen
end
diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb
index 3ad6e067674..ee5cfcd850d 100644
--- a/spec/support/shared_context/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_context/policies/project_policy_shared_context.rb
@@ -25,6 +25,7 @@ RSpec.shared_context 'ProjectPolicy context' do
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_release
+ read_prometheus
]
end
diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
index 98ab04c5636..eb051166a69 100644
--- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
@@ -4,7 +4,7 @@ shared_examples 'set sort order from user preference' do
# however any other field present in user_preferences table can be used for testing.
context 'when database is in read-only mode' do
- it 'it does not update user preference' do
+ it 'does not update user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index ba9b7d3bdcf..01bee603274 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -6,7 +6,7 @@ shared_examples 'helm commands' do
EOS
end
- it 'should return appropriate command' do
+ it 'returns appropriate command' do
expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
end
end
diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
index 90d67fd00fc..244f4766a84 100644
--- a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
@@ -1,11 +1,11 @@
shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
include ProjectForksHelper
- def get_action(action, project)
+ def get_action(action, project, extra_params = {})
if action
- get action, params: { author_id: project.creator.id }
+ get action, params: { author_id: project.creator.id }.merge(extra_params)
else
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ get :index, params: { namespace_id: project.namespace, project_id: project }.merge(extra_params)
end
end
@@ -17,23 +17,44 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
end
end
- before do
- @issuable_ids = %w[fix improve/awesome].map do |source_branch|
- create_issuable(issuable_type, project, source_branch: source_branch).id
+ let!(:issuables) do
+ %w[fix improve/awesome].map do |source_branch|
+ create_issuable(issuable_type, project, source_branch: source_branch)
end
end
+ let(:issuable_ids) { issuables.map(&:id) }
+
it "creates indexed meta-data object for issuable notes and votes count" do
get_action(action, project)
meta_data = assigns(:issuable_meta_data)
aggregate_failures do
- expect(meta_data.keys).to match_array(@issuable_ids)
+ expect(meta_data.keys).to match_array(issuables.map(&:id))
expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
end
end
+ context 'searching' do
+ let(:result_issuable) { issuables.first }
+ let(:search) { result_issuable.title }
+
+ before do
+ stub_feature_flags(attempt_project_search_optimizations: true)
+ end
+
+ # .simple_sorts is the same across all Sortable classes
+ sorts = ::Issue.simple_sorts.keys + %w[popularity priority label_priority]
+ sorts.each do |sort|
+ it "works when sorting by #{sort}" do
+ get_action(action, project, search: search, sort: sort)
+
+ expect(assigns(:issuable_meta_data).keys).to include(result_issuable.id)
+ end
+ end
+ end
+
it "avoids N+1 queries" do
control = ActiveRecord::QueryRecorder.new { get_action(action, project) }
issuable = create_issuable(issuable_type, project, source_branch: 'csv')
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
index d87b3181e80..033b65bdc84 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
@@ -9,12 +9,12 @@ shared_examples 'cluster application helm specs' do |application_name|
application.cluster.application_helm.ca_cert = nil
end
- it 'should not include cert files when there is no ca_cert entry' do
+ it 'does not include cert files when there is no ca_cert entry' do
expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
end
end
- it 'should include cert files when there is a ca_cert entry' do
+ it 'includes cert files when there is a ca_cert entry' do
expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem')
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
index 4604d867507..b337a1c18d8 100644
--- a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
@@ -1,4 +1,28 @@
# frozen_string_literal: true
shared_examples 'tag quick action' do
+ context "post note to existing commit" do
+ it 'tags this commit' do
+ add_note("/tag #{tag_name} #{tag_message}")
+
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content "tagged commit #{truncated_commit_sha}"
+ expect(page).to have_content tag_name
+
+ visit project_tag_path(project, tag_name)
+ expect(page).to have_content tag_name
+ expect(page).to have_content tag_message
+ expect(page).to have_content truncated_commit_sha
+ end
+ end
+
+ context 'preview', :js do
+ it 'removes quick action from note and explains it' do
+ preview_note("/tag #{tag_name} #{tag_message}")
+
+ expect(page).not_to have_content '/tag'
+ expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"}
+ expect(page).to have_content tag_name
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
index c68e5aee842..336500487fe 100644
--- a/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
@@ -1,4 +1,35 @@
# frozen_string_literal: true
shared_examples 'confidential quick action' do
+ context 'when the current user can update issues' do
+ it 'does not create a note, and marks the issue as confidential' do
+ add_note('/confidential')
+
+ expect(page).not_to have_content '/confidential'
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content 'made the issue confidential'
+
+ expect(issue.reload).to be_confidential
+ end
+ end
+
+ context 'when the current user cannot update the issue' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not create a note, and does not mark the issue as confidential' do
+ add_note('/confidential')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content 'made the issue confidential'
+
+ expect(issue.reload).not_to be_confidential
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
index 5904164fcfc..dd1676a08e2 100644
--- a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
@@ -1,25 +1,35 @@
# frozen_string_literal: true
-shared_examples 'remove_due_date action not available' do
- it 'does not remove the due date' do
- add_note("/remove_due_date")
+shared_examples 'remove_due_date quick action' do
+ context 'remove_due_date action available and due date can be removed' do
+ it 'removes the due date accordingly' do
+ add_note('/remove_due_date')
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content '/remove_due_date'
- end
-end
+ expect(page).not_to have_content '/remove_due_date'
+ expect(page).to have_content 'Commands applied'
+
+ visit project_issue_path(project, issue)
-shared_examples 'remove_due_date action available and due date can be removed' do
- it 'removes the due date accordingly' do
- add_note('/remove_due_date')
+ page.within '.due_date' do
+ expect(page).to have_content 'No due date'
+ end
+ end
+ end
- expect(page).not_to have_content '/remove_due_date'
- expect(page).to have_content 'Commands applied'
+ context 'remove_due_date action not available' do
+ let(:guest) { create(:user) }
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
- visit project_issue_path(project, issue)
+ it 'does not remove the due date' do
+ add_note("/remove_due_date")
- page.within '.due_date' do
- expect(page).to have_content 'No due date'
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content '/remove_due_date'
end
end
end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
index 31d88183f0d..c454ddc4bba 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -1,4 +1,51 @@
# frozen_string_literal: true
shared_examples 'merge quick action' do
+ context 'when the current user can merge the MR' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'merges the MR' do
+ add_note("/merge")
+
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload).to be_merged
+ end
+ end
+
+ context 'when the head diff changes in the meanwhile' do
+ before do
+ merge_request.source_branch = 'another_branch'
+ merge_request.save
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ add_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+
+ context 'when the current user cannot merge the MR' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ add_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
index ccb4a85325b..cf2bdb1dd68 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
@@ -1,4 +1,81 @@
# frozen_string_literal: true
shared_examples 'target_branch quick action' do
+ describe '/target_branch command in merge request' do
+ let(:another_project) { create(:project, :public, :repository) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+
+ before do
+ another_project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'changes target_branch in new merge_request' do
+ visit project_new_merge_request_path(another_project, new_url_opts)
+
+ fill_in "merge_request_title", with: 'My brand new feature'
+ fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
+ click_button "Submit merge request"
+
+ merge_request = another_project.merge_requests.first
+ expect(merge_request.description).to eq "le feature \nFeature description:"
+ expect(merge_request.target_branch).to eq 'fix'
+ end
+
+ it 'does not change target branch when merge request is edited' do
+ new_merge_request = create(:merge_request, source_project: another_project)
+
+ visit edit_project_merge_request_path(another_project, new_merge_request)
+ fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
+ click_button "Save changes"
+
+ new_merge_request = another_project.merge_requests.first
+ expect(new_merge_request.description).to include('/target_branch')
+ expect(new_merge_request.target_branch).not_to eq('fix')
+ end
+ end
+
+ describe '/target_branch command from note' do
+ context 'when the current user can change target branch' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'changes target branch from a note' do
+ add_note("message start \n/target_branch merge-test\n message end.")
+
+ wait_for_requests
+ expect(page).not_to have_content('/target_branch')
+ expect(page).to have_content('message start')
+ expect(page).to have_content('message end.')
+
+ expect(merge_request.reload.target_branch).to eq 'merge-test'
+ end
+
+ it 'does not fail when target branch does not exists' do
+ add_note('/target_branch totally_not_existing_branch')
+
+ expect(page).not_to have_content('/target_branch')
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+
+ context 'when current user can not change target branch' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not change target branch' do
+ add_note('/target_branch merge-test')
+
+ expect(page).not_to have_content '/target_branch merge-test'
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb
new file mode 100644
index 00000000000..78a8e49fd76
--- /dev/null
+++ b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+shared_examples 'logs kubernetes errors' do
+ let(:error_hash) do
+ {
+ service: service.class.name,
+ app_id: application.id,
+ project_ids: application.cluster.project_ids,
+ group_ids: [],
+ error_code: error_code
+ }
+ end
+
+ let(:logger_hash) do
+ error_hash.merge(
+ exception: error_name,
+ message: error_message,
+ backtrace: instance_of(Array)
+ )
+ end
+
+ it 'logs into kubernetes.log and Sentry' do
+ expect(service.send(:logger)).to receive(:error).with(logger_hash)
+
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
+ error,
+ extra: hash_including(error_hash)
+ )
+
+ service.execute
+ end
+end
diff --git a/spec/support/shared_examples/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/snippet_visibility_shared_examples.rb
index 4f662db2120..833c31a57cb 100644
--- a/spec/support/shared_examples/snippet_visibility_shared_examples.rb
+++ b/spec/support/shared_examples/snippet_visibility_shared_examples.rb
@@ -220,11 +220,11 @@ RSpec.shared_examples 'snippet visibility' do
end
context "For #{params[:project_type]} project and #{params[:user_type]} users" do
- it 'should agree with the read_project_snippet policy' do
+ it 'agrees with the read_project_snippet policy' do
expect(can?(user, :read_project_snippet, snippet)).to eq(outcome)
end
- it 'should return proper outcome' do
+ it 'returns proper outcome' do
results = described_class.new(user, project: project).execute
expect(results.include?(snippet)).to eq(outcome)
@@ -232,7 +232,7 @@ RSpec.shared_examples 'snippet visibility' do
end
context "Without a given project and #{params[:user_type]} users" do
- it 'should return proper outcome' do
+ it 'returns proper outcome' do
results = described_class.new(user).execute
expect(results.include?(snippet)).to eq(outcome)
end
@@ -283,16 +283,16 @@ RSpec.shared_examples 'snippet visibility' do
let!(:snippet) { create(:personal_snippet, visibility_level: snippet_visibility, author: author) }
context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do
- it 'should agree with read_personal_snippet policy' do
+ it 'agrees with read_personal_snippet policy' do
expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome)
end
- it 'should return proper outcome' do
+ it 'returns proper outcome' do
results = described_class.new(user).execute
expect(results.include?(snippet)).to eq(outcome)
end
- it 'should return personal snippets when the user cannot read cross project' do
+ it 'returns personal snippets when the user cannot read cross project' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 3592a11360d..42352f9b9f8 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -71,7 +71,7 @@ describe RecordsUploads do
expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end
- it 'it destroys Upload records at the same path before recording' do
+ it 'destroys Upload records at the same path before recording' do
existing = Upload.create!(
path: File.join('uploads', 'rails_sample.jpg'),
size: 512.kilobytes,
@@ -88,10 +88,19 @@ describe RecordsUploads do
end
describe '#destroy_upload callback' do
- it 'it destroys Upload records at the same path after removal' do
+ it 'destroys Upload records at the same path after removal' do
uploader.store!(upload_fixture('rails_sample.jpg'))
expect { uploader.remove! }.to change { Upload.count }.from(1).to(0)
end
end
+
+ describe '#filename' do
+ it 'gets the filename from the path recorded in the database, not CarrierWave' do
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ expect_any_instance_of(GitlabUploader).not_to receive(:filename)
+
+ expect(uploader.filename).to eq('rails_sample.jpg')
+ end
+ end
end
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index 38cfb84f0d5..29e15960fb8 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -12,7 +12,7 @@ describe 'groups/edit.html.haml' do
end
shared_examples_for '"Share with group lock" setting' do |checkbox_options|
- it 'should have the correct label, help text, and checkbox options' do
+ it 'has the correct label, help text, and checkbox options' do
assign(:group, test_group)
allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 908ecb898e4..12925a5ab07 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -45,7 +45,7 @@ describe 'projects/_home_panel' do
context 'badges' do
shared_examples 'show badges' do
- it 'should render the all badges' do
+ it 'renders the all badges' do
render
expect(rendered).to have_selector('.project-badges a')
@@ -70,7 +70,7 @@ describe 'projects/_home_panel' do
context 'has no badges' do
let(:project) { create(:project) }
- it 'should not render any badge' do
+ it 'does not render any badge' do
render
expect(rendered).not_to have_selector('.project-badges')
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index b52fc719a64..ff2d491539b 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -13,4 +13,14 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
expect(rendered).to have_text('You must add a Kubernetes cluster integration to this project with a domain in order for your deployment strategy to work correctly.')
end
+
+ context 'when the project has an available kubernetes cluster' do
+ let!(:cluster) { create(:cluster, cluster_type: :project_type, projects: [project]) }
+
+ it 'does not show a warning message' do
+ render
+
+ expect(rendered).not_to have_text('You must add a Kubernetes cluster')
+ end
+ end
end
diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb
index 4769d569548..cbbb984935f 100644
--- a/spec/views/shared/milestones/_issuables.html.haml.rb
+++ b/spec/views/shared/milestones/_issuables.html.haml.rb
@@ -11,12 +11,12 @@ describe 'shared/milestones/_issuables.html.haml' do
stub_template 'shared/milestones/_issuable.html.haml' => ''
end
- it 'should show the issuables count if show_counter is true' do
+ it 'shows the issuables count if show_counter is true' do
render 'shared/milestones/issuables', show_counter: true
expect(rendered).to have_content('100')
end
- it 'should not show the issuables count if show_counter is false' do
+ it 'does not show the issuables count if show_counter is false' do
render 'shared/milestones/issuables', show_counter: false
expect(rendered).not_to have_content('100')
end
@@ -24,7 +24,7 @@ describe 'shared/milestones/_issuables.html.haml' do
describe 'a high issuables count' do
let(:issuables_size) { 1000 }
- it 'should show a delimited number if show_counter is true' do
+ it 'shows a delimited number if show_counter is true' do
render 'shared/milestones/issuables', show_counter: true
expect(rendered).to have_content('1,000')
end
diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb
index 3b14045e61f..dc223861037 100644
--- a/spec/views/shared/projects/_project.html.haml_spec.rb
+++ b/spec/views/shared/projects/_project.html.haml_spec.rb
@@ -8,13 +8,13 @@ describe 'shared/projects/_project.html.haml' do
allow(view).to receive(:can?) { true }
end
- it 'should render creator avatar if project has a creator' do
+ it 'renders creator avatar if project has a creator' do
render 'shared/projects/project', use_creator_avatar: true, project: project
expect(rendered).to have_selector('img.avatar')
end
- it 'should render a generic avatar if project does not have a creator' do
+ it 'renders a generic avatar if project does not have a creator' do
project.creator = nil
render 'shared/projects/project', use_creator_avatar: true, project: project
diff --git a/yarn.lock b/yarn.lock
index 15fa876300b..b0ce4d49562 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -645,17 +645,17 @@
dependencies:
bootstrap "^4.1.3"
-"@gitlab/eslint-config@^1.4.0":
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.4.0.tgz#2e59e55a7cd024e3a450d2a896060ec4d763a5dc"
- integrity sha512-nkecTWRNS/KD9q5lHFSc3J6zO/g1/OV9DaKiay+0nLjnGO9jQVRArRIYpnzgbUz2p15jOMVToVafW0YbbHZkwg==
+"@gitlab/eslint-config@^1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.5.0.tgz#0c8c3ae74f276eb6671bd7c60f331bc0f2d2e5cf"
+ integrity sha512-KgJgoIZNpGauFpCV1iCptesYN7I8abtYRBLU9xcH0oocC/xp3JmbLfsZ+lEtrk8pl99Q2mKiAuaPpzxjXr6hBw==
dependencies:
babel-eslint "^10.0.1"
eslint-config-airbnb-base "^13.1.0"
eslint-config-prettier "^3.3.0"
eslint-plugin-filenames "^1.3.2"
- eslint-plugin-import "^2.14.0"
- eslint-plugin-promise "^4.0.1"
+ eslint-plugin-import "^2.16.0"
+ eslint-plugin-promise "^4.1.1"
eslint-plugin-vue "^5.0.0"
"@gitlab/svgs@^1.58.0":
@@ -663,10 +663,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f"
integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg==
-"@gitlab/ui@^3.0.0":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.0.0.tgz#33ca2808dbd4395e69a366a219d1edc1f3dbccd5"
- integrity sha512-pDEa2k6ln5GE/N2z0V7dNEeFtSTW0p9ipO2/N9q6QMxO7fhhOhpMC0QVbdIljKTbglspDWI5v6BcqUjzYri5Pg==
+"@gitlab/ui@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.1.0.tgz#718fce208c14dc539859715a03978922c5b003b5"
+ integrity sha512-xCz8uCeP/4pzuQSoqfrFLeB0lfVHpvQ08r8/CkhSoDTpv34AXvmaxbv1PR8lIadRi43KuPTN639zHHKC2Z0b1g==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -1164,15 +1164,7 @@ apollo-link-http-common@^0.2.8:
ts-invariant "^0.3.2"
tslib "^1.9.3"
-apollo-link@^1.0.0, apollo-link@^1.2.3:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.3.tgz#9bd8d5fe1d88d31dc91dae9ecc22474d451fb70d"
- integrity sha512-iL9yS2OfxYhigme5bpTbmRyC+Htt6tyo2fRMHT3K1XRL/C5IQDDz37OjpPy4ndx7WInSvfSZaaOTKFja9VWqSw==
- dependencies:
- apollo-utilities "^1.0.0"
- zen-observable-ts "^0.8.10"
-
-apollo-link@^1.2.11, apollo-link@^1.2.6:
+apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3, apollo-link@^1.2.6:
version "1.2.11"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d"
integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==
@@ -1191,7 +1183,7 @@ apollo-upload-client@^10.0.0:
apollo-link-http-common "^0.2.8"
extract-files "^5.0.0"
-apollo-utilities@1.2.1, apollo-utilities@^1.0.0, apollo-utilities@^1.2.1:
+apollo-utilities@1.2.1, apollo-utilities@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c"
integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg==
@@ -3673,7 +3665,7 @@ eslint-import-resolver-jest@^2.1.1:
micromatch "^3.1.6"
resolve "^1.5.0"
-eslint-import-resolver-node@^0.3.1:
+eslint-import-resolver-node@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==
@@ -3697,13 +3689,13 @@ eslint-import-resolver-webpack@^0.10.1:
resolve "^1.4.0"
semver "^5.3.0"
-eslint-module-utils@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
- integrity sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=
+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==
dependencies:
debug "^2.6.8"
- pkg-dir "^1.0.0"
+ pkg-dir "^2.0.0"
eslint-plugin-filenames@^1.3.2:
version "1.3.2"
@@ -3722,21 +3714,21 @@ eslint-plugin-html@5.0.0:
dependencies:
htmlparser2 "^3.10.0"
-eslint-plugin-import@^2.14.0:
- version "2.14.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8"
- integrity sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==
+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==
dependencies:
contains-path "^0.1.0"
- debug "^2.6.8"
+ debug "^2.6.9"
doctrine "1.5.0"
- eslint-import-resolver-node "^0.3.1"
- eslint-module-utils "^2.2.0"
- has "^1.0.1"
- lodash "^4.17.4"
- minimatch "^3.0.3"
+ eslint-import-resolver-node "^0.3.2"
+ eslint-module-utils "^2.3.0"
+ has "^1.0.3"
+ lodash "^4.17.11"
+ minimatch "^3.0.4"
read-pkg-up "^2.0.0"
- resolve "^1.6.0"
+ resolve "^1.9.0"
eslint-plugin-jasmine@^2.10.1:
version "2.10.1"
@@ -3748,10 +3740,10 @@ eslint-plugin-jest@^22.3.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2"
integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA==
-eslint-plugin-promise@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2"
- integrity sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==
+eslint-plugin-promise@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
+ integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==
eslint-plugin-vue@^5.0.0:
version "5.0.0"
@@ -4472,7 +4464,7 @@ fstream@^1.0.0, fstream@^1.0.2:
mkdirp ">=0.5 0"
rimraf "2"
-function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
+function-bind@^1.1.0, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
@@ -4910,12 +4902,12 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
- integrity sha1-hGFzP1OLCDfJNh45qauelwTcLyg=
+has@^1.0.1, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
- function-bind "^1.0.2"
+ function-bind "^1.1.1"
hash-base@^2.0.0:
version "2.0.2"
@@ -8088,12 +8080,12 @@ pixelmatch@^4.0.2:
dependencies:
pngjs "^3.0.0"
-pkg-dir@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
- integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q=
+pkg-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+ integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
dependencies:
- find-up "^1.0.0"
+ find-up "^2.1.0"
pkg-dir@^3.0.0:
version "3.0.0"
@@ -9102,7 +9094,7 @@ resolve@1.1.7, resolve@1.1.x:
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.6.0:
+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.10.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
@@ -9991,10 +9983,10 @@ stylelint-config-recommended@^2.1.0:
resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-2.1.0.tgz#f526d5c771c6811186d9eaedbed02195fee30858"
integrity sha512-ajMbivOD7JxdsnlS5945KYhvt7L/HwN6YeYF2BH6kE4UCLJR0YvXMf+2j7nQpJyYLZx9uZzU5G1ZOSBiWAc6yA==
-stylelint-error-string-formatter@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/stylelint-error-string-formatter/-/stylelint-error-string-formatter-1.0.1.tgz#366387825d6fb59569e8c5c3f5682398733756f9"
- integrity sha512-8zy0UsdnQZKVDwjWMQX36b30TaNMGcM2FzBcK9cshpXerpJ3AvF2/zw7FJ3Efm6DFviTBVsxR14F3FnDFhCxJw==
+stylelint-error-string-formatter@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stylelint-error-string-formatter/-/stylelint-error-string-formatter-1.0.2.tgz#3076c6703d3e0170daeb55fe85030d63c834745e"
+ integrity sha512-xN69xRB0eTgYcGKVHWIiH2L+Xx8fRPliTiLjaS4YlbfBqkdTuZh2wjtfvNCkCzBTNeINWa5GpSa9RFYXdtwV6w==
stylelint-scss@^3.5.4:
version "3.5.4"
@@ -11536,13 +11528,6 @@ yeast@0.1.2:
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
-zen-observable-ts@^0.8.10:
- version "0.8.10"
- resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz#18e2ce1c89fe026e9621fd83cc05168228fce829"
- integrity sha512-5vqMtRggU/2GhePC9OU4sYEWOdvmayp2k3gjPf4F0mXwB3CSbbNznfDUvDJx9O2ZTa1EIXdJhPchQveFKwNXPQ==
- dependencies:
- zen-observable "^0.8.0"
-
zen-observable-ts@^0.8.18:
version "0.8.18"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8"