summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml36
-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--Dangerfile32
-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/issue_card_inner.vue6
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js6
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js4
-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.js9
-rw-r--r--app/assets/javascripts/commit/image_file.js44
-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.vue4
-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/environments/components/confirm_rollback_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue2
-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/lib/files.js6
-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/lib/utils/webpack.js6
-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.js11
-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.js33
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue44
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue24
-rw-r--r--app/assets/javascripts/notes/mixins/draft.js8
-rw-r--r--app/assets/javascripts/notes/mixins/get_discussion.js7
-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/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js51
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js8
-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.vue118
-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/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue20
-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.scss38
-rw-r--r--app/assets/stylesheets/components/project_list_item.scss24
-rw-r--r--app/assets/stylesheets/framework/buttons.scss1
-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/markdown_area.scss91
-rw-r--r--app/assets/stylesheets/framework/variables.scss97
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss8
-rw-r--r--app/assets/stylesheets/pages/diff.scss33
-rw-r--r--app/assets/stylesheets/pages/environments.scss338
-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/pages/prometheus.scss270
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/assets/stylesheets/utilities.scss17
-rw-r--r--app/assets/stylesheets/vendors/atwho.scss92
-rw-r--r--app/controllers/abuse_reports_controller.rb6
-rw-r--r--app/controllers/admin/groups_controller.rb3
-rw-r--r--app/controllers/application_controller.rb4
-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/confirmations_controller.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/help_controller.rb33
-rw-r--r--app/controllers/invites_controller.rb8
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb12
-rw-r--r--app/controllers/passwords_controller.rb4
-rw-r--r--app/controllers/profiles/chat_names_controller.rb10
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb6
-rw-r--r--app/controllers/profiles/preferences_controller.rb6
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb6
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb1
-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/refs_controller.rb2
-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/tags/releases_controller.rb10
-rw-r--r--app/controllers/projects/tree_controller.rb2
-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/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb4
-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.rb30
-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.rb20
-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/ci/pipeline.rb4
-rw-r--r--app/models/concerns/artifact_migratable.rb14
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/noteable.rb8
-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/gpg_signature.rb9
-rw-r--r--app/models/group.rb11
-rw-r--r--app/models/individual_note_discussion.rb2
-rw-r--r--app/models/instance_configuration.rb2
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/models/project_wiki.rb16
-rw-r--r--app/models/release.rb1
-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/ci/pipeline_policy.rb12
-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/after_branch_delete_service.rb20
-rw-r--r--app/services/ci/create_pipeline_service.rb2
-rw-r--r--app/services/clusters/applications/base_helm_service.rb12
-rw-r--r--app/services/concerns/suggestible.rb7
-rw-r--r--app/services/delete_branch_service.rb10
-rw-r--r--app/services/error_tracking/list_projects_service.rb4
-rw-r--r--app/services/git/base_hooks_service.rb94
-rw-r--r--app/services/git/branch_hooks_service.rb144
-rw-r--r--app/services/git/branch_push_service.rb215
-rw-r--r--app/services/git/tag_hooks_service.rb36
-rw-r--r--app/services/git/tag_push_service.rb62
-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/merge_requests/push_options_handler_service.rb162
-rw-r--r--app/services/merge_requests/refresh_service.rb10
-rw-r--r--app/services/note_summary.rb4
-rw-r--r--app/services/preview_markdown_service.rb28
-rw-r--r--app/services/projects/update_statistics_service.rb19
-rw-r--r--app/services/prometheus/proxy_service.rb116
-rw-r--r--app/services/quick_actions/interpret_service.rb19
-rw-r--r--app/services/releases/concerns.rb2
-rw-r--r--app/services/releases/create_service.rb20
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-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/admin/users/_user_detail.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml7
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml8
-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/help/index.html.haml5
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml47
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml16
-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/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml2
-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/all_queues.yml1
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/project_cache_worker.rb20
-rw-r--r--app/workers/reactive_caching_worker.rb7
-rw-r--r--app/workers/update_project_statistics_worker.rb18
-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/30157-api-expose-single-environment.yml5
-rw-r--r--changelogs/unreleased/38564-cant-leave-subgroup.yml5
-rw-r--r--changelogs/unreleased/43263-git-push-option-to-create-mr.yml5
-rw-r--r--changelogs/unreleased/47234-composable-auto-devops.yml5
-rw-r--r--changelogs/unreleased/47771-highlighting-in-diff.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/52560-fix-duplicate-tag-system-hooks.yml5
-rw-r--r--changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml6
-rw-r--r--changelogs/unreleased/53279-fix-updated_at-api.yml5
-rw-r--r--changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.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/55268-exclude-system-notes-from-commits-in-mr.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/58981-migrate-clusters-tests-to-jest.yml5
-rw-r--r--changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml5
-rw-r--r--changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml5
-rw-r--r--changelogs/unreleased/59708-vendor-css.yml5
-rw-r--r--changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml5
-rw-r--r--changelogs/unreleased/60068-avoid-null-domain-help-text.yml5
-rw-r--r--changelogs/unreleased/60116-fix-button-wrapping.yml5
-rw-r--r--changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.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/always-link-instance-configuration.yml5
-rw-r--r--changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml5
-rw-r--r--changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml5
-rw-r--r--changelogs/unreleased/create-label-and-list-checkbox.yml5
-rw-r--r--changelogs/unreleased/delay-update-statictics.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/ekigbo-extend-timezone-dropdown.yml5
-rw-r--r--changelogs/unreleased/expose-pipeline-variables-via-api.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/feature-webide_escaping.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/fix-pull-request-importer.yml5
-rw-r--r--changelogs/unreleased/fixed-duplicated-large-text-on-diffs.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/instance-configuration-artifact-size.yml5
-rw-r--r--changelogs/unreleased/issue-58418-release-notes.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/prevent-running-mr-pipelines-when-target-updated.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-add-gitaly-ref-name-caching-tree-controller.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-git-gc-after-initial-fetch.yml5
-rw-r--r--changelogs/unreleased/sh-improve-find-commit-caching.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/sidekiq_queues.yml3
-rw-r--r--config/webpack.config.js2
-rw-r--r--danger/gitlab_ui_wg/Dangerfile55
-rw-r--r--danger/roulette/Dangerfile27
-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/20180115094742_add_default_project_creation_setting.rb19
-rw-r--r--db/migrate/20180115113902_add_project_creation_level_to_groups.rb17
-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/README.md2
-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/auth/okta.md7
-rw-r--r--doc/administration/container_registry.md12
-rw-r--r--doc/administration/git_protocol.md2
-rw-r--r--doc/administration/job_traces.md8
-rw-r--r--doc/api/commits.md29
-rw-r--r--doc/api/environments.md105
-rw-r--r--doc/api/pipelines.md30
-rw-r--r--doc/api/project_clusters.md4
-rw-r--r--doc/api/runners.md37
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/chatops/README.md2
-rw-r--r--doc/ci/docker/using_docker_build.md35
-rw-r--r--doc/ci/docker/using_docker_images.md3
-rw-r--r--doc/ci/environments.md4
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md2
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md2
-rw-r--r--doc/ci/examples/test-scala-application.md2
-rw-r--r--doc/ci/introduction/index.md2
-rw-r--r--doc/ci/large_repositories/index.md235
-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/pipelines.md4
-rw-r--r--doc/ci/review_apps/index.md8
-rw-r--r--doc/ci/ssh_keys/README.md4
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/variables/README.md23
-rw-r--r--doc/ci/variables/predefined_variables.md5
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md30
-rw-r--r--doc/ci/yaml/README.md161
-rw-r--r--doc/development/api_graphql_styleguide.md140
-rw-r--r--doc/development/documentation/index.md48
-rw-r--r--doc/development/documentation/styleguide.md55
-rw-r--r--doc/development/ee_features.md11
-rw-r--r--doc/development/fe_guide/style_guide_scss.md10
-rw-r--r--doc/development/fe_guide/vue.md6
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/gitaly.md2
-rw-r--r--doc/development/testing_guide/best_practices.md6
-rw-r--r--doc/development/testing_guide/frontend_testing.md4
-rw-r--r--doc/development/testing_guide/review_apps.md4
-rw-r--r--doc/gitlab-basics/create-issue.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/raketasks/backup_restore.md2
-rw-r--r--doc/topics/autodevops/index.md23
-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.md13
-rw-r--r--doc/user/index.md4
-rw-r--r--doc/user/markdown.md38
-rw-r--r--doc/user/profile/account/two_factor_authentication.md14
-rw-r--r--doc/user/project/clusters/index.md35
-rw-r--r--doc/user/project/clusters/serverless/img/function-details-loaded.pngbin0 -> 93515 bytes
-rw-r--r--doc/user/project/clusters/serverless/index.md97
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/issues/create_new_issue.md2
-rw-r--r--doc/user/project/issues/index.md227
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md (renamed from doc/user/project/issues/issues_functionalities.md)6
-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/index.md60
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md8
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--doc/user/project/pages/lets_encrypt_for_gitlab_pages.md2
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--doc/user/project/quick_actions.md2
-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.rb23
-rw-r--r--lib/api/environments.rb15
-rw-r--r--lib/api/groups.rb1
-rw-r--r--lib/api/helpers/internal_helpers.rb22
-rw-r--r--lib/api/internal.rb27
-rw-r--r--lib/api/issues.rb14
-rw-r--r--lib/api/merge_requests.rb1
-rw-r--r--lib/api/pipelines.rb13
-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/background_migration.rb28
-rw-r--r--lib/gitlab/background_migration/migrate_stage_index.rb4
-rw-r--r--lib/gitlab/ci/build/image.rb10
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/port.rb32
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb2
-rw-r--r--lib/gitlab/ci/config/entry/image.rb22
-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/config/entry/port.rb46
-rw-r--r--lib/gitlab/ci/config/entry/ports.rb46
-rw-r--r--lib/gitlab/ci/config/entry/service.rb4
-rw-r--r--lib/gitlab/ci/config/entry/services.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.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.yml887
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml38
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml18
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml27
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml575
-rw-r--r--lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml23
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml6
-rw-r--r--lib/gitlab/ci/templates/Serverless.gitlab-ci.yml13
-rw-r--r--lib/gitlab/config/entry/configurable.rb10
-rw-r--r--lib/gitlab/config/entry/factory.rb2
-rw-r--r--lib/gitlab/config/entry/node.rb20
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb5
-rw-r--r--lib/gitlab/config/entry/validators.rb126
-rw-r--r--lib/gitlab/danger/helper.rb5
-rw-r--r--lib/gitlab/data_builder/pipeline.rb2
-rw-r--r--lib/gitlab/data_builder/push.rb9
-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/git_post_receive.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb9
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb5
-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/import/merge_request_helpers.rb2
-rw-r--r--lib/gitlab/legacy_github_import/release_formatter.rb1
-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/push_options.rb70
-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/sentry/client.rb30
-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.pot443
-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.json19
-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/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb4
-rw-r--r--qa/qa/tools/generate_perf_testdata.rb67
-rw-r--r--qa/spec/spec_helper.rb2
-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.rb4
-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.rb47
-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/refs_controller_spec.rb4
-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/tags/releases_controller_spec.rb61
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb2
-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.rb20
-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/issuables/shortcuts_issuable_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.rb8
-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.rb183
-rw-r--r--spec/features/labels_hierarchy_spec.rb4
-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_creates_image_diff_notes_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.rb195
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-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/settings/user_manages_merge_requests_settings_spec.rb4
-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_creates_wiki_page_spec.rb2
-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/graphql/introspection.graphql92
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/artifact.json16
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/artifact_file.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deployment.json32
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/environment.json23
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/job.json63
-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/public_api/v4/runner.json24
-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/.eslintrc.yml4
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js (renamed from spec/javascripts/clusters/clusters_bundle_spec.js)100
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js (renamed from spec/javascripts/clusters/components/application_row_spec.js)12
-rw-r--r--spec/frontend/clusters/components/applications_spec.js (renamed from spec/javascripts/clusters/components/applications_spec.js)4
-rw-r--r--spec/frontend/clusters/services/mock_data.js (renamed from spec/javascripts/clusters/services/mock_data.js)0
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js (renamed from spec/javascripts/clusters/stores/clusters_store_spec.js)0
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js2
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_error_spec.js2
-rw-r--r--spec/frontend/helpers/monitor_helper_spec.js45
-rw-r--r--spec/frontend/ide/lib/common/disposable_spec.js2
-rw-r--r--spec/frontend/ide/lib/diff/diff_spec.js93
-rw-r--r--spec/frontend/ide/lib/editor_options_spec.js2
-rw-r--r--spec/frontend/ide/lib/files_spec.js17
-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/pages/admin/abuse_reports/abuse_reports_spec.js4
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js14
-rw-r--r--spec/frontend/serverless/components/area_spec.js122
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js (renamed from spec/javascripts/serverless/components/environment_row_spec.js)41
-rw-r--r--spec/frontend/serverless/components/function_details_spec.js117
-rw-r--r--spec/frontend/serverless/components/function_row_spec.js (renamed from spec/javascripts/serverless/components/function_row_spec.js)14
-rw-r--r--spec/frontend/serverless/components/functions_spec.js110
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js38
-rw-r--r--spec/frontend/serverless/components/pod_box_spec.js23
-rw-r--r--spec/frontend/serverless/components/url_spec.js (renamed from spec/javascripts/serverless/components/url_spec.js)21
-rw-r--r--spec/frontend/serverless/mock_data.js (renamed from spec/javascripts/serverless/mock_data.js)57
-rw-r--r--spec/frontend/serverless/store/actions_spec.js90
-rw-r--r--spec/frontend/serverless/store/getters_spec.js43
-rw-r--r--spec/frontend/serverless/store/mutations_spec.js86
-rw-r--r--spec/frontend/serverless/utils.js20
-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/blob_helper_spec.rb13
-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/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/diffs/components/diff_file_spec.js8
-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/monitoring/utils_spec.js29
-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/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js167
-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/functions_spec.js68
-rw-r--r--spec/javascripts/serverless/stores/serverless_store_spec.js36
-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/entities/job_request/image_spec.rb31
-rw-r--r--spec/lib/api/entities/job_request/port_spec.rb22
-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/migrate_stage_index_spec.rb2
-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/background_migration_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/build/image_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/build/port_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/entry/port_spec.rb173
-rw-r--r--spec/lib/gitlab/ci/config/entry/ports_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/entry/services_spec.rb87
-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.rb63
-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.rb8
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb12
-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/github_import/importer/repository_importer_spec.rb5
-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/import/merge_request_helpers_spec.rb73
-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/legacy_github_import/release_formatter_spec.rb1
-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/push_options_spec.rb103
-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/lib/sentry/client_spec.rb48
-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/instance_configuration_spec.rb7
-rw-r--r--spec/models/merge_request_spec.rb8
-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/release_spec.rb16
-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/ci/pipeline_policy_spec.rb46
-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/environments_spec.rb22
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb26
-rw-r--r--spec/requests/api/internal_spec.rb151
-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.rb81
-rw-r--r--spec/requests/api/project_clusters_spec.rb62
-rw-r--r--spec/requests/api/runner_spec.rb58
-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/after_branch_delete_service_spec.rb15
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb53
-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/error_tracking/list_projects_service_spec.rb26
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb339
-rw-r--r--spec/services/git/branch_push_service_spec.rb258
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb144
-rw-r--r--spec/services/git/tag_push_service_spec.rb165
-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/push_options_handler_service_spec.rb404
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb55
-rw-r--r--spec/services/note_summary_spec.rb10
-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/projects/update_statistics_service_spec.rb40
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb195
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb41
-rw-r--r--spec/services/releases/create_service_spec.rb10
-rw-r--r--spec/services/suggestions/apply_service_spec.rb64
-rw-r--r--spec/services/system_note_service_spec.rb83
-rw-r--r--spec/services/task_list_toggle_service_spec.rb21
-rw-r--r--spec/spec_helper.rb4
-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/filtered_search_helpers.rb6
-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.rb5
-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/create_merge_request_quick_action_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb34
-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/quick_actions/merge_request/wip_quick_action_shared_examples.rb43
-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/support/shared_examples/time_tracking_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/wiki_file_attachments_examples.rb2
-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--spec/workers/post_receive_spec.rb25
-rw-r--r--spec/workers/project_cache_worker_spec.rb47
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb17
-rw-r--r--yarn.lock125
1111 files changed, 19969 insertions, 8037 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f89a52e7a3e..7d01afc9a16 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,7 +4,12 @@ include:
- local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
.dedicated-runner: &dedicated-runner
- retry: 1
+ retry:
+ max: 1 # This is confusing but this means "2 runs at max".
+ when:
+ - unknown_failure
+ - api_failure
+ - runner_system_failure
tags:
- gitlab-org
@@ -76,6 +81,11 @@ stages:
- postgres:9.6
- redis:alpine
+.use-pg-10: &use-pg-10
+ services:
+ - postgres:10.0
+ - redis:alpine
+
.use-mysql: &use-mysql
services:
- mysql:5.7
@@ -97,6 +107,15 @@ stages:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
+.only-schedules-master: &only-schedules-master
+ only:
+ - schedules@gitlab-org/gitlab-ce
+ - schedules@gitlab-org/gitlab-ee
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
+
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
@@ -172,6 +191,11 @@ stages:
<<: *rspec-metadata
<<: *use-pg
+.rspec-metadata-pg-10: &rspec-metadata-pg-10
+ <<: *rspec-metadata
+ <<: *use-pg-10
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
+
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
@@ -457,6 +481,8 @@ setup-test-env:
- schedules@gitlab-org/gitlab-ce
- schedules@gitlab-org/gitlab-ee
kubernetes: active
+ variables:
+ - $REVIEW_APP_CLEANUP
except:
refs:
- tags
@@ -525,8 +551,14 @@ rspec-pg:
<<: *rspec-metadata-pg
parallel: 50
+rspec-pg-10:
+ <<: *rspec-metadata-pg-10
+ <<: *only-schedules-master
+ parallel: 50
+
rspec-mysql:
<<: *rspec-metadata-mysql
+ <<: *only-schedules-master
parallel: 50
.rspec-quarantine: &rspec-quarantine
@@ -852,8 +884,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:
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 32f4b4d23c3..3e8cb456003 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,15 +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')
+
+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/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 90ab3a76342..206573dd444 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -92,6 +93,9 @@ export default {
const { referencePath, groupId } = this.issue;
return !groupId ? referencePath.split('#')[0] : null;
},
+ orderedLabels() {
+ return _.sortBy(this.issue.labels, 'title');
+ },
},
methods: {
isIndexLessThanlimit(index) {
@@ -176,7 +180,7 @@ export default {
</div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<button
- v-for="label in issue.labels"
+ v-for="label in orderedLabels"
v-if="showLabel(label)"
:key="label.id"
v-gl-tooltip
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/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index c14d69c5d18..6b54e8baefb 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,6 +1,8 @@
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
+import { isEE } from '~/lib/utils/common_utils';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@@ -8,6 +10,8 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
page: 'boards',
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
+ isGroup: isEE(),
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
this.store = store;
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..df855261b3c 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -262,7 +262,7 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
- this.service.installApplication(appId, data.params).catch(() => {
+ return this.service.installApplication(appId, data.params).catch(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
this.store.updateAppProperty(
appId,
@@ -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/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index d4ecfa4aa93..bc666aef54b 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -71,29 +71,39 @@ export default class ImageFile {
// eslint-disable-next-line class-methods-use-this
initDraggable($el, padding, callback) {
var dragging = false;
- var $body = $('body');
- var $offsetEl = $el.parent();
-
- $el.off('mousedown').on('mousedown', function() {
+ const $body = $('body');
+ const $offsetEl = $el.parent();
+ const dragStart = function() {
dragging = true;
$body.css('user-select', 'none');
- });
+ };
+ const dragStop = function() {
+ dragging = false;
+ $body.css('user-select', '');
+ };
+ const dragMove = function(e) {
+ const moveX = e.pageX || e.touches[0].pageX;
+ const left = moveX - ($offsetEl.offset().left + padding);
+ if (!dragging) return;
+
+ callback(e, left);
+ };
+
+ $el
+ .off('mousedown')
+ .off('touchstart')
+ .on('mousedown', dragStart)
+ .on('touchstart', dragStart);
$body
.off('mouseup')
.off('mousemove')
- .on('mouseup', function() {
- dragging = false;
- $body.css('user-select', '');
- })
- .on('mousemove', function(e) {
- var left;
- if (!dragging) return;
-
- left = e.pageX - ($offsetEl.offset().left + padding);
-
- callback(e, left);
- });
+ .off('touchend')
+ .off('touchmove')
+ .on('mouseup', dragStop)
+ .on('touchend', dragStop)
+ .on('mousemove', dragMove)
+ .on('touchmove', dragMove);
}
prepareFrames(view) {
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.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 58a9605c181..f5876a73eff 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -188,10 +188,6 @@ export default {
/>
</div>
</template>
- <div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff">
- {{ __('This source diff could not be displayed because it is too large.') }}
- <span v-html="viewBlobLink"></span>
- </div>
</div>
</template>
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/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index a8ee3f4ac10..70b5c6b0094 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -50,7 +50,7 @@ export default {
},
modalText() {
- const linkStart = `<a class="commit-sha" href="${_.escape(this.commitUrl)}">`;
+ const linkStart = `<a class="commit-sha mr-0" href="${_.escape(this.commitUrl)}">`;
const commitId = _.escape(this.commitShortSha);
const linkEnd = '</a>';
const name = _.escape(this.name);
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index c541ea3445b..f0e80cba753 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -504,22 +504,28 @@ export default {
class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell"
>
- <span v-if="shouldRenderDeploymentID"> {{ deploymentInternalId }} </span>
+ <span v-if="shouldRenderDeploymentID" class="text-break-word">
+ {{ deploymentInternalId }}
+ </span>
- <span v-if="!model.isFolder && deploymentHasUser">
+ <span v-if="!model.isFolder && deploymentHasUser" class="text-break-word">
by
<user-avatar-link
:link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username"
- class="js-deploy-user-container"
+ class="js-deploy-user-container float-none"
/>
</span>
</div>
<div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell">
- <a v-if="shouldRenderBuildName" :href="buildPath" class="build-link flex-truncate-parent">
+ <a
+ v-if="shouldRenderBuildName"
+ :href="buildPath"
+ class="build-link cgray flex-truncate-parent"
+ >
<span class="flex-truncate-child">{{ buildName }}</span>
</a>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 266cdc42518..bafbc00597e 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -72,10 +72,9 @@ export default {
<gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
- variant="secondary"
:disabled="isLoading"
:title="title"
- class="d-none d-md-block"
+ class="d-none d-md-block text-secondary"
@click="onClick"
>
<icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 6d74d136a94..13195d32cc4 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -39,7 +39,7 @@ export default {
:aria-label="title"
:href="terminalPath"
:class="{ disabled: disabled }"
- class="btn terminal-button d-none d-sm-none d-md-block"
+ class="btn terminal-button d-none d-sm-none d-md-block text-secondary"
>
<icon name="terminal" />
</a>
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/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index 5dfba8fe531..df100f753d7 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -1,6 +1,8 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import { decorateData, sortTree } from '../stores/utils';
+export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
+
export const splitParent = path => {
const idx = path.lastIndexOf('/');
@@ -45,7 +47,7 @@ export const decorateFiles = ({
id: path,
name,
path,
- url: `/${projectId}/tree/${branchId}/-/${path}/`,
+ url: `/${projectId}/tree/${branchId}/-/${escapeFileUrl(path)}/`,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
@@ -81,7 +83,7 @@ export const decorateFiles = ({
id: path,
name,
path,
- url: `/${projectId}/blob/${branchId}/-/${path}`,
+ url: `/${projectId}/blob/${branchId}/-/${escapeFileUrl(path)}`,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
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/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index a4dad6f1615..37b5409a51d 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -6,5 +6,11 @@ export function resetServiceWorkersPublicPath() {
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+
+ // monaco-editor-webpack-plugin currently (incorrectly) references the
+ // public path as a property of `window`. Once this is fixed upstream we
+ // can remove this line
+ // see: https://github.com/Microsoft/monaco-editor-webpack-plugin/pull/63
window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line
}
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..e97320fd682 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,12 @@ 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'),
+};
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..ef309c8a398
--- /dev/null
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -0,0 +1,33 @@
+import { timeWindows } 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 getTimeDifferenceSeconds = timeWindow => {
+ switch (timeWindow) {
+ case timeWindows.thirtyMinutes:
+ return 60 * 30;
+ case timeWindows.threeHours:
+ return 60 * 60 * 3;
+ case timeWindows.oneDay:
+ return 60 * 60 * 24 * 1;
+ case timeWindows.threeDays:
+ return 60 * 60 * 24 * 3;
+ case timeWindows.oneWeek:
+ return 60 * 60 * 24 * 7 * 1;
+ default:
+ return 60 * 60 * 8;
+ }
+};
+
+export const getTimeDiff = selectedTimeWindow => {
+ const end = Date.now() / 1000; // convert milliseconds to seconds
+ const start = end - getTimeDifferenceSeconds(selectedTimeWindow);
+
+ 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_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index ff303d0f55a..fbf75ed0e41 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions } from 'vuex';
import $ from 'jquery';
+import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
@@ -16,7 +17,7 @@ export default {
noteForm,
Suggestions,
},
- mixins: [autosave],
+ mixins: [autosave, getDiscussion],
props: {
note: {
type: Object,
@@ -76,8 +77,8 @@ export default {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
- handleFormUpdate(note, parentElement, callback) {
- this.$emit('handleFormUpdate', note, parentElement, callback);
+ handleFormUpdate(note, parentElement, callback, resolveDiscussion) {
+ this.$emit('handleFormUpdate', note, parentElement, callback, resolveDiscussion);
},
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelForm', shouldConfirm, isDirty);
@@ -111,6 +112,8 @@ export default {
:line="line"
:note="note"
:help-page-path="helpPagePath"
+ :discussion="discussion"
+ :resolve-discussion="note.resolve_discussion"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
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/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 5fa0ab3de98..47d74c2f892 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -4,6 +4,7 @@ import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -23,7 +24,7 @@ export default {
noteBody,
TimelineEntryItem,
},
- mixins: [noteable, resolvable],
+ mixins: [noteable, resolvable, draftMixin],
props: {
note: {
type: Object,
@@ -73,9 +74,6 @@ export default {
'is-editable': this.note.current_user.can_edit,
};
},
- canResolve() {
- return this.note.resolvable && !!this.getUserData.id;
- },
canReportAsAbuse() {
return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
@@ -96,7 +94,7 @@ export default {
return '';
}
- // We need to do this to ensure we have the currect sentence order
+ // We need to do this to ensure we have the correct sentence order
// when translating this as the sentence order may change from one
// language to the next. See:
// https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771
@@ -156,12 +154,16 @@ export default {
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
- formUpdateHandler(noteText, parentElement, callback) {
+ formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) {
this.$emit('handleUpdateNote', {
note: this.note,
noteText,
+ resolveDiscussion,
callback: () => this.updateSuccess(),
});
+
+ if (this.isDraft) return;
+
const data = {
endpoint: this.note.path,
note: {
@@ -234,6 +236,7 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
+ <slot slot="note-header-info" name="note-header-info"></slot>
<span v-if="commit" v-html="actionText"></span>
<span v-else class="d-none d-sm-inline">&middot;</span>
</note-header>
@@ -247,12 +250,15 @@ export default {
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
- :can-resolve="note.current_user.can_resolve"
+ :can-resolve="canResolve"
:report-abuse-path="note.report_abuse_path"
- :resolvable="note.resolvable"
- :is-resolved="note.resolved"
+ :resolvable="note.resolvable || note.isDraft"
+ :is-resolved="note.resolved || note.resolve_discussion"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
+ :is-draft="note.isDraft"
+ :resolve-discussion="note.isDraft && note.resolve_discussion"
+ :discussion-id="discussionId"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
diff --git a/app/assets/javascripts/notes/mixins/draft.js b/app/assets/javascripts/notes/mixins/draft.js
new file mode 100644
index 00000000000..1370f3978df
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/draft.js
@@ -0,0 +1,8 @@
+export default {
+ computed: {
+ isDraft: () => false,
+ canResolve() {
+ return this.note.current_user.can_resolve;
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/mixins/get_discussion.js b/app/assets/javascripts/notes/mixins/get_discussion.js
new file mode 100644
index 00000000000..b5d820fe083
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/get_discussion.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ discussion() {
+ return {};
+ },
+ },
+};
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/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index 95b57d5e048..c1f6edf2f27 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,17 +1,33 @@
-/* eslint-disable class-methods-use-this */
+const defaultTimezone = 'UTC';
+
+export const formatUtcOffset = offset => {
+ const parsed = parseInt(offset, 10);
+ if (Number.isNaN(parsed) || parsed === 0) {
+ return `0`;
+ }
+ const prefix = offset > 0 ? '+' : '-';
+ return `${prefix} ${Math.abs(offset / 3600)}`;
+};
-import $ from 'jquery';
+export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`;
-const defaultTimezone = 'UTC';
+const defaults = {
+ $inputEl: null,
+ $dropdownEl: null,
+ onSelectTimezone: null,
+};
export default class TimezoneDropdown {
- constructor() {
- this.$dropdown = $('.js-timezone-dropdown');
+ constructor({ $dropdownEl, $inputEl, onSelectTimezone } = defaults) {
+ this.$dropdown = $dropdownEl;
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
- this.$input = $('#schedule_cron_timezone');
+ this.$input = $inputEl;
this.timezoneData = this.$dropdown.data('data');
+
this.initDefaultTimezone();
this.initDropdown();
+
+ this.onSelectTimezone = onSelectTimezone;
}
initDropdown() {
@@ -24,28 +40,12 @@ export default class TimezoneDropdown {
fields: ['name'],
},
clicked: cfg => this.updateInputValue(cfg),
- text: item => this.formatTimezone(item),
+ text: item => formatTimezone(item),
});
this.setDropdownToggle();
}
- formatUtcOffset(offset) {
- let prefix = '';
-
- if (offset > 0) {
- prefix = '+';
- } else if (offset < 0) {
- prefix = '-';
- }
-
- return `${prefix} ${Math.abs(offset / 3600)}`;
- }
-
- formatTimezone(item) {
- return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`;
- }
-
initDefaultTimezone() {
const initialValue = this.$input.val();
@@ -56,13 +56,14 @@ export default class TimezoneDropdown {
setDropdownToggle() {
const initialValue = this.$input.val();
-
this.$dropdownToggle.text(initialValue);
}
updateInputValue({ selectedObj, e }) {
e.preventDefault();
this.$input.val(selectedObj.identifier);
- gl.pipelineScheduleFieldErrors.updateFormValidityState();
+ if (this.onSelectTimezone) {
+ this.onSelectTimezone({ selectedObj, e });
+ }
}
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index 4d494efef6c..dc6df27f1c7 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -41,7 +41,13 @@ export default () => {
const formElement = document.getElementById('new-pipeline-schedule-form');
- gl.timezoneDropdown = new TimezoneDropdown();
+ gl.timezoneDropdown = new TimezoneDropdown({
+ $dropdownEl: $('.js-timezone-dropdown'),
+ $inputEl: $('#schedule_cron_timezone'),
+ onSelectTimezone: () => {
+ gl.pipelineScheduleFieldErrors.updateFormValidityState();
+ },
+ });
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
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..6d908524da9
--- /dev/null
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -0,0 +1,118 @@
+<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="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/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 3f282138bdf..944b9c0c083 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -162,7 +162,7 @@ export default {
</template>
<icon name="commit" class="commit-icon js-commit-icon" />
- <gl-link :href="commitUrl" class="commit-sha"> {{ shortSha }} </gl-link>
+ <gl-link :href="commitUrl" class="commit-sha mr-0"> {{ shortSha }} </gl-link>
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
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..2b5b2269ec8 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
@@ -40,12 +40,15 @@ export default {
},
beforeDestroy() {
document.body.removeEventListener('mouseup', this.stopDrag);
- this.$refs.dragger.removeEventListener('mousedown', this.startDrag);
+ document.body.removeEventListener('touchend', this.stopDrag);
+ document.body.removeEventListener('mousemove', this.dragMove);
+ document.body.removeEventListener('touchmove', this.dragMove);
},
methods: {
dragMove(e) {
if (!this.dragging) return;
- const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left;
+ const moveX = e.pageX || e.touches[0].pageX;
+ const left = moveX - this.$refs.dragTrack.getBoundingClientRect().left;
const dragTrackWidth =
this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
@@ -60,11 +63,13 @@ export default {
this.dragging = true;
document.body.style.userSelect = 'none';
document.body.addEventListener('mousemove', this.dragMove);
+ document.body.addEventListener('touchmove', this.dragMove);
},
stopDrag() {
this.dragging = false;
document.body.style.userSelect = '';
document.body.removeEventListener('mousemove', this.dragMove);
+ document.body.removeEventListener('touchmove', this.dragMove);
},
prepareOnionSkin() {
if (this.onionOldImgInfo && this.onionNewImgInfo) {
@@ -82,6 +87,7 @@ export default {
this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100;
document.body.addEventListener('mouseup', this.stopDrag);
+ document.body.addEventListener('touchend', this.stopDrag);
}
},
onionNewImgLoaded(imgInfo) {
@@ -102,7 +108,7 @@ export default {
:style="{
width: onionMaxPixelWidth,
height: onionMaxPixelHeight,
- 'user-select': dragging === true ? 'none' : '',
+ 'user-select': dragging ? 'none' : null,
}"
class="onion-skin-frame"
>
@@ -140,7 +146,14 @@ export default {
</div>
<div class="controls">
<div class="transparent"></div>
- <div ref="dragTrack" class="drag-track" @mousedown="startDrag" @mouseup="stopDrag">
+ <div
+ ref="dragTrack"
+ class="drag-track"
+ @mousedown="startDrag"
+ @mouseup="stopDrag"
+ @touchstart="startDrag"
+ @touchend="stopDrag"
+ >
<div ref="dragger" :style="{ left: onionDraggerPixelPos }" class="dragger"></div>
</div>
<div class="opaque"></div>
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..ad3b3b81ac5 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
@@ -46,6 +46,8 @@ export default {
window.removeEventListener('resize', this.resizeThrottled, false);
document.body.removeEventListener('mouseup', this.stopDrag);
document.body.removeEventListener('mousemove', this.dragMove);
+ document.body.removeEventListener('touchend', this.stopDrag);
+ document.body.removeEventListener('touchmove', this.dragMove);
},
mounted() {
window.addEventListener('resize', this.resize, false);
@@ -54,7 +56,8 @@ export default {
dragMove(e) {
if (!this.dragging) return;
- let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left;
+ const moveX = e.pageX || e.touches[0].pageX;
+ let leftValue = moveX - this.$refs.swipeFrame.getBoundingClientRect().left;
const spaceLeft = 20;
const { clientWidth } = this.$refs.swipeFrame;
if (leftValue <= 0) {
@@ -68,13 +71,13 @@ export default {
},
startDrag() {
this.dragging = true;
- document.body.style.userSelect = 'none';
document.body.addEventListener('mousemove', this.dragMove);
+ document.body.addEventListener('touchmove', this.dragMove);
},
stopDrag() {
this.dragging = false;
- document.body.style.userSelect = '';
document.body.removeEventListener('mousemove', this.dragMove);
+ document.body.removeEventListener('touchmove', this.dragMove);
},
prepareSwipe() {
if (this.swipeOldImgInfo && this.swipeNewImgInfo) {
@@ -85,6 +88,7 @@ export default {
Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2;
document.body.addEventListener('mouseup', this.stopDrag);
+ document.body.addEventListener('touchend', this.stopDrag);
}
},
swipeNewImgLoaded(imgInfo) {
@@ -104,7 +108,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"
@@ -139,6 +149,8 @@ export default {
class="swipe-bar"
@mousedown="startDrag"
@mouseup="stopDrag"
+ @touchstart="startDrag"
+ @touchend="stopDrag"
>
<span class="top-handle"></span> <span class="bottom-handle"></span>
</span>
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..a2f518cd24e 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -6,40 +6,32 @@
*= 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.
- */
+// Vendors specific styles
+@import "vendors/**/*";
+
+// 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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 695ce014659..ab8f397f3a0 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -139,6 +139,7 @@
@include btn-white;
color: $gl-text-color;
+ white-space: nowrap;
&:focus:active {
outline: 0;
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/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 7c10de828cd..bfd96a4bc05 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -163,88 +163,6 @@
}
}
-.atwho-view {
- overflow-y: auto;
- overflow-x: hidden;
-
- .name,
- small.aliases,
- small.params {
- float: left;
- }
-
- small.aliases,
- small.params {
- padding: 2px 5px;
- }
-
- small.description {
- float: right;
- padding: 3px 5px;
- }
-
- .avatar-inline {
- margin-bottom: 0;
- }
-
- .has-warning {
- .name,
- .description {
- color: $orange-700;
- }
- }
-
- .cur {
- .avatar {
- @include disable-all-animation;
- border: 1px solid $white-light;
- }
- }
-
- ul > li {
- @include clearfix;
- white-space: nowrap;
- }
-
- // TODO: fallback to global style
- .atwho-view-ul {
- padding: 8px 1px;
-
- li {
- padding: 8px 16px;
- border: 0;
-
- &.cur {
- background-color: $gray-darker;
- color: $gl-text-color;
-
- small {
- color: inherit;
- }
-
- &.has-warning {
- color: $orange-700;
- background-color: $orange-100;
- }
- }
-
- div.avatar {
- display: inline-flex;
- justify-content: center;
- align-items: center;
-
- .center {
- line-height: 14px;
- }
- }
-
- strong {
- color: $gl-text-color;
- }
- }
- }
-}
-
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
@@ -269,15 +187,6 @@
}
@include media-breakpoint-down(xs) {
- .atwho-view-ul {
- width: 350px;
- }
-
- .atwho-view ul li {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
.referenced-users {
margin-right: 0;
}
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/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index a80158943c6..0c1067bfacc 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -179,6 +179,14 @@ $ide-commit-header-height: 48px;
display: none;
}
+ .monaco-editor .selected-text {
+ z-index: 1;
+ }
+
+ .monaco-editor .view-lines {
+ z-index: 2;
+ }
+
.is-readonly,
.editor.original {
.view-lines {
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..93dffb5ff09 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -12,34 +12,6 @@
.environments-container {
.ci-table {
- .deployment-column {
- > span {
- word-break: break-all;
- }
-
- .avatar {
- float: none;
- }
- }
-
- .btn-group {
- > .btn:not(.btn-danger) {
- color: $gl-text-color-secondary;
- }
-
- svg path {
- fill: $gl-text-color-secondary;
- }
-
- .dropdown {
- outline: none;
- }
- }
-
- .btn .text-center {
- display: inline;
- }
-
.commit-title {
margin: 0;
}
@@ -49,47 +21,16 @@
color: $gl-text-color-secondary;
}
- .dropdown-menu {
- .fa {
- margin-right: 6px;
- color: $gl-text-color-secondary;
- }
- }
-
.build-link,
.ref-name {
color: $gl-text-color;
}
- .stop-env-link,
- .external-url {
- color: $gl-text-color-secondary;
-
- .stop-env-icon {
- font-size: 14px;
- }
- }
-
- .deployment .build-column {
- .build-link {
- color: $gl-text-color;
- }
-
- .avatar {
- float: none;
- margin-right: 0;
- }
- }
-
.folder-icon {
margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
vertical-align: text-top;
-
- .fa:nth-child(1) {
- margin-right: 3px;
- }
}
.folder-name {
@@ -103,12 +44,6 @@
text-align: center;
}
- .branch-commit {
- .commit-sha {
- margin-right: 0;
- }
- }
-
.no-btn {
border: 0;
background: none;
@@ -168,11 +103,6 @@
opacity: 0.25;
}
-.prometheus-graph-overlay {
- fill: none;
- opacity: 0;
- pointer-events: all;
-}
.rect-text-metric {
fill: $white-light;
@@ -203,276 +133,10 @@
stroke: $gray-darkest;
}
-.prometheus-graphs {
- .environments {
- .dropdown-menu-toggle {
- svg {
- position: absolute;
- right: 5%;
- top: 25%;
- }
- }
-
- .dropdown-menu-toggle,
- .dropdown-menu {
- width: 240px;
- }
- }
-}
-
.environments-actions {
.external-url,
.monitoring-url,
- .terminal-button,
- .stop-env-link {
+ .terminal-button {
width: 38px;
}
}
-
-.prometheus-panel {
- margin-top: 20px;
-}
-
-.prometheus-graph-group {
- display: flex;
- flex-wrap: wrap;
- padding: $gl-padding / 2;
-}
-
-.prometheus-graph {
- padding: $gl-padding / 2;
-}
-
-.prometheus-graph-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: $gl-padding-8;
-
- h5 {
- font-size: $gl-font-size-large;
- margin: 0;
- }
-}
-
-.prometheus-graph-cursor {
- position: absolute;
- background: $gray-600;
- width: 1px;
-}
-
-.prometheus-graph-flag {
- display: block;
- min-width: 160px;
- border: 0;
- box-shadow: 0 1px 4px 0 $black-transparent;
-
- h5 {
- padding: 0;
- margin: 0;
- font-size: 14px;
- line-height: 1.2;
- }
-
- .deploy-meta-content {
- border-bottom: 1px solid $white-dark;
-
- svg {
- height: 15px;
- vertical-align: bottom;
- }
- }
-
- &.popover {
- padding: 0;
-
- &.left {
- left: auto;
- right: 0;
- margin-right: 10px;
-
- > .arrow {
- right: -14px;
- border-left-color: $border-color;
- }
-
- > .arrow::after {
- border-top: 6px solid transparent;
- border-bottom: 6px solid transparent;
- border-left: 4px solid $gray-50;
- }
-
- .arrow-shadow {
- right: -3px;
- box-shadow: 1px 0 9px 0 $black-transparent;
- }
- }
-
- &.right {
- left: 0;
- right: auto;
- margin-left: 10px;
-
- > .arrow {
- left: -7px;
- border-right-color: $border-color;
- }
-
- > .arrow::after {
- border-top: 6px solid transparent;
- border-bottom: 6px solid transparent;
- border-right: 4px solid $gray-50;
- }
-
- .arrow-shadow {
- left: -3px;
- box-shadow: 1px 0 8px 0 $black-transparent;
- }
- }
-
- > .arrow {
- top: 10px;
- margin: 0;
- }
-
- .arrow-shadow {
- content: '';
- position: absolute;
- width: 7px;
- height: 7px;
- background-color: transparent;
- transform: rotate(45deg);
- top: 13px;
- }
-
- > .popover-title,
- > .popover-content,
- > .popover-header,
- > .popover-body {
- padding: 8px;
- font-size: 12px;
- white-space: nowrap;
- position: relative;
- }
-
- > .popover-title {
- background-color: $gray-50;
- border-radius: $border-radius-default $border-radius-default 0 0;
- }
- }
-
- strong {
- font-weight: 600;
- }
-}
-
-.prometheus-table {
- border-collapse: collapse;
- padding: 0;
- margin: 0;
-
- td {
- vertical-align: middle;
-
- + td {
- padding-left: 8px;
- vertical-align: top;
- }
- }
-
- .legend-metric-title {
- font-size: 12px;
- vertical-align: middle;
- }
-}
-
-.prometheus-svg-container {
- position: relative;
- height: 0;
- width: 100%;
- padding: 0;
- padding-bottom: 100%;
-
- .text-metric-usage {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 12px;
- }
-
- > svg {
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-
- text {
- fill: $gl-text-color;
- stroke-width: 0;
- }
-
- .text-metric-bold {
- font-weight: $gl-font-weight-bold;
- }
-
- .label-axis-text {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 10px;
- }
-
- .legend-axis-text {
- fill: $black;
- }
-
- .tick {
- > line {
- stroke: $gray-darker;
- }
-
- > text {
- fill: $gray-600;
- font-size: 10px;
- }
- }
-
- .y-label-text,
- .x-label-text {
- fill: $gray-darkest;
- }
-
- .axis-tick {
- stroke: $gray-darker;
- }
-
- .deploy-info-text {
- dominant-baseline: text-before-edge;
- font-size: 12px;
- }
-
- .deploy-info-text-link {
- font-family: $monospace-font;
- fill: $blue-600;
-
- &:hover {
- fill: $blue-800;
- }
- }
-
- @include media-breakpoint-down(sm) {
- .label-axis-text,
- .text-metric-usage,
- .legend-axis-text {
- font-size: 8px;
- }
-
- .tick > text {
- font-size: 8px;
- }
- }
- }
-}
-
-.prometheus-table-row-highlight {
- background-color: $gray-100;
-}
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/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
new file mode 100644
index 00000000000..c03554b287f
--- /dev/null
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -0,0 +1,270 @@
+.prometheus-graphs {
+ .dropdowns {
+ .dropdown-menu-toggle {
+ svg {
+ position: absolute;
+ right: 5%;
+ top: 25%;
+ }
+ }
+
+ .dropdown-menu-toggle,
+ .dropdown-menu {
+ width: 240px;
+ }
+ }
+}
+
+.prometheus-panel {
+ margin-top: 20px;
+}
+
+.prometheus-graph-group {
+ display: flex;
+ flex-wrap: wrap;
+ padding: $gl-padding / 2;
+}
+
+.prometheus-graph {
+ padding: $gl-padding / 2;
+}
+
+.prometheus-graph-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: $gl-padding-8;
+
+ h5 {
+ font-size: $gl-font-size-large;
+ margin: 0;
+ }
+}
+
+.prometheus-graph-cursor {
+ position: absolute;
+ background: $gray-600;
+ width: 1px;
+}
+
+.prometheus-graph-flag {
+ display: block;
+ min-width: 160px;
+ border: 0;
+ box-shadow: 0 1px 4px 0 $black-transparent;
+
+ h5 {
+ padding: 0;
+ margin: 0;
+ font-size: 14px;
+ line-height: 1.2;
+ }
+
+ .deploy-meta-content {
+ border-bottom: 1px solid $white-dark;
+
+ svg {
+ height: 15px;
+ vertical-align: bottom;
+ }
+ }
+
+ &.popover {
+ padding: 0;
+
+ &.left {
+ left: auto;
+ right: 0;
+ margin-right: 10px;
+
+ > .arrow {
+ right: -14px;
+ border-left-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ border-left: 4px solid $gray-50;
+ }
+
+ .arrow-shadow {
+ right: -3px;
+ box-shadow: 1px 0 9px 0 $black-transparent;
+ }
+ }
+
+ &.right {
+ left: 0;
+ right: auto;
+ margin-left: 10px;
+
+ > .arrow {
+ left: -7px;
+ border-right-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ border-right: 4px solid $gray-50;
+ }
+
+ .arrow-shadow {
+ left: -3px;
+ box-shadow: 1px 0 8px 0 $black-transparent;
+ }
+ }
+
+ > .arrow {
+ top: 10px;
+ margin: 0;
+ }
+
+ .arrow-shadow {
+ content: '';
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ background-color: transparent;
+ transform: rotate(45deg);
+ top: 13px;
+ }
+
+ > .popover-title,
+ > .popover-content,
+ > .popover-header,
+ > .popover-body {
+ padding: 8px;
+ font-size: 12px;
+ white-space: nowrap;
+ position: relative;
+ }
+
+ > .popover-title {
+ background-color: $gray-50;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+ }
+
+ strong {
+ font-weight: 600;
+ }
+}
+
+.prometheus-table {
+ border-collapse: collapse;
+ padding: 0;
+ margin: 0;
+
+ td {
+ vertical-align: middle;
+
+ + td {
+ padding-left: 8px;
+ vertical-align: top;
+ }
+ }
+
+ .legend-metric-title {
+ font-size: 12px;
+ vertical-align: middle;
+ }
+}
+
+.prometheus-svg-container {
+ position: relative;
+ height: 0;
+ width: 100%;
+ padding: 0;
+ padding-bottom: 100%;
+
+ .text-metric-usage {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 12px;
+ }
+
+ > svg {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
+
+ text {
+ fill: $gl-text-color;
+ stroke-width: 0;
+ }
+
+ .text-metric-bold {
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .label-axis-text {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 10px;
+ }
+
+ .legend-axis-text {
+ fill: $black;
+ }
+
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
+
+ > text {
+ fill: $gray-600;
+ font-size: 10px;
+ }
+ }
+
+ .y-label-text,
+ .x-label-text {
+ fill: $gray-darkest;
+ }
+
+ .axis-tick {
+ stroke: $gray-darker;
+ }
+
+ .deploy-info-text {
+ dominant-baseline: text-before-edge;
+ font-size: 12px;
+ }
+
+ .deploy-info-text-link {
+ font-family: $monospace-font;
+ fill: $blue-600;
+
+ &:hover {
+ fill: $blue-800;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ .label-axis-text,
+ .text-metric-usage,
+ .legend-axis-text {
+ font-size: 8px;
+ }
+
+ .tick > text {
+ font-size: 8px;
+ }
+ }
+ }
+}
+
+.prometheus-table-row-highlight {
+ background-color: $gray-100;
+}
+
+.prometheus-graph-overlay {
+ fill: none;
+ opacity: 0;
+ pointer-events: all;
+}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index e4ed685bd1b..7b0538dca20 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -232,7 +232,7 @@
}
}
-.settings-flex-row {
+.content-list > .settings-flex-row {
display: flex;
align-items: center;
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/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss
new file mode 100644
index 00000000000..ccf3824ea56
--- /dev/null
+++ b/app/assets/stylesheets/vendors/atwho.scss
@@ -0,0 +1,92 @@
+.atwho-view {
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ .name,
+ small.aliases,
+ small.params {
+ float: left;
+ }
+
+ small.aliases,
+ small.params {
+ padding: 2px 5px;
+ }
+
+ small.description {
+ float: right;
+ padding: 3px 5px;
+ }
+
+ .avatar-inline {
+ margin-bottom: 0;
+ }
+
+ .has-warning {
+ .name,
+ .description {
+ color: $orange-700;
+ }
+ }
+
+ .cur {
+ .avatar {
+ @include disable-all-animation;
+ border: 1px solid $white-light;
+ }
+ }
+
+ ul > li {
+ @include clearfix;
+ white-space: nowrap;
+ }
+
+ // TODO: fallback to global style
+ .atwho-view-ul {
+ padding: 8px 1px;
+
+ li {
+ padding: 8px 16px;
+ border: 0;
+
+ &.cur {
+ background-color: $gray-darker;
+ color: $gl-text-color;
+
+ small {
+ color: inherit;
+ }
+
+ &.has-warning {
+ color: $orange-700;
+ background-color: $orange-100;
+ }
+ }
+
+ div.avatar {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
+ .center {
+ line-height: 14px;
+ }
+ }
+
+ strong {
+ color: $gl-text-color;
+ }
+ }
+ }
+}
+
+@include media-breakpoint-down(xs) {
+ .atwho-view-ul {
+ width: 350px;
+ }
+
+ .atwho-view ul li {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 68e14f0c2e5..7d8016f763d 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -16,7 +16,7 @@ class AbuseReportsController < ApplicationController
if @abuse_report.save
@abuse_report.notify
- message = "Thank you for your report. A GitLab administrator will look into it shortly."
+ message = _("Thank you for your report. A GitLab administrator will look into it shortly.")
redirect_to @abuse_report.user, notice: message
else
render :new
@@ -37,9 +37,9 @@ class AbuseReportsController < ApplicationController
@user = User.find_by(id: params[:user_id])
if @user.nil?
- redirect_to root_path, alert: "Cannot create the abuse report. The user has been deleted."
+ redirect_to root_path, alert: _("Cannot create the abuse report. The user has been deleted.")
elsif @user.blocked?
- redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked."
+ redirect_to @user, alert: _("Cannot create the abuse report. This user has been blocked.")
end
end
# rubocop: enable CodeReuse/ActiveRecord
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/application_controller.rb b/app/controllers/application_controller.rb
index b7eb6af6d67..d5f1e35a79b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -293,7 +293,7 @@ class ApplicationController < ActionController::Base
unless Gitlab::Auth::LDAP::Access.allowed?(current_user)
sign_out current_user
- flash[:alert] = "Access denied for your LDAP account."
+ flash[:alert] = _("Access denied for your LDAP account.")
redirect_to new_user_session_path
end
end
@@ -340,7 +340,7 @@ class ApplicationController < ActionController::Base
def require_email
if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
- return redirect_to profile_path, notice: 'Please complete your profile with email address'
+ return redirect_to profile_path, notice: _('Please complete your profile with email address')
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/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 2c4aab67448..2ae500a2fdf 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -22,7 +22,7 @@ class ConfirmationsController < Devise::ConfirmationsController
after_sign_in(resource)
else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
- flash[:notice] = flash[:notice] + " Please sign in."
+ flash[:notice] = flash[:notice] + _(" Please sign in.")
new_session_path(:user, anchor: 'login-pane')
end
end
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/help_controller.rb b/app/controllers/help_controller.rb
index a9d6addd4a4..10cdce98437 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -22,7 +22,7 @@ class HelpController < ApplicationController
end
def show
- @path = clean_path_info(path_params[:path])
+ @path = Rack::Utils.clean_path_info(path_params[:path])
respond_to do |format|
format.any(:markdown, :md, :html) do
@@ -75,35 +75,4 @@ class HelpController < ApplicationController
params
end
-
- PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
-
- # Taken from ActionDispatch::FileHandler
- # Cleans up the path, to prevent directory traversal outside the doc folder.
- def clean_path_info(path_info)
- parts = path_info.split(PATH_SEPS)
-
- clean = []
-
- # Walk over each part of the path
- parts.each do |part|
- # Turn `one//two` or `one/./two` into `one/two`.
- next if part.empty? || part == '.'
-
- if part == '..'
- # Turn `one/two/../` into `one`
- clean.pop
- else
- # Add simple folder names to the clean path.
- clean << part
- end
- end
-
- # If the path was an absolute path (i.e. `/` or `/one/two`),
- # add `/` to the front of the clean path.
- clean.unshift '/' if parts.empty? || parts.first.empty?
-
- # Join all the clean path parts by the path separator.
- ::File.join(*clean)
- end
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 315d1375e02..a78d87eceea 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -13,9 +13,9 @@ class InvitesController < ApplicationController
if member.accept_invite!(current_user)
label, path = source_info(member.source)
- redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}."
+ redirect_to path, notice: _("You have been granted %{member_human_access} access to %{label}.") % { member_human_access: member.human_access, label: label }
else
- redirect_back_or_default(options: { alert: "The invitation could not be accepted." })
+ redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") })
end
end
@@ -30,9 +30,9 @@ class InvitesController < ApplicationController
new_user_session_path
end
- redirect_to path, notice: "You have declined the invitation to join #{label}."
+ redirect_to path, notice: _("You have declined the invitation to join %{label}.") % { label: label }
else
- redirect_back_or_default(options: { alert: "The invitation could not be declined." })
+ redirect_back_or_default(options: { alert: _("The invitation could not be declined.") })
end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index e90e8278c13..d9b3b4bbbd9 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -105,11 +105,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def redirect_identity_link_failed(error_message)
- redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
+ redirect_to profile_account_path, notice: _("Authentication failed: %{error_message}") % { error_message: error_message }
end
def redirect_identity_linked
- redirect_to profile_account_path, notice: 'Authentication method updated'
+ redirect_to profile_account_path, notice: _('Authentication method updated')
end
def handle_service_ticket(provider, ticket)
@@ -147,10 +147,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def handle_signup_error
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
- message = ["Signing in using your #{label} account without a pre-existing GitLab account is not allowed."]
+ message = [_("Signing in using your %{label} account without a pre-existing GitLab account is not allowed.") % { label: label }]
if Gitlab::CurrentSettings.allow_signup?
- message << "Create a GitLab account first, and then connect it to your #{label} account."
+ message << _("Create a GitLab account first, and then connect it to your %{label} account.") % { label: label }
end
flash[:notice] = message.join(' ')
@@ -168,14 +168,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def fail_auth0_login
- flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
+ flash[:alert] = _('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
redirect_to new_user_session_path
end
def handle_disabled_provider
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
- flash[:alert] = "Signing in using #{label} has been disabled"
+ flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label }
redirect_to new_user_session_path
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 28f113b5cbe..77de5cb45c9 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -22,7 +22,7 @@ class PasswordsController < Devise::PasswordsController
).first_or_initialize
unless user.reset_password_period_valid?
- flash[:alert] = 'Your password reset token has expired.'
+ flash[:alert] = _('Your password reset token has expired.')
redirect_to(new_user_password_url(user_email: user['email']))
end
end
@@ -52,7 +52,7 @@ class PasswordsController < Devise::PasswordsController
end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
- alert: "Password authentication is unavailable."
+ alert: _("Password authentication is unavailable.")
end
def throttle_reset
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
index 2e78b9e6dc7..80b8279e91e 100644
--- a/app/controllers/profiles/chat_names_controller.rb
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -15,9 +15,9 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
new_chat_name = current_user.chat_names.new(chat_name_params)
if new_chat_name.save
- flash[:notice] = "Authorized #{new_chat_name.chat_name}"
+ flash[:notice] = _("Authorized %{new_chat_name}") % { new_chat_name: new_chat_name.chat_name }
else
- flash[:alert] = "Could not authorize chat nickname. Try again!"
+ flash[:alert] = _("Could not authorize chat nickname. Try again!")
end
delete_chat_name_token
@@ -27,7 +27,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
def deny
delete_chat_name_token
- flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}."
+ flash[:notice] = _("Denied authorization of chat nickname %{user_name}.") % { user_name: chat_name_params[:user_name] }
redirect_to profile_chat_names_path
end
@@ -36,9 +36,9 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
@chat_name = chat_names.find(params[:id])
if @chat_name.destroy
- flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!"
+ flash[:notice] = _("Deleted chat nickname: %{chat_name}!") % { chat_name: @chat_name.chat_name }
else
- flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
+ flash[:alert] = _("Could not delete chat nickname %{chat_name}.") % { chat_name: @chat_name.chat_name }
end
redirect_to profile_chat_names_path, status: :found
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 4b6ec2697b7..213d900a563 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -11,7 +11,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
if @personal_access_token.save
PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
- redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
+ redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.")
else
set_index_vars
render :index
@@ -22,9 +22,9 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@personal_access_token = finder.find(params[:id])
if @personal_access_token.revoke!
- flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+ flash[:notice] = _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: @personal_access_token.name }
else
- flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+ flash[:alert] = _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: @personal_access_token.name }
end
redirect_to profile_personal_access_tokens_path
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 0227af2c266..0e30df1b15b 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -11,13 +11,13 @@ class Profiles::PreferencesController < Profiles::ApplicationController
result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute
if result[:status] == :success
- flash[:notice] = 'Preferences saved.'
+ flash[:notice] = _('Preferences saved.')
else
- flash[:alert] = 'Failed to save preferences.'
+ flash[:alert] = _('Failed to save preferences.')
end
rescue ArgumentError => e
# Raised when `dashboard` is given an invalid value.
- flash[:alert] = "Failed to save preferences (#{e.message})."
+ flash[:alert] = _("Failed to save preferences (%{error_message}).") % { error_message: e.message }
end
respond_to do |format|
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
index e6a154fb6aa..866c4dee6e2 100644
--- a/app/controllers/profiles/u2f_registrations_controller.rb
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -4,6 +4,6 @@ class Profiles::U2fRegistrationsController < Profiles::ApplicationController
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
u2f_registration.destroy
- redirect_to profile_two_factor_auth_path, status: 302, notice: "Successfully deleted U2F device."
+ redirect_to profile_two_factor_auth_path, status: 302, notice: _("Successfully deleted U2F device.")
end
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/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 7e072788fc9..b04ffe80db4 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -9,6 +9,8 @@ class Projects::BlobController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
prepend_before_action :authenticate_user!, only: [:edit]
+ around_action :allow_gitaly_ref_name_caching, only: [:show]
+
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 2510a31c9b3..f540ccee386 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -7,6 +7,7 @@ class Projects::CommitsController < Projects::ApplicationController
include RendersCommits
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
+ around_action :allow_gitaly_ref_name_caching
before_action :whitelist_query_limiting, except: :commits_root
before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root
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..e35f34be23c 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])
+ 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/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index b97fbe19bbf..b3447812ef2 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -4,6 +4,8 @@ class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
include TreeHelper
+ around_action :allow_gitaly_ref_name_caching, only: [:logs_tree]
+
before_action :require_non_empty_project
before_action :validate_ref_id
before_action :assign_ref_vars
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/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb
index 334e1847cc8..5e4c601a693 100644
--- a/app/controllers/projects/tags/releases_controller.rb
+++ b/app/controllers/projects/tags/releases_controller.rb
@@ -12,16 +12,13 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController
end
def update
- # Release belongs to Tag which is not active record object,
- # it exists only to save a description to each Tag.
- # If description is empty we should destroy the existing record.
if release_params[:description].present?
release.update(release_params)
else
release.destroy
end
- redirect_to project_tag_path(@project, @tag.name)
+ redirect_to project_tag_path(@project, tag.name)
end
private
@@ -30,11 +27,10 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController
@tag ||= @repository.find_tag(params[:tag_id])
end
- # rubocop: disable CodeReuse/ActiveRecord
def release
- @release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
+ @release ||= Releases::CreateService.new(project, current_user, tag: @tag.name)
+ .find_or_build_release
end
- # rubocop: enable CodeReuse/ActiveRecord
def release_params
params.require(:release).permit(:description)
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 90d53aa08ea..7509cc29a76 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -6,6 +6,8 @@ class Projects::TreeController < Projects::ApplicationController
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
+ around_action :allow_gitaly_ref_name_caching, only: [:show]
+
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
before_action :assign_dir_vars, only: [:create_dir]
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..89dc43a48a1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -10,6 +10,8 @@ class ProjectsController < Projects::ApplicationController
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
+ around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
+
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve]
before_action :redirect_git_extension, only: [:show]
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 8b8d87524a8..0fa4677ced1 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -27,7 +27,7 @@ class RegistrationsController < Devise::RegistrationsController
persist_accepted_terms_if_required(new_user)
end
else
- flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash[:alert] = s_('Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
render action: 'new'
end
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 2b76921ebd8..77757c4a3ef 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -16,7 +16,7 @@ class SentNotificationsController < ApplicationController
noteable = @sent_notification.noteable
noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
- flash[:notice] = "You have been unsubscribed from this thread."
+ flash[:notice] = _("You have been unsubscribed from this thread.")
if current_user
redirect_to noteable_path(noteable)
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 4bd7d71e264..6943795e8ac 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -70,7 +70,7 @@ class SessionsController < Devise::SessionsController
increment_failed_login_captcha_counter
self.resource = resource_class.new
- flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
flash.delete :recaptcha_error
respond_with_navigational(resource) { render :new }
@@ -122,7 +122,7 @@ class SessionsController < Devise::SessionsController
end
redirect_to edit_user_password_path(reset_password_token: @token),
- notice: "Please create a password for your new account."
+ notice: _("Please create a password for your new account.")
end
# rubocop: enable CodeReuse/ActiveRecord
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..1afe000c5f8 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -1,13 +1,43 @@
# frozen_string_literal: true
class GitlabSchema < GraphQL::Schema
+ # Currently an IntrospectionQuery has a complexity of 179.
+ # These values will evolve over time.
+ DEFAULT_MAX_COMPLEXITY = 200
+ AUTHENTICATED_COMPLEXITY = 250
+ ADMIN_COMPLEXITY = 300
+
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 3e1bb9af5cc..7e631053b54 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -19,10 +19,14 @@ module BlobHelper
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
segments = [ide_path, 'project', project.full_path, 'edit', ref]
- segments.concat(['-', path]) if path.present?
+ segments.concat(['-', encode_ide_path(path)]) if path.present?
File.join(segments)
end
+ def encode_ide_path(path)
+ url_encode(path).gsub('%2F', '/')
+ end
+
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
return unless blob = readable_blob(options, path, project, ref)
@@ -73,7 +77,7 @@ module BlobHelper
project,
ref,
path,
- label: "Replace",
+ label: _("Replace"),
action: "replace",
btn_class: "default",
modal_type: "upload"
@@ -85,7 +89,7 @@ module BlobHelper
project,
ref,
path,
- label: "Delete",
+ label: _("Delete"),
action: "delete",
btn_class: "remove",
modal_type: "remove"
@@ -97,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
@@ -197,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/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 01d96754518..b81a3cf8362 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -750,6 +750,10 @@ module Ci
self.sha == sha || self.source_sha == sha
end
+ def triggered_by?(current_user)
+ user == current_user
+ end
+
private
def ci_yaml_from_repo
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/noteable.rb b/app/models/concerns/noteable.rb
index 3c74034b527..423ce7e7db1 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -13,6 +13,14 @@ module Noteable
end
end
+ # The timestamp of the note (e.g. the :updated_at attribute if provided via
+ # API call)
+ def system_note_timestamp
+ @system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ attr_writer :system_note_timestamp
+
def base_class_name
self.class.base_class.name
end
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/gpg_signature.rb b/app/models/gpg_signature.rb
index 7f9ff7bbda6..46cac1d41bb 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -38,6 +38,15 @@ class GpgSignature < ApplicationRecord
.safe_find_or_create_by!(commit_sha: attributes[:commit_sha])
end
+ # Find commits that are lacking a signature in the database at present
+ def self.unsigned_commit_shas(commit_shas)
+ return [] if commit_shas.empty?
+
+ signed = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha)
+
+ commit_shas - signed
+ end
+
def gpg_key=(model)
case model
when GpgKey
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/instance_configuration.rb b/app/models/instance_configuration.rb
index 11289887e00..a9b1962f24c 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -39,7 +39,7 @@ class InstanceConfiguration
def gitlab_ci
Settings.gitlab_ci
.to_h
- .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes,
+ .merge(artifacts_max_size: { value: Gitlab::CurrentSettings.max_artifacts_size.megabytes,
default: 100.megabytes })
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 97c6dcc4745..261935fd054 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -90,7 +90,7 @@ class Issue < ApplicationRecord
state :closed
before_transition any => :closed do |issue|
- issue.closed_at = Time.zone.now
+ issue.closed_at = issue.system_note_timestamp
end
before_transition closed: :opened do |issue|
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b780b67492f..458c57c1dc6 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -858,15 +858,6 @@ class MergeRequest < ApplicationRecord
end
def related_notes
- # Fetch comments only from last 100 commits
- commits_for_notes_limit = 100
- commit_ids = commit_shas.take(commits_for_notes_limit)
-
- commit_notes = Note
- .except(:order)
- .where(project_id: [source_project_id, target_project_id])
- .for_commit_id(commit_ids)
-
# We're using a UNION ALL here since this results in better performance
# compared to using OR statements. We're using UNION ALL since the queries
# used won't produce any duplicates (e.g. a note for a commit can't also be
@@ -878,6 +869,16 @@ class MergeRequest < ApplicationRecord
alias_method :discussion_notes, :related_notes
+ def commit_notes
+ # Fetch comments only from last 100 commits
+ commit_ids = commit_shas.take(100)
+
+ Note
+ .user
+ .where(project_id: [source_project_id, target_project_id])
+ .for_commit_id(commit_ids)
+ end
+
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
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/release.rb b/app/models/release.rb
index 746fc31a038..0f9e94373c7 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -15,6 +15,7 @@ class Release < ApplicationRecord
accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true
+ validates :name, presence: true, on: :create
scope :sorted, -> { order(created_at: :desc) }
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/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 2c90b8a73cd..662c29a0973 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -14,6 +14,10 @@ module Ci
@subject.external?
end
+ condition(:triggerer_of_pipeline) do
+ @subject.triggered_by?(@user)
+ end
+
# Disallow users without permissions from accessing internal pipelines
rule { ~can?(:read_build) & ~external_pipeline }.policy do
prevent :read_pipeline
@@ -29,6 +33,14 @@ module Ci
enable :destroy_pipeline
end
+ rule { can?(:admin_pipeline) }.policy do
+ enable :read_pipeline_variable
+ end
+
+ rule { can?(:update_pipeline) & triggerer_of_pipeline }.policy do
+ enable :read_pipeline_variable
+ end
+
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project)
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/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb
deleted file mode 100644
index ece9fbbef43..00000000000
--- a/app/services/after_branch_delete_service.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# Branch can be deleted either by DeleteBranchService or by Git::BranchPushService.
-class AfterBranchDeleteService < BaseService
- attr_reader :branch_name
-
- def execute(branch_name)
- @branch_name = branch_name
-
- stop_environments
- end
-
- private
-
- def stop_environments
- Ci::StopEnvironmentsService
- .new(project, current_user)
- .execute(branch_name)
- end
-end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 8973c5ffc9e..41dee4e5641 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -37,7 +37,7 @@ module Ci
variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user,
- push_options: params[:push_options],
+ push_options: params[:push_options] || {},
chat_data: params[:chat_data],
**extra_options(options))
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/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 8322a3d74f4..4c3ac19f754 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -29,14 +29,4 @@ class DeleteBranchService < BaseService
def success(message)
super().merge(message: message)
end
-
- def build_push_data(branch)
- Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- branch.dereferenced_target.sha,
- Gitlab::Git::BLANK_SHA,
- "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
- [])
- end
end
diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb
index 4e92353a13c..8d08f0cda94 100644
--- a/app/services/error_tracking/list_projects_service.rb
+++ b/app/services/error_tracking/list_projects_service.rb
@@ -15,8 +15,8 @@ module ErrorTracking
result = setting.list_sentry_projects
rescue Sentry::Client::Error => e
return error(e.message, :bad_request)
- rescue Sentry::Client::SentryError => e
- return error(e.message, :unprocessable_entity)
+ rescue Sentry::Client::MissingKeysError => e
+ return error(e.message, :internal_server_error)
end
success(projects: result[:projects])
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
new file mode 100644
index 00000000000..fce4040e390
--- /dev/null
+++ b/app/services/git/base_hooks_service.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Git
+ class BaseHooksService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ # The N most recent commits to process in a single push payload.
+ PROCESS_COMMIT_LIMIT = 100
+
+ def execute
+ project.repository.after_create if project.empty_repo?
+
+ create_events
+ create_pipelines
+ execute_project_hooks
+
+ # Not a hook, but it needs access to the list of changed commits
+ enqueue_invalidate_cache
+
+ push_data
+ end
+
+ private
+
+ def hook_name
+ raise NotImplementedError, "Please implement #{self.class}##{__method__}"
+ end
+
+ def commits
+ raise NotImplementedError, "Please implement #{self.class}##{__method__}"
+ end
+
+ def limited_commits
+ commits.last(PROCESS_COMMIT_LIMIT)
+ end
+
+ def commits_count
+ commits.count
+ end
+
+ def event_message
+ nil
+ end
+
+ def invalidated_file_types
+ []
+ end
+
+ def create_events
+ EventCreateService.new.push(project, current_user, push_data)
+ end
+
+ def create_pipelines
+ Ci::CreatePipelineService
+ .new(project, current_user, push_data)
+ .execute(:push, pipeline_options)
+ end
+
+ def execute_project_hooks
+ project.execute_hooks(push_data, hook_name)
+ project.execute_services(push_data, hook_name)
+ end
+
+ def enqueue_invalidate_cache
+ ProjectCacheWorker.perform_async(
+ project.id,
+ invalidated_file_types,
+ [:commit_count, :repository_size]
+ )
+ end
+
+ def push_data
+ @push_data ||= Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ limited_commits,
+ event_message,
+ commits_count: commits_count,
+ push_options: params[:push_options] || {}
+ )
+
+ # Dependent code may modify the push data, so return a duplicate each time
+ @push_data.dup
+ end
+
+ # to be overridden in EE
+ def pipeline_options
+ {}
+ end
+ end
+end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
new file mode 100644
index 00000000000..d21a6bb1b9a
--- /dev/null
+++ b/app/services/git/branch_hooks_service.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+module Git
+ class BranchHooksService < ::Git::BaseHooksService
+ def execute
+ execute_branch_hooks
+
+ super.tap do
+ enqueue_update_gpg_signatures
+ end
+ end
+
+ private
+
+ def hook_name
+ :push_hooks
+ end
+
+ def commits
+ strong_memoize(:commits) do
+ if creating_default_branch?
+ # The most recent PROCESS_COMMIT_LIMIT commits in the default branch
+ offset = [count_commits_in_branch - PROCESS_COMMIT_LIMIT, 0].max
+ project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
+ elsif creating_branch?
+ # Use the pushed commits that aren't reachable by the default branch
+ # as a heuristic. This may include more commits than are actually
+ # pushed, but that shouldn't matter because we check for existing
+ # cross-references later.
+ project.repository.commits_between(project.default_branch, params[:newrev])
+ elsif updating_branch?
+ project.repository.commits_between(params[:oldrev], params[:newrev])
+ else # removing branch
+ []
+ end
+ end
+ end
+
+ def commits_count
+ return count_commits_in_branch if creating_default_branch?
+
+ super
+ end
+
+ def invalidated_file_types
+ return super unless default_branch? && !creating_branch?
+
+ paths = limited_commits.each_with_object(Set.new) do |commit, set|
+ commit.raw_deltas.each do |diff|
+ set << diff.new_path
+ end
+ end
+
+ Gitlab::FileDetector.types_in_paths(paths)
+ end
+
+ def execute_branch_hooks
+ project.repository.after_push_commit(branch_name)
+
+ branch_create_hooks if creating_branch?
+ branch_update_hooks if updating_branch?
+ branch_change_hooks if creating_branch? || updating_branch?
+ branch_remove_hooks if removing_branch?
+ end
+
+ def branch_create_hooks
+ project.repository.after_create_branch
+ project.after_create_default_branch if default_branch?
+ end
+
+ def branch_update_hooks
+ # Update the bare repositories info/attributes file using the contents of
+ # the default branch's .gitattributes file
+ project.repository.copy_gitattributes(params[:ref]) if default_branch?
+ end
+
+ def branch_change_hooks
+ enqueue_process_commit_messages
+ end
+
+ def branch_remove_hooks
+ project.repository.after_remove_branch
+ end
+
+ # Schedules processing of commit messages
+ def enqueue_process_commit_messages
+ # don't process commits for the initial push to the default branch
+ return if creating_default_branch?
+
+ limited_commits.each do |commit|
+ next unless commit.matches_cross_reference_regex?
+
+ ProcessCommitWorker.perform_async(
+ project.id,
+ current_user.id,
+ commit.to_hash,
+ default_branch?
+ )
+ end
+ end
+
+ def enqueue_update_gpg_signatures
+ unsigned = GpgSignature.unsigned_commit_shas(limited_commits.map(&:sha))
+ return if unsigned.empty?
+
+ signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
+ return if signable.empty?
+
+ CreateGpgSignatureWorker.perform_async(signable, project.id)
+ end
+
+ def creating_branch?
+ Gitlab::Git.blank_ref?(params[:oldrev])
+ end
+
+ def updating_branch?
+ !creating_branch? && !removing_branch?
+ end
+
+ def removing_branch?
+ Gitlab::Git.blank_ref?(params[:newrev])
+ end
+
+ def creating_default_branch?
+ creating_branch? && default_branch?
+ end
+
+ def count_commits_in_branch
+ strong_memoize(:count_commits_in_branch) do
+ project.repository.commit_count_for_ref(params[:ref])
+ end
+ end
+
+ def default_branch?
+ strong_memoize(:default_branch) do
+ [nil, branch_name].include?(project.default_branch)
+ end
+ end
+
+ def branch_name
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
+ end
+ end
+end
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index b55aeb5f2b9..da053ce80c7 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -1,14 +1,10 @@
# frozen_string_literal: true
module Git
- class BranchPushService < BaseService
- attr_accessor :push_data, :push_commits
+ class BranchPushService < ::BaseService
include Gitlab::Access
include Gitlab::Utils::StrongMemoize
- # The N most recent commits to process in a single push payload.
- PROCESS_COMMIT_LIMIT = 100
-
# This method will be called after each git update
# and only if the provided user and project are present in GitLab.
#
@@ -23,108 +19,43 @@ module Git
# 6. Checks if the project's main language has changed
#
def execute
- update_commits
+ return unless Gitlab::Git.branch_ref?(params[:ref])
+
+ enqueue_update_mrs
+ enqueue_detect_repository_languages
+
execute_related_hooks
perform_housekeeping
update_remote_mirrors
- update_caches
+ stop_environments
- update_signatures
+ true
end
- def update_commits
- project.repository.after_create if project.empty_repo?
- project.repository.after_push_commit(branch_name)
-
- if push_remove_branch?
- project.repository.after_remove_branch
- @push_commits = []
- elsif push_to_new_branch?
- project.repository.after_create_branch
-
- # Re-find the pushed commits.
- if default_branch?
- # Initial push to the default branch. Take the full history of that branch as "newly pushed".
- process_default_branch
- else
- # Use the pushed commits that aren't reachable by the default branch
- # as a heuristic. This may include more commits than are actually pushed, but
- # that shouldn't matter because we check for existing cross-references later.
- @push_commits = project.repository.commits_between(project.default_branch, params[:newrev])
-
- # don't process commits for the initial push to the default branch
- process_commit_messages
- end
- elsif push_to_existing_branch?
- # Collect data for this git push
- @push_commits = project.repository.commits_between(params[:oldrev], params[:newrev])
-
- process_commit_messages
-
- # Update the bare repositories info/attributes file using the contents of the default branches
- # .gitattributes file
- update_gitattributes if default_branch?
- end
- end
-
- def update_gitattributes
- project.repository.copy_gitattributes(params[:ref])
- end
-
- def update_caches
- if default_branch?
- if push_to_new_branch?
- # If this is the initial push into the default branch, the file type caches
- # will already be reset as a result of `Project#change_head`.
- types = []
- else
- paths = Set.new
-
- last_pushed_commits.each do |commit|
- commit.raw_deltas.each do |diff|
- paths << diff.new_path
- end
- end
-
- types = Gitlab::FileDetector.types_in_paths(paths.to_a)
- end
-
- DetectRepositoryLanguagesWorker.perform_async(@project.id, current_user.id)
- else
- types = []
- end
-
- ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size])
+ # Update merge requests that may be affected by this push. A new branch
+ # could cause the last commit of a merge request to change.
+ def enqueue_update_mrs
+ UpdateMergeRequestsWorker.perform_async(
+ project.id,
+ current_user.id,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref]
+ )
end
- # rubocop: disable CodeReuse/ActiveRecord
- def update_signatures
- commit_shas = last_pushed_commits.map(&:sha)
+ def enqueue_detect_repository_languages
+ return unless default_branch?
- return if commit_shas.empty?
-
- shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha)
- commit_shas -= shas_with_cached_signatures
-
- return if commit_shas.empty?
-
- commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas)
-
- CreateGpgSignatureWorker.perform_async(commit_shas, project.id)
+ DetectRepositoryLanguagesWorker.perform_async(project.id, current_user.id)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # Schedules processing of commit messages.
- def process_commit_messages
- default = default_branch?
+ # Only stop environments if the ref is a branch that is being deleted
+ def stop_environments
+ return unless removing_branch?
- last_pushed_commits.each do |commit|
- if commit.matches_cross_reference_regex?
- ProcessCommitWorker
- .perform_async(project.id, current_user.id, commit.to_hash, default)
- end
- end
+ Ci::StopEnvironmentsService.new(project, current_user).execute(branch_name)
end
def update_remote_mirrors
@@ -135,23 +66,7 @@ module Git
end
def execute_related_hooks
- # Update merge requests that may be affected by this push. A new branch
- # could cause the last commit of a merge request to change.
- #
- UpdateMergeRequestsWorker
- .perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
-
- EventCreateService.new.push(project, current_user, build_push_data)
- Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options)
-
- project.execute_hooks(build_push_data.dup, :push_hooks)
- project.execute_services(build_push_data.dup, :push_hooks)
-
- if push_remove_branch?
- AfterBranchDeleteService
- .new(project, current_user)
- .execute(branch_name)
- end
+ BranchHooksService.new(project, current_user, params).execute
end
def perform_housekeeping
@@ -161,84 +76,18 @@ module Git
rescue Projects::HousekeepingService::LeaseTaken
end
- def process_default_branch
- offset = [push_commits_count_for_ref - PROCESS_COMMIT_LIMIT, 0].max
- @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT)
-
- project.after_create_default_branch
- end
-
- def build_push_data
- @push_data ||= Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- @push_commits,
- commits_count: commits_count,
- push_options: params[:push_options] || []
- )
- end
-
- def push_to_existing_branch?
- # Return if this is not a push to a branch (e.g. new commits)
- branch_ref? && !Gitlab::Git.blank_ref?(params[:oldrev])
- end
-
- def push_to_new_branch?
- strong_memoize(:push_to_new_branch) do
- branch_ref? && Gitlab::Git.blank_ref?(params[:oldrev])
- end
- end
-
- def push_remove_branch?
- strong_memoize(:push_remove_branch) do
- branch_ref? && Gitlab::Git.blank_ref?(params[:newrev])
- end
- end
-
- def default_branch?
- branch_ref? &&
- (branch_name == project.default_branch || project.default_branch.nil?)
- end
-
- def commit_user(commit)
- commit.author || current_user
+ def removing_branch?
+ Gitlab::Git.blank_ref?(params[:newrev])
end
def branch_name
- strong_memoize(:branch_name) do
- Gitlab::Git.ref_name(params[:ref])
- end
+ strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
end
- def branch_ref?
- strong_memoize(:branch_ref) do
- Gitlab::Git.branch_ref?(params[:ref])
- end
- end
-
- def commits_count
- return push_commits_count_for_ref if default_branch? && push_to_new_branch?
-
- Array(@push_commits).size
- end
-
- def push_commits_count_for_ref
- strong_memoize(:push_commits_count_for_ref) do
- project.repository.commit_count_for_ref(params[:ref])
+ def default_branch?
+ strong_memoize(:default_branch) do
+ [nil, branch_name].include?(project.default_branch)
end
end
-
- def last_pushed_commits
- @last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT)
- end
-
- private
-
- def pipeline_options
- {} # to be overridden in EE
- end
end
end
diff --git a/app/services/git/tag_hooks_service.rb b/app/services/git/tag_hooks_service.rb
new file mode 100644
index 00000000000..18eb780579f
--- /dev/null
+++ b/app/services/git/tag_hooks_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Git
+ class TagHooksService < ::Git::BaseHooksService
+ private
+
+ def hook_name
+ :tag_push_hooks
+ end
+
+ def commits
+ [tag_commit].compact
+ end
+
+ def event_message
+ tag&.message
+ end
+
+ def tag
+ strong_memoize(:tag) do
+ next if Gitlab::Git.blank_ref?(params[:newrev])
+
+ tag_name = Gitlab::Git.ref_name(params[:ref])
+ tag = project.repository.find_tag(tag_name)
+
+ tag if tag && tag.target == params[:newrev]
+ end
+ end
+
+ def tag_commit
+ strong_memoize(:tag_commit) do
+ project.commit(tag.dereferenced_target) if tag
+ end
+ end
+ end
+end
diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb
index 318dfd4f886..ee4166dccd0 100644
--- a/app/services/git/tag_push_service.rb
+++ b/app/services/git/tag_push_service.rb
@@ -1,68 +1,14 @@
# frozen_string_literal: true
module Git
- class TagPushService < BaseService
- attr_accessor :push_data
-
+ class TagPushService < ::BaseService
def execute
- project.repository.after_create if project.empty_repo?
- project.repository.before_push_tag
-
- @push_data = build_push_data
-
- EventCreateService.new.push(project, current_user, push_data)
- Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options)
+ return unless Gitlab::Git.tag_ref?(params[:ref])
- SystemHooksService.new.execute_hooks(build_system_push_data, :tag_push_hooks)
- project.execute_hooks(push_data.dup, :tag_push_hooks)
- project.execute_services(push_data.dup, :tag_push_hooks)
-
- ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
+ project.repository.before_push_tag
+ TagHooksService.new(project, current_user, params).execute
true
end
-
- private
-
- def build_push_data
- commits = []
- message = nil
-
- unless Gitlab::Git.blank_ref?(params[:newrev])
- tag_name = Gitlab::Git.ref_name(params[:ref])
- tag = project.repository.find_tag(tag_name)
-
- if tag && tag.target == params[:newrev]
- commit = project.commit(tag.dereferenced_target)
- commits = [commit].compact
- message = tag.message
- end
- end
-
- Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- commits,
- message,
- push_options: params[:push_options] || [])
- end
-
- def build_system_push_data
- Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- [],
- '')
- end
-
- def pipeline_options
- {} # to be overridden in EE
- end
end
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/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
new file mode 100644
index 00000000000..d92eb0a68c3
--- /dev/null
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class PushOptionsHandlerService
+ LIMIT = 10
+
+ attr_reader :branches, :changes_by_branch, :current_user, :errors,
+ :project, :push_options, :target_project
+
+ def initialize(project, current_user, changes, push_options)
+ @project = project
+ @target_project = @project.default_merge_request_target
+ @current_user = current_user
+ @branches = get_branches(changes)
+ @push_options = push_options
+ @errors = []
+ end
+
+ def execute
+ validate_service
+ return self if errors.present?
+
+ branches.each do |branch|
+ execute_for_branch(branch)
+ rescue Gitlab::Access::AccessDeniedError
+ errors << 'User access was denied'
+ rescue StandardError => e
+ Gitlab::AppLogger.error(e)
+ errors << 'An unknown error occurred'
+ end
+
+ self
+ end
+
+ private
+
+ def get_branches(raw_changes)
+ Gitlab::ChangesList.new(raw_changes).map do |changes|
+ next unless Gitlab::Git.branch_ref?(changes[:ref])
+
+ # Deleted branch
+ next if Gitlab::Git.blank_ref?(changes[:newrev])
+
+ # Default branch
+ branch_name = Gitlab::Git.branch_name(changes[:ref])
+ next if branch_name == target_project.default_branch
+
+ branch_name
+ end.compact.uniq
+ end
+
+ def validate_service
+ errors << 'User is required' if current_user.nil?
+
+ unless target_project.merge_requests_enabled?
+ errors << "Merge requests are not enabled for project #{target_project.full_path}"
+ end
+
+ if branches.size > LIMIT
+ errors << "Too many branches pushed (#{branches.size} were pushed, limit is #{LIMIT})"
+ end
+
+ if push_options[:target] && !target_project.repository.branch_exists?(push_options[:target])
+ errors << "Branch #{push_options[:target]} does not exist"
+ end
+ end
+
+ # Returns a Hash of branch => MergeRequest
+ def merge_requests
+ @merge_requests ||= MergeRequest.from_project(target_project)
+ .opened
+ .from_source_branches(branches)
+ .index_by(&:source_branch)
+ end
+
+ def execute_for_branch(branch)
+ merge_request = merge_requests[branch]
+
+ if merge_request
+ update!(merge_request)
+ else
+ create!(branch)
+ end
+ end
+
+ def create!(branch)
+ unless push_options[:create]
+ errors << "A merge_request.create push option is required to create a merge request for branch #{branch}"
+ return
+ end
+
+ # Use BuildService to assign the standard attributes of a merge request
+ merge_request = ::MergeRequests::BuildService.new(
+ project,
+ current_user,
+ create_params(branch)
+ ).execute
+
+ unless merge_request.errors.present?
+ merge_request = ::MergeRequests::CreateService.new(
+ project,
+ current_user,
+ merge_request.attributes
+ ).execute
+ end
+
+ collect_errors_from_merge_request(merge_request) unless merge_request.persisted?
+ end
+
+ def update!(merge_request)
+ merge_request = ::MergeRequests::UpdateService.new(
+ target_project,
+ current_user,
+ update_params
+ ).execute(merge_request)
+
+ collect_errors_from_merge_request(merge_request) unless merge_request.valid?
+ end
+
+ def create_params(branch)
+ params = {
+ assignee: current_user,
+ source_branch: branch,
+ source_project: project,
+ target_branch: push_options[:target] || target_project.default_branch,
+ target_project: target_project
+ }
+
+ if push_options.key?(:merge_when_pipeline_succeeds)
+ params.merge!(
+ merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
+ merge_user: current_user
+ )
+ end
+
+ params
+ end
+
+ def update_params
+ params = {}
+
+ if push_options.key?(:merge_when_pipeline_succeeds)
+ params.merge!(
+ merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
+ merge_user: current_user
+ )
+ end
+
+ if push_options.key?(:target)
+ params[:target_branch] = push_options[:target]
+ end
+
+ params
+ end
+
+ def collect_errors_from_merge_request(merge_request)
+ merge_request.errors.full_messages.each do |error|
+ errors << error
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 51d27673787..e0460f7081c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
post_merge_manually_merged
reload_merge_requests
outdate_suggestions
+ refresh_pipelines_on_merge_requests
reset_merge_when_pipeline_succeeds
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -107,8 +108,6 @@ module MergeRequests
end
merge_request.mark_as_unchecked
- create_pipeline_for(merge_request, current_user)
- UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
# Upcoming method calls need the refreshed version of
@@ -134,6 +133,13 @@ module MergeRequests
end
end
+ def refresh_pipelines_on_merge_requests
+ merge_requests_for_source_branch.each do |merge_request|
+ create_pipeline_for(merge_request, current_user)
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
+ end
+ end
+
def reset_merge_when_pipeline_succeeds
merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
end
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
index 81f6f92f75c..60a68568833 100644
--- a/app/services/note_summary.rb
+++ b/app/services/note_summary.rb
@@ -5,7 +5,9 @@ class NoteSummary
attr_reader :metadata
def initialize(noteable, project, author, body, action: nil, commit_count: nil)
- @note = { noteable: noteable, project: project, author: author, note: body }
+ @note = { noteable: noteable,
+ created_at: noteable.system_note_timestamp,
+ project: project, author: author, note: body }
@metadata = { action: action, commit_count: commit_count }.compact
set_commit_params if note[:noteable].is_a?(Commit)
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/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb
new file mode 100644
index 00000000000..f32a779fab0
--- /dev/null
+++ b/app/services/projects/update_statistics_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Projects
+ class UpdateStatisticsService < BaseService
+ def execute
+ return unless project && project.repository.exists?
+
+ Rails.logger.info("Updating statistics for project #{project.id}")
+
+ project.statistics.refresh!(only: statistics.map(&:to_sym))
+ end
+
+ private
+
+ def statistics
+ params[:statistics]
+ end
+ 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/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index f463e08ee7e..8ff73522e5f 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -96,14 +96,27 @@ module QuickActions
end
def find_labels(labels_params = nil)
+ extract_references(labels_params, :label) | find_labels_by_name_no_tilde(labels_params)
+ end
+
+ def find_labels_by_name_no_tilde(labels_params)
+ return Label.none if label_with_tilde?(labels_params)
+
finder_params = { include_ancestor_groups: true }
finder_params[:project_id] = project.id if project
finder_params[:group_id] = group.id if group
- finder_params[:name] = labels_params.split if labels_params
+ finder_params[:name] = extract_label_names(labels_params) if labels_params
- result = LabelsFinder.new(current_user, finder_params).execute
+ LabelsFinder.new(current_user, finder_params).execute
+ end
+
+ def label_with_tilde?(labels_params)
+ labels_params&.include?('~')
+ end
- extract_references(labels_params, :label) | result
+ def extract_label_names(labels_params)
+ # '"A" "A B C" A B' => ["A", "A B C", "A", "B"]
+ labels_params.scan(/"([^"]+)"|([^ ]+)/).flatten.compact
end
def find_label_references(labels_param)
diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb
index a04bb8f9e14..ff6b696ca96 100644
--- a/app/services/releases/concerns.rb
+++ b/app/services/releases/concerns.rb
@@ -15,7 +15,7 @@ module Releases
end
def name
- params[:name]
+ params[:name] || tag_name
end
def description
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index c6e143d440d..a271a7e5e49 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -15,6 +15,10 @@ module Releases
create_release(tag)
end
+ def find_or_build_release
+ release || build_release(existing_tag)
+ end
+
private
def ensure_tag
@@ -38,7 +42,17 @@ module Releases
end
def create_release(tag)
- release = project.releases.create!(
+ release = build_release(tag)
+
+ release.save!
+
+ success(tag: tag, release: release)
+ rescue => e
+ error(e.message, 400)
+ end
+
+ def build_release(tag)
+ project.releases.build(
name: name,
description: description,
author: current_user,
@@ -46,10 +60,6 @@ module Releases
sha: tag.dereferenced_target.sha,
links_attributes: params.dig(:assets, 'links') || []
)
-
- success(tag: tag, release: release)
- rescue => e
- error(e.message, 400)
end
end
end
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 039d6e2ebad..b45e567079b 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -12,7 +12,7 @@ module ResourceEvents
label_hash = {
resource_column(resource) => resource.id,
user_id: user.id,
- created_at: Time.now
+ created_at: resource.system_note_timestamp
}
labels = added_labels.map do |label|
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index ea8ac7e4656..acbbb0da929 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -258,7 +258,7 @@ module SystemNoteService
body = "created #{issue.to_reference} to continue this discussion"
note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
- note = Note.create(note_attributes.merge(system: true))
+ note = Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp))
note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
note
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/admin/users/_user_detail.html.haml b/app/views/admin/users/_user_detail.html.haml
index 3319b4bad3a..13d10dcd625 100644
--- a/app/views/admin/users/_user_detail.html.haml
+++ b/app/views/admin/users/_user_detail.html.haml
@@ -6,7 +6,7 @@
= image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 prepend-top-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
= link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id }
- = render_if_exists 'admin/users/user_detail_note', user: user
+ = render_if_exists 'admin/users/user_listing_note', user: user
- user_badges_in_admin_section(user).each do |badge|
- css_badge = "badge badge-#{badge[:variant]}" if badge[:variant].present?
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 4ffa8d89504..c4178296e67 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -53,6 +53,8 @@
- else
Disabled
+ = render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
+
%li
%span.light External User:
%strong
@@ -134,6 +136,8 @@
%strong
= link_to @user.created_by.name, [:admin, @user.created_by]
+ = render_if_exists partial: "namespaces/shared_runner_status", locals: { namespace: @user.namespace }
+
.col-md-6
- unless @user == current_user
- unless @user.confirmed?
@@ -146,6 +150,9 @@
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
%br
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
+
+ = render_if_exists 'admin/users/user_detail_note'
+
- if @user.blocked?
.card.border-info
.card-header.bg-info.text-white
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index c787d7420b7..369b0f7e62c 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -6,14 +6,14 @@
- tooltip = "#{subject.name} - #{status.status_tooltip}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item d-flex', 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' } }
+ .menu-item.mini-pipeline-graph-dropdown-item.d-flex{ 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/help/index.html.haml b/app/views/help/index.html.haml
index f40fdb0b86b..916f98a62d1 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -24,8 +24,9 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
- %p= link_to 'Check the current instance configuration ', help_instance_configuration_url
- %hr
+
+%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
+%hr
.row.prepend-top-default
.col-md-8
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/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
index 935581643cd..9082bfc409d 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -2,34 +2,29 @@
.form-group
= label_tag :merge_method_merge, class: 'label-bold' do
- Merge method
+ = _('Merge method')
.form-check
= form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input"
= label_tag :project_merge_method_merge, class: 'form-check-label' do
- %strong Merge commit
- %br
- %span.descr
- A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
+ .mb-3
+ = _('Merge commit')
+ .text-secondary
+ = _('A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.')
- .form-check
- = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input"
- = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do
- %strong Merge commit with semi-linear history
- %br
- %span.descr
- A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
- This way you could make sure that if this merge request would build, after merging to target branch it would also build.
- %br
- %span.descr
- When fast-forward merge is not possible, the user is given the option to rebase.
+.form-check
+ = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input"
+ = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do
+ .mb-3
+ = _('Merge commit with semi-linear history')
+ .text-secondary
+ = _('A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. This way you could make sure that if this merge request would build, after merging to target branch it would also build.')
+ .text-secondary
+ = _('When fast-forward merge is not possible, the user is given the option to rebase.')
- .form-check
- = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input"
- = label_tag :project_merge_method_ff, class: 'form-check-label' do
- %strong Fast-forward merge
- %br
- %span.descr
- No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
- %br
- %span.descr
- When fast-forward merge is not possible, the user is given the option to rebase.
+.form-check
+ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input"
+ = label_tag :project_merge_method_ff, class: 'form-check-label' do
+ .mb-3
+ = _('Fast-forward merge')
+ .text-secondary
+ = _('No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. When fast-forward merge is not possible, the user is given the option to rebase.')
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 6ac2e06afa5..3a9f7ca42db 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -4,21 +4,21 @@
.form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
= form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
= form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
- %strong Only allow merge requests to be merged if the pipeline succeeds
- %br
- %span.descr
- Pipelines need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
+ .mb-3
+ = _('Only allow merge requests to be merged if the pipeline succeeds')
+ .text-secondary
+ = _('Pipelines need to be configured to enable this feature.')
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
= render_if_exists 'projects/merge_pipelines_settings', form: form
.form-check
= form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input'
= form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
- %strong Only allow merge requests to be merged if all discussions are resolved
+ %p= _('Only allow merge requests to be merged if all discussions are resolved')
.form-check
= form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input'
= form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do
- %strong Automatically resolve merge request diff discussions when they become outdated
+ %p= _('Automatically resolve merge request diff discussions when they become outdated')
.form-check
= form.check_box :printing_merge_request_link_enabled, class: 'form-check-input'
= form.label :printing_merge_request_link_enabled, class: 'form-check-label' do
- %strong Show link to create/view merge request when pushing from the command line
+ %p= _('Show link to create/view merge request when pushing from the command line')
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/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 9d069c025ba..6d48475d505 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -30,7 +30,7 @@
= custom_icon("icon_commit")
- if commit_sha
- = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha"
+ = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0"
- if job.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.'))
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 282566eeadc..9774b797928 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -6,7 +6,7 @@
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
.icon-container.commit-icon
= custom_icon("icon_commit")
- = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha"
+ = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha mr-0"
%p.commit-title.flex-truncate-parent
%span.flex-truncate-child
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 85bc8ec07e3..a11e23b6daa 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -18,7 +18,7 @@
- if deployment.user
%div
by
- = user_avatar(user: deployment.user, size: 20)
+ = user_avatar(user: deployment.user, size: 20, css_class: "mr-0 float-none")
.table-section.section-15{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created")
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/all_queues.yml b/app/workers/all_queues.yml
index 3e8c2a1209a..f9b2e698fc9 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -143,6 +143,7 @@
- repository_remove_remote
- system_hook_push
- update_merge_requests
+- update_project_statistics
- upload_checksum
- web_hook
- repository_update_remote_mirror
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 396f44396a3..a5554f07699 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,7 +3,7 @@
class PostReceive
include ApplicationWorker
- def perform(gl_repository, identifier, changes, push_options = [])
+ def perform(gl_repository, identifier, changes, push_options = {})
project, repo_type = Gitlab::GlRepository.parse(gl_repository)
if project.nil?
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b31099bc670..b2e0701008a 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -18,7 +18,7 @@ class ProjectCacheWorker
return unless project && project.repository.exists?
- update_statistics(project, statistics.map(&:to_sym))
+ update_statistics(project, statistics)
project.repository.refresh_method_caches(files.map(&:to_sym))
@@ -26,20 +26,28 @@ class ProjectCacheWorker
end
# rubocop: enable CodeReuse/ActiveRecord
+ # NOTE: triggering both an immediate update and one in 15 minutes if we
+ # successfully obtain the lease. That way, we only need to wait for the
+ # statistics to become accurate if they were already updated once in the
+ # last 15 minutes.
def update_statistics(project, statistics = [])
return if Gitlab::Database.read_only?
- return unless try_obtain_lease_for(project.id, :update_statistics)
+ return unless try_obtain_lease_for(project.id, statistics)
- Rails.logger.info("Updating statistics for project #{project.id}")
+ Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
- project.statistics.refresh!(only: statistics)
+ UpdateProjectStatisticsWorker.perform_in(LEASE_TIMEOUT, project.id, statistics)
end
private
- def try_obtain_lease_for(project_id, section)
+ def try_obtain_lease_for(project_id, statistics)
Gitlab::ExclusiveLease
- .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT)
+ .new(project_cache_worker_key(project_id, statistics), timeout: LEASE_TIMEOUT)
.try_obtain
end
+
+ def project_cache_worker_key(project_id, statistics)
+ ["project_cache_worker", project_id, *statistics.sort].join(":")
+ end
end
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/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
new file mode 100644
index 00000000000..9a29cc12707
--- /dev/null
+++ b/app/workers/update_project_statistics_worker.rb
@@ -0,0 +1,18 @@
+
+# frozen_string_literal: true
+
+# Worker for updating project statistics.
+class UpdateProjectStatisticsWorker
+ include ApplicationWorker
+
+ # project_id - The ID of the project for which to flush the cache.
+ # statistics - An Array containing columns from ProjectStatistics to
+ # refresh, if empty all columns will be refreshed
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(project_id, statistics = [])
+ project = Project.find_by(id: project_id)
+
+ Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute
+ 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/30157-api-expose-single-environment.yml b/changelogs/unreleased/30157-api-expose-single-environment.yml
new file mode 100644
index 00000000000..f9619dbcc7d
--- /dev/null
+++ b/changelogs/unreleased/30157-api-expose-single-environment.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add new API endpoint to expose a single environment.'
+merge_request: 26887
+author:
+type: added
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/43263-git-push-option-to-create-mr.yml b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
new file mode 100644
index 00000000000..d50c33da162
--- /dev/null
+++ b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Allow merge requests to be created via git push options
+merge_request: 26752
+author:
+type: added
diff --git a/changelogs/unreleased/47234-composable-auto-devops.yml b/changelogs/unreleased/47234-composable-auto-devops.yml
new file mode 100644
index 00000000000..9403c5ae6d3
--- /dev/null
+++ b/changelogs/unreleased/47234-composable-auto-devops.yml
@@ -0,0 +1,5 @@
+---
+title: Split Auto-DevOps.gitlab-ci.yml into reusable templates
+merge_request: 26520
+author:
+type: changed
diff --git a/changelogs/unreleased/47771-highlighting-in-diff.yml b/changelogs/unreleased/47771-highlighting-in-diff.yml
new file mode 100644
index 00000000000..a8e8cbf0174
--- /dev/null
+++ b/changelogs/unreleased/47771-highlighting-in-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Enabled text selection highlighting in diffs in Web IDE
+merge_request: 26721
+author: Isaac Smith
+type: fixed
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/52560-fix-duplicate-tag-system-hooks.yml b/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml
new file mode 100644
index 00000000000..b8d58d6bd30
--- /dev/null
+++ b/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml
@@ -0,0 +1,5 @@
+---
+title: Only execute system hooks once when pushing tags
+merge_request: 26888
+author:
+type: fixed
diff --git a/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml
new file mode 100644
index 00000000000..6fefd05049c
--- /dev/null
+++ b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml
@@ -0,0 +1,6 @@
+---
+title: Allow merge requests to be set to merge when pipeline succeeds via git push
+ options
+merge_request: 26842
+author:
+type: added
diff --git a/changelogs/unreleased/53279-fix-updated_at-api.yml b/changelogs/unreleased/53279-fix-updated_at-api.yml
new file mode 100644
index 00000000000..c64dada7eaa
--- /dev/null
+++ b/changelogs/unreleased/53279-fix-updated_at-api.yml
@@ -0,0 +1,5 @@
+---
+title: "Respect updated_at attribute in notes produced by API calls"
+merge_request: 27124
+author: Ben Gamari
+type: fixed
diff --git a/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml b/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml
new file mode 100644
index 00000000000..30d8c0e95d7
--- /dev/null
+++ b/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml
@@ -0,0 +1,5 @@
+---
+title: Fix quick actions add label name middle word overlaps
+merge_request: 26602
+author: Jacopo Beschi @jacopo-beschi
+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/55268-exclude-system-notes-from-commits-in-mr.yml b/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml
new file mode 100644
index 00000000000..7af4739136b
--- /dev/null
+++ b/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude system notes from commits in merge request discussions
+merge_request: 26396
+author:
+type: fixed
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/58981-migrate-clusters-tests-to-jest.yml b/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml
new file mode 100644
index 00000000000..3df13dbb960
--- /dev/null
+++ b/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate clusters tests to jest
+merge_request: 27013
+author:
+type: other
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/59621-order-labels-alphabetically-in-issue-boards.yml b/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml
new file mode 100644
index 00000000000..8b82d757303
--- /dev/null
+++ b/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml
@@ -0,0 +1,5 @@
+---
+title: Order labels alphabetically in issue boards
+merge_request: 26927
+author:
+type: changed
diff --git a/changelogs/unreleased/59708-vendor-css.yml b/changelogs/unreleased/59708-vendor-css.yml
new file mode 100644
index 00000000000..ec7def7a9e6
--- /dev/null
+++ b/changelogs/unreleased/59708-vendor-css.yml
@@ -0,0 +1,5 @@
+---
+title: Creates a vendors folder for external CSS
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml b/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml
new file mode 100644
index 00000000000..bfea3ac52af
--- /dev/null
+++ b/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml
@@ -0,0 +1,5 @@
+---
+title: "Make touch events work on image diff swipe view and onion skin"
+merge_request: 26971
+author: ftab
+type: added
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/60116-fix-button-wrapping.yml b/changelogs/unreleased/60116-fix-button-wrapping.yml
new file mode 100644
index 00000000000..d6df920b51d
--- /dev/null
+++ b/changelogs/unreleased/60116-fix-button-wrapping.yml
@@ -0,0 +1,5 @@
+---
+title: Add to white-space nowrap to all buttons
+merge_request: 27069
+author:
+type: fixed
diff --git a/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml b/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml
new file mode 100644
index 00000000000..8c3a47cf62c
--- /dev/null
+++ b/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml
@@ -0,0 +1,5 @@
+---
+title: Handle possible HTTP exception for Sentry client
+merge_request: 27080
+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/always-link-instance-configuration.yml b/changelogs/unreleased/always-link-instance-configuration.yml
new file mode 100644
index 00000000000..3f08747edf7
--- /dev/null
+++ b/changelogs/unreleased/always-link-instance-configuration.yml
@@ -0,0 +1,5 @@
+---
+title: Always show instance configuration link
+merge_request: 26783
+author: Bastian Blank
+type: fixed
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/ce-proj-settings-ok-mr-settings-only.yml b/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml
new file mode 100644
index 00000000000..4bbbc706e62
--- /dev/null
+++ b/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml
@@ -0,0 +1,5 @@
+---
+title: Improve project merge request settings
+merge_request: 26495
+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/delay-update-statictics.yml b/changelogs/unreleased/delay-update-statictics.yml
new file mode 100644
index 00000000000..d0201fb6db8
--- /dev/null
+++ b/changelogs/unreleased/delay-update-statictics.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the bug that the project statistics is not updated
+merge_request: 26854
+author: Hiroyuki Sato
+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/ekigbo-extend-timezone-dropdown.yml b/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml
new file mode 100644
index 00000000000..42bc320a542
--- /dev/null
+++ b/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Extend timezone dropdown
+merge_request: 26311
+author:
+type: changed
diff --git a/changelogs/unreleased/expose-pipeline-variables-via-api.yml b/changelogs/unreleased/expose-pipeline-variables-via-api.yml
new file mode 100644
index 00000000000..f37bf0c5cd8
--- /dev/null
+++ b/changelogs/unreleased/expose-pipeline-variables-via-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose pipeline variables via API
+merge_request: 26501
+author: Agustin Henze <tin@redhat.com>
+type: added
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/feature-webide_escaping.yml b/changelogs/unreleased/feature-webide_escaping.yml
new file mode 100644
index 00000000000..88fa1bd948e
--- /dev/null
+++ b/changelogs/unreleased/feature-webide_escaping.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug with hashes in urls in WebIDE
+merge_request: 54376
+author: Kieran Andrews
+type: fixed
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/fix-pull-request-importer.yml b/changelogs/unreleased/fix-pull-request-importer.yml
new file mode 100644
index 00000000000..5f642a0710b
--- /dev/null
+++ b/changelogs/unreleased/fix-pull-request-importer.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of PR import
+merge_request: 27121
+author:
+type: performance
diff --git a/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml b/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml
new file mode 100644
index 00000000000..770186a64b0
--- /dev/null
+++ b/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed duplicated diff too large error message
+merge_request:
+author:
+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/instance-configuration-artifact-size.yml b/changelogs/unreleased/instance-configuration-artifact-size.yml
new file mode 100644
index 00000000000..077f8631af5
--- /dev/null
+++ b/changelogs/unreleased/instance-configuration-artifact-size.yml
@@ -0,0 +1,5 @@
+---
+title: Display maximum artifact size from runtime config
+merge_request: 26784
+author: Bastian Blank
+type: fixed
diff --git a/changelogs/unreleased/issue-58418-release-notes.yml b/changelogs/unreleased/issue-58418-release-notes.yml
new file mode 100644
index 00000000000..80e6529eb12
--- /dev/null
+++ b/changelogs/unreleased/issue-58418-release-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Set release name when adding release notes to an existing tag
+merge_request: 26807
+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/prevent-running-mr-pipelines-when-target-updated.yml b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml
new file mode 100644
index 00000000000..d003ca55feb
--- /dev/null
+++ b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml
@@ -0,0 +1,5 @@
+---
+title: Create pipelines for merge requests only when source branch is updated
+merge_request: 26921
+author:
+type: fixed
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-add-gitaly-ref-name-caching-tree-controller.yml b/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml
new file mode 100644
index 00000000000..a051c1f70a8
--- /dev/null
+++ b/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Gitaly FindCommit caching for TreeController
+merge_request: 27100
+author:
+type: performance
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-git-gc-after-initial-fetch.yml b/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml
new file mode 100644
index 00000000000..867d7e6b9df
--- /dev/null
+++ b/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml
@@ -0,0 +1,5 @@
+---
+title: 'GitHub import: Run housekeeping after initial import'
+merge_request: 26600
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-improve-find-commit-caching.yml b/changelogs/unreleased/sh-improve-find-commit-caching.yml
new file mode 100644
index 00000000000..1b38684d018
--- /dev/null
+++ b/changelogs/unreleased/sh-improve-find-commit-caching.yml
@@ -0,0 +1,5 @@
+---
+title: Expand FindCommit caching to blob and refs
+merge_request: 27084
+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/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 2dc0da00919..8bc2426ec4c 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -89,4 +89,5 @@
- [project_daily_statistics, 1]
- [import_issues_csv, 2]
- [chat_notification, 2]
- - [migrate_external_diffs, 1]
+ - [migrate_external_diffs, 1]
+ - [update_project_statistics, 1]
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/danger/gitlab_ui_wg/Dangerfile b/danger/gitlab_ui_wg/Dangerfile
new file mode 100644
index 00000000000..02d94fa5ab7
--- /dev/null
+++ b/danger/gitlab_ui_wg/Dangerfile
@@ -0,0 +1,55 @@
+def mention_single_codebase_approvers
+ frontend_maintainers = %w(@filipa @iamphill @psimyn @sarahghp @mishunov)
+ ux_maintainers = %w(@tauriedavis @rverissimo)
+
+ rows = []
+ users = []
+
+ if gitlab.mr_labels.include?('frontend')
+ frontend_maintainer = frontend_maintainers.sample
+
+ rows << "| ~frontend | `#{frontend_maintainer}`"
+ users << frontend_maintainer
+ end
+
+ if gitlab.mr_labels.include?('UX')
+ ux_maintainers = ux_maintainers.sample
+
+ rows << "| ~UX | `#{ux_maintainers}`"
+ users << ux_maintainers
+ end
+
+ if rows.empty?
+ backup_maintainer = frontend_maintainers.sample
+
+ rows << "| ~frontend / ~UX | `#{backup_maintainer}`"
+ users << backup_maintainer
+ end
+
+ markdown(<<~MARKDOWN.strip)
+ ## GitLab UI Working Group changes
+
+ This merge request contains changes related to the work of [cleaning up CSS and creating
+ reusable components](https://gitlab.com/groups/gitlab-org/-/epics/950).
+ These changes will need to be reviewed and approved by the following engineers:
+
+ | Category | Reviewer
+ |----------|---------
+ #{rows.join("\n")}
+
+ To make sure this happens, please follow these steps:
+
+ 1. Add all of the mentioned users to the list of merge request approvals.
+ 2. Assign the merge request to the first person in the above list.
+
+ If you are a reviewer, please follow these steps:
+
+ 1. Review the merge request. If it is good to go, approve it.
+ 2. Once approved, assign to the next person in the above list. If you are
+ the last person in the list, merge the merge request.
+ MARKDOWN
+end
+
+if gitlab.mr_labels.include?('CSS cleanup')
+ mention_single_codebase_approvers
+end
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index 808bc96a0a0..e6820f49ee2 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'digest/md5'
+
MESSAGE = <<MARKDOWN
## Reviewer roulette
@@ -29,15 +31,22 @@ Please consider creating a merge request to
for them.
MARKDOWN
-def spin(team, project, category)
+def spin(team, project, category, branch_name)
+ rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16))
+
reviewers = team.select { |member| member.reviewer?(project, category) }
+ traintainers = team.select { |member| member.traintainer?(project, category) }
maintainers = team.select { |member| member.maintainer?(project, category) }
# TODO: filter out people who are currently not in the office
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652
+ #
# TODO: take CODEOWNERS into account?
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653
- reviewer = reviewers[rand(reviewers.size)]
- maintainer = maintainers[rand(maintainers.size)]
+ # Make traintainers have triple the chance to be picked as a reviewer
+ reviewer = (reviewers + traintainers + traintainers).sample(random: rng)
+ maintainer = maintainers.sample(random: rng)
"| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |"
end
@@ -60,7 +69,15 @@ categories = changes.keys - [:unknown]
# Single codebase MRs are reviewed using a slightly different process, so we
# disable the review roulette for such MRs.
-if changes.any? && !gitlab.mr_labels.include?('single codebase')
+# CSS Clean up MRs are reviewed using a slightly different process, so we
+# disable the review roulette for such MRs.
+if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_labels.include?('CSS cleanup')
+ # Strip leading and trailing CE/EE markers
+ canonical_branch_name = gitlab
+ .mr_json['source_branch']
+ .gsub(/^[ce]e-/, '')
+ .gsub(/-[ce]e$/, '')
+
team =
begin
helper.project_team
@@ -75,7 +92,7 @@ if changes.any? && !gitlab.mr_labels.include?('single codebase')
project = helper.project_name
unknown = changes.fetch(:unknown, [])
- rows = categories.map { |category| spin(team, project, category) }
+ rows = categories.map { |category| spin(team, project, category, canonical_branch_name) }
markdown(MESSAGE)
markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
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/20180115094742_add_default_project_creation_setting.rb b/db/migrate/20180115094742_add_default_project_creation_setting.rb
new file mode 100644
index 00000000000..465a89c39e8
--- /dev/null
+++ b/db/migrate/20180115094742_add_default_project_creation_setting.rb
@@ -0,0 +1,19 @@
+class AddDefaultProjectCreationSetting < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:application_settings, :default_project_creation)
+ add_column_with_default(:application_settings, :default_project_creation, :integer, default: 2)
+ 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/20180115113902_add_project_creation_level_to_groups.rb b/db/migrate/20180115113902_add_project_creation_level_to_groups.rb
new file mode 100644
index 00000000000..a10ce54087c
--- /dev/null
+++ b/db/migrate/20180115113902_add_project_creation_level_to_groups.rb
@@ -0,0 +1,17 @@
+class AddProjectCreationLevelToGroups < ActiveRecord::Migration[4.2]
+ 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
+ if 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/README.md b/doc/README.md
index aead50ea97e..14a1eeffda0 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -288,7 +288,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. |
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
-| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. |
+| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
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/auth/okta.md b/doc/administration/auth/okta.md
index 638405126a5..aa4e1b0d2e0 100644
--- a/doc/administration/auth/okta.md
+++ b/doc/administration/auth/okta.md
@@ -92,18 +92,23 @@ Now that the Okta app is configured, it's time to enable it in GitLab.
1. Add the provider configuration.
>**Notes:**
+ >
>- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
installation to generate the correct value).
+ >
>- To get the `idp_cert_fingerprint` fingerprint, first download the
certificate from the Okta app you registered and then run:
`openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert`
with the location of your certificate.
+ >
>- Change the value of `idp_sso_target_url`, with the value of the
**Identity Provider Single Sign-On URL** from the step when you
configured the Okta app.
- >- Change the value of `issuer` to a unique name, which will identify the application
+ >
+ >- Change the value of `issuer` to the value of the **Audience Restriction** from your Okta app configuration. This will identify GitLab
to the IdP.
+ >
>- Leave `name_identifier_format` as-is.
**For Omnibus GitLab installations**
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/administration/job_traces.md b/doc/administration/job_traces.md
index 63945506f3c..aa9d87562a3 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -10,13 +10,13 @@ In the following table you can see the phases a trace goes through.
| Phase | State | Condition | Data flow | Stored path |
| ----- | ----- | --------- | --------- | ----------- |
-| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
-| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
-| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
+| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
+| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
+| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/gitlab-rails/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
| 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log`|
The `ROOT_PATH` varies per your environment. For Omnibus GitLab it
-would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source
+would be `/var/opt/gitlab`, whereas for installations from source
it would be `/home/git/gitlab`.
## Changing the job traces local location
diff --git a/doc/api/commits.md b/doc/api/commits.md
index c8c282a71d9..92f53c7b5e6 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -130,6 +130,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Cont
```
Example response:
+
```json
{
"id": "ed899a2f4b50b4370feeea94676502b42383c746",
@@ -155,28 +156,28 @@ Example response:
}
```
-GitLab supports [form encoding](../README.md#encoding-api-parameters-of-array-and-hash-types). The following is an example using Commit API with form encoding:
+GitLab supports [form encoding](README.md#encoding-api-parameters-of-array-and-hash-types). The following is an example using Commit API with form encoding:
```bash
curl --request POST \
--form "branch=master" \
--form "commit_message=some commit message" \
--form "start_branch=master" \
- --form "actions[][action]=create" \
- --form "actions[][file_path]=foo/bar" \
- --form "actions[][content]=</path/to/local.file" \
- --form "actions[][action]=delete" \
- --form "actions[][file_path]=foo/bar2" \
- --form "actions[][action]=move" \
- --form "actions[][file_path]=foo/bar3" \
- --form "actions[][previous_path]=foo/bar4" \
- --form "actions[][content]=</path/to/local1.file" \
- --form "actions[][action]=update" \
+ --form "actions[][action]=create" \
+ --form "actions[][file_path]=foo/bar" \
+ --form "actions[][content]=</path/to/local.file" \
+ --form "actions[][action]=delete" \
+ --form "actions[][file_path]=foo/bar2" \
+ --form "actions[][action]=move" \
+ --form "actions[][file_path]=foo/bar3" \
+ --form "actions[][previous_path]=foo/bar4" \
+ --form "actions[][content]=</path/to/local1.file" \
+ --form "actions[][action]=update" \
--form "actions[][file_path]=foo/bar5" \
- --form "actions[][content]=</path/to/local2.file" \
- --form "actions[][action]=chmod" \
+ --form "actions[][content]=</path/to/local2.file" \
+ --form "actions[][action]=chmod" \
--form "actions[][file_path]=foo/bar5" \
- --form "actions[][execute_filemode]=true" \
+ --form "actions[][execute_filemode]=true" \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/1/repository/commits"
```
diff --git a/doc/api/environments.md b/doc/api/environments.md
index 4a38dd73747..ebcdc546d08 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -29,6 +29,111 @@ Example response:
]
```
+## Get a specific environment
+
+```
+GET /projects/:id/environments/:environment_id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `environment_id` | integer | yes | The ID of the environment |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1"
+```
+
+Example of response
+
+```json
+{
+ "id": 1,
+ "name": "review/fix-foo",
+ "slug": "review-fix-foo-dfjre3",
+ "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"
+ "last_deployment": {
+ "id": 100,
+ "iid": 34,
+ "ref": "fdroid",
+ "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab",
+ "created_at": "2019-03-25T18:55:13.252Z",
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "state": "active",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ }
+ "deployable": {
+ "id": 710,
+ "status": "success",
+ "stage": "deploy",
+ "name": "staging",
+ "ref": "fdroid",
+ "tag": false,
+ "coverage": null,
+ "created_at": "2019-03-25T18:55:13.215Z",
+ "started_at": "2019-03-25T12:54:50.082Z",
+ "finished_at": "2019-03-25T18:55:13.216Z",
+ "duration": 21623.13423,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.dev/root",
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "bio": null,
+ "location": null,
+ "public_email": "",
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "organization": null
+ }
+ "commit": {
+ "id": "416d8ea11849050d3d1f5104cf8cf51053e790ab",
+ "short_id": "416d8ea1",
+ "created_at": "2016-01-02T15:39:18.000Z",
+ "parent_ids": [
+ "e9a4449c95c64358840902508fc827f1a2eab7df"
+ ],
+ "title": "Removed fabric to fix #40",
+ "message": "Removed fabric to fix #40\n",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "authored_date": "2016-01-02T15:39:18.000Z",
+ "committer_name": "Administrator",
+ "committer_email": "admin@example.com",
+ "committed_date": "2016-01-02T15:39:18.000Z"
+ },
+ "pipeline": {
+ "id": 34,
+ "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab",
+ "ref": "fdroid",
+ "status": "success",
+ "web_url": "http://localhost:3000/Commit451/lab-coat/pipelines/34"
+ },
+ "web_url": "http://localhost:3000/Commit451/lab-coat/-/jobs/710",
+ "artifacts": [
+ {
+ "file_type": "trace",
+ "size": 1305,
+ "filename": "job.log",
+ "file_format": null
+ }
+ ],
+ "runner": null,
+ "artifacts_expire_at": null
+ }
+ }
+}
+```
+
## Create a new environment
Creates a new environment with the given name and external_url.
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 43bbf463c8d..1a4310ef328 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -93,6 +93,36 @@ Example of response
}
```
+### Get variables of a pipeline
+
+```
+GET /projects/:id/pipelines/:pipeline_id/variables
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/variables"
+```
+
+Example of response
+
+```json
+[
+ {
+ "key": "RUN_NIGHTLY_BUILD",
+ "value": "true"
+ },
+ {
+ "key": "foo",
+ "value": "bar"
+ }
+]
+```
+
## Create a new pipeline
> [Introduced][ce-7209] in GitLab 8.14
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index 02334f0298e..f36e352da67 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -159,7 +159,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the project owned by the authenticated user |
| `name` | String | yes | The name of the cluster |
-| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base_domain) of the cluster |
+| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster |
| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true |
| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API |
| `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes |
@@ -250,7 +250,7 @@ Parameters:
| `id` | integer | yes | The ID of the project owned by the authenticated user |
| `cluster_id` | integer | yes | The ID of the cluster |
| `name` | String | no | The name of the cluster |
-| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base_domain) of the cluster |
+| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster |
| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API |
| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes |
| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 7d7215e6b80..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.
@@ -256,6 +279,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## List runner's jobs
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15432) in GitLab 10.3.
+
List jobs that are being processed or were processed by specified Runner.
```
@@ -454,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 |
@@ -464,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:
@@ -492,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:
@@ -514,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/README.md b/doc/ci/README.md
index 913e9d4d789..a7ad2018b09 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -62,6 +62,7 @@ into more features:
| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes in a per-branch basis. |
+| [Optimizing GitLab for large repositories](large_repositories/index.md) | Useful tips on how to optimize GitLab and GitLab Runner for big repositories. |
| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. |
| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. |
diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md
index a06fe6961a7..5ecdf0f8c54 100644
--- a/doc/ci/chatops/README.md
+++ b/doc/ci/chatops/README.md
@@ -24,7 +24,7 @@ Since ChatOps is built upon GitLab CI/CD, the job has all the same features and
* It is strongly recommended to set `only: [chat]` so the job does not run as part of the standard CI pipeline.
* If the job is set to `when: manual`, the pipeline will be created however the job will wait to be started.
-* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](https://docs.gitlab.com/ce/ci/variables/README.html#priority-of-variables).
+* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](../variables/README.html#priority-of-environment-variables).
### Controlling the ChatOps reply
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 9266c4511be..5222cc45bc4 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -303,20 +303,19 @@ services:
- docker:dind
variables:
- CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
before_script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
script:
- - docker pull $CONTAINER_IMAGE:latest || true
- - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_COMMIT_SHA --tag $CONTAINER_IMAGE:latest .
- - docker push $CONTAINER_IMAGE:$CI_COMMIT_SHA
- - docker push $CONTAINER_IMAGE:latest
+ - docker pull $CI_REGISTRY_IMAGE:latest || true
+ - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
+ - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
+ - docker push $CI_REGISTRY_IMAGE:latest
```
The steps in the `script` section for the `build` stage can be summed up to:
@@ -324,7 +323,7 @@ The steps in the `script` section for the `build` stage can be summed up to:
1. The first command tries to pull the image from the registry so that it can be
used as a cache for the `docker build` command.
1. The second command builds a Docker image using the pulled image as a
- cache (notice the `--cache-from $CONTAINER_IMAGE:latest` argument) if
+ cache (notice the `--cache-from $CI_REGISTRY_IMAGE:latest` argument) if
available, and tags it.
1. The last two commands push the tagged Docker images to the container registry
so that they may also be used as cache for subsequent builds.
@@ -421,14 +420,14 @@ and depend on the visibility of your project.
For all projects, mostly suitable for public ones:
-- **Using the special `gitlab-ci-token` user**: This user is created for you in order to
+- **Using the special `$CI_REGISTRY_USER` variable**: The user specified by this variable is created for you in order to
push to the Registry connected to your project. Its password is automatically
- set with the `$CI_JOB_TOKEN` variable. This allows you to automate building and deploying
+ set with the `$CI_REGISTRY_PASSWORD` variable. This allows you to automate building and deploying
your Docker images and has read/write access to the Registry. This is ephemeral,
so it's only valid for one job. You can use the following example as-is:
```sh
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
```
For private and internal projects:
@@ -436,8 +435,10 @@ For private and internal projects:
- **Using a personal access token**: You can create and use a
[personal access token](../../user/profile/personal_access_tokens.md)
in case your project is private:
- - For read (pull) access, the scope should be `read_registry`.
- - For read/write (pull/push) access, use `api`.
+
+ - For read (pull) access, the scope should be `read_registry`.
+ - For read/write (pull/push) access, use `api`.
+
Replace the `<username>` and `<access_token>` in the following example:
```sh
@@ -469,9 +470,9 @@ could look like:
DOCKER_DRIVER: overlay2
stage: build
script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
- - docker build -t registry.example.com/group/project/image:latest .
- - docker push registry.example.com/group/project/image:latest
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - docker build -t $CI_REGISTRY/group/project/image:latest .
+ - docker push $CI_REGISTRY/group/project/image:latest
```
You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
@@ -486,7 +487,7 @@ variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
before_script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
@@ -526,7 +527,7 @@ variables:
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
before_script:
- - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 3314c76d234..13c26bc5f47 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -500,7 +500,7 @@ To configure access for `registry.example.com:5000`, follow these steps:
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
```
-1. Create a [variable] `DOCKER_AUTH_CONFIG` with the content of the
+1. Create a [variable](../variables/README.md#gitlab-cicd-environment-variables) `DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
```json
@@ -642,7 +642,6 @@ creation.
[postgres-hub]: https://hub.docker.com/r/_/postgres/
[mysql-hub]: https://hub.docker.com/r/_/mysql/
[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
-[variable]: ../variables/README.md#variables
[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint
[cmd]: https://docs.docker.com/engine/reference/builder/#cmd
[register]: https://docs.gitlab.com/runner/register/
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 54493bc2922..3e52cc786dd 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -224,7 +224,7 @@ The `name` and `url` parameters for dynamic environments can use most available
including:
- [Predefined environment variables](variables/README.md#predefined-environment-variables)
-- [Project and group variables](variables/README.md#variables)
+- [Project and group variables](variables/README.md#gitlab-cicd-environment-variables)
- [`.gitlab-ci.yml` variables](yaml/README.md#variables)
However, you cannot use variables defined:
@@ -664,7 +664,7 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
### Scoping environments with specs **[PREMIUM]**
Some GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) features can behave differently for each
-environment. For example, you can [create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-variables-premium).
+environment. For example, you can [create a secret variable to be injected only into a production environment](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium).
In most cases, these features use the _environment specs_ mechanism, which offers
an efficient way to implement scoping within each environment group.
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index 908cf85980e..d6ad00a77da 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -511,7 +511,7 @@ Errors can be easily debugged through GitLab's build logs, and within minutes of
you can see the changes live on your game.
Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables
-rapid but stable development. We can easily test changes in a separate [environment](../../../ci/environments.md#introduction-to-environments-and-deployments),
+rapid but stable development. We can easily test changes in a separate [environment](../../environments.md),
or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing
and tedious, but having faith in a stable deployment with GitLab CI/CD allows
a lot of breathing room in quickly getting changes to players.
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 0f809ed05ca..f56d5429fb7 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -116,7 +116,7 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/id_rsa
```
-Now, let's add it to your GitLab project as a [variable](../../variables/README.md#variables).
+Now, let's add it to your GitLab project as a [variable](../../variables/README.md#gitlab-cicd-environment-variables).
Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
They can be added per project by navigating to the project's **Settings** > **CI/CD**.
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index fa18cb22aed..e1164b8d03a 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -69,5 +69,5 @@ in the `.gitlab-ci.yml` file with your application's name.
## Heroku API key
You can look up your Heroku API key in your
-[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-variables) with
+[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-environment-variables) with
this value in **Project ➔ Variables** with key `HEROKU_API_KEY`.
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index d505f2ae4ce..6055d8c282a 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -127,7 +127,7 @@ displayed by GitLab:
![pipeline status](img/pipeline_status.png)
At the end, if anything goes wrong, you can easily
-[roll back](../environments.md#rolling-back-changes) all the changes:
+[roll back](../environments.md#retrying-and-rolling-back) all the changes:
![rollback button](img/rollback.png)
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
new file mode 100644
index 00000000000..cfe638c0a22
--- /dev/null
+++ b/doc/ci/large_repositories/index.md
@@ -0,0 +1,235 @@
+# Optimizing GitLab for large repositories
+
+Large repositories consisting of more than 50k files in a worktree
+often require special consideration because of
+the time required to clone and check out.
+
+GitLab and GitLab Runner handle this scenario well
+but require optimized configuration to efficiently perform its
+set of operations.
+
+The general guidelines for handling big repositories are simple.
+Each guideline is described in more detail in the sections below:
+
+- Always fetch incrementally. Do not clone in a way that results in recreating all of the worktree.
+- Always use shallow clone to reduce data transfer. Be aware that this puts more burden
+ on GitLab instance due to higher CPU impact.
+- Control the clone directory if you heavily use a fork-based workflow.
+- Optimize `git clean` flags to ensure that you remove or keep data that might affect or speed-up your build.
+
+## Shallow cloning
+
+> Introduced in GitLab Runner 8.9.
+
+GitLab and GitLab Runner always perform a full clone by default.
+While it means that all changes from GitLab are received,
+it often results in receiving extra commit logs.
+
+Ideally, you should always use `GIT_DEPTH` with a small number
+like 10. This will instruct GitLab Runner to perform shallow clones.
+Shallow clones makes Git request only the latest set of changes for a given branch,
+up to desired number of commits as defined by the `GIT_DEPTH` variable.
+
+This significantly speeds up fetching of changes from Git repositories,
+especially if the repository has a very long backlog consisting of number
+of big files as we effectively reduce amount of data transfer.
+
+The following example makes GitLab Runner shallow clone to fetch only a given branch,
+it does not fetch any other branches nor tags.
+
+```yaml
+variables:
+ GIT_DEPTH: 10
+
+test:
+ script:
+ - ls -al
+```
+
+## Git strategy
+
+> Introduced in GitLab Runner 8.9.
+
+By default, GitLab is configured to always prefer the `GIT_STRATEGY: fetch` strategy.
+The `GIT_STRATEGY: fetch` strategy will re-use existing worktrees if found
+on disk. This is different to the `GIT_STRATEGY: clone` strategy
+as in case of clones, if a worktree is found, it is removed before clone.
+
+Usage of `fetch` is preferred because it reduces the amount of data to transfer and
+does not really impact the operations that you might do on a repository from CI.
+
+However, `fetch` does require access to the previous worktree. This works
+well when using the `shell` or `docker` executor because these
+try to preserve worktrees and try to re-use them by default.
+
+This does not work today for `kubernetes` executor and has limitations when using
+`docker+machine`. `kubernetes` executor today always clones into ephemeral directory.
+
+GitLab also offers the `GIT_STRATEGY: none` strategy. This disables any `fetch` and `checkout` commands
+done by GitLab, requiring you to do them.
+
+## Git clone path
+
+> Introduced in GitLab Runner 11.10.
+
+`GIT_CLONE_PATH` allows you to control where you clone your sources.
+This can have implications if you heavily use big repositories with fork workflow.
+
+Fork workflow from GitLab Runner's perspective is stored as a separate repository
+with separate worktree. That means that GitLab Runner cannot optimize the usage
+of worktrees and you might have to instruct GitLab Runner to use that.
+
+In such cases, ideally you want to make the GitLab Runner executor be used only used only
+for the given project and not shared across different projects to make this
+process more efficient.
+
+The `GIT_CLONE_PATH` has to be within the `$CI_BUILDS_DIR`. Currently,
+it is impossible to pick any path from disk.
+
+## Git clean flags
+
+> Introduced in GitLab Runner 11.10.
+
+`GIT_CLEAN_FLAGS` allows you to control whether or not you require
+the `git clean` command to be executed for each CI job.
+By default, GitLab ensures that you have your worktree on the given SHA,
+and that your repository is clean.
+
+`GIT_CLEAN_FLAGS` is disabled when set to `none`. On very big repositories, this
+might be desired because `git clean` is disk I/O intensive. Controlling that
+with `GIT_CLEAN_FLAGS: -ffdx -e .build/`, for example, allows you to control and
+disable removal of some directories within the worktree between subsequent runs,
+which can speed-up the incremental builds. This has the biggest effect
+if you re-use existing machines, and have an existing worktree that you can re-use
+for builds.
+
+For exact parameters accepted by `GIT_CLEAN_FLAGS`, see the documentation
+for [git clean](https://git-scm.com/docs/git-clean). The
+available parameters are dependent on Git version.
+
+## Fork-based workflow
+
+> Introduced in GitLab Runner 11.10.
+
+Following the guidelines above, lets imagine that we want to:
+
+- Optimize for a big project (more than 50k files in directory).
+- Use forks-based workflow for contributing.
+- Reuse existing worktrees. Have preconfigured runners that are pre-cloned with repositories.
+- Runner assigned only to project and all forks.
+
+Lets consider the following two examples, one using `shell` executor and
+other using `docker` executor.
+
+### `shell` executor example
+
+Lets assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+
+```toml
+concurrent = 4
+
+[[runners]]
+ url = "GITLAB_URL"
+ token = "TOKEN"
+ executor = "shell"
+ builds_dir = "/builds"
+ cache_dir = "/cache"
+
+ [runners.custom_build_dir]
+ enabled = true
+```
+
+This `config.toml`:
+
+- Uses the `shell` executor,
+- Specifies a custom `/builds` directory where all clones will be stored.
+- Enables the ability to specify `GIT_CLONE_PATH`,
+- Runs at most 4 jobs at once.
+
+### `docker` executor example
+
+Lets assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+
+```toml
+concurrent = 4
+
+[[runners]]
+ url = "GITLAB_URL"
+ token = "TOKEN"
+ executor = "docker"
+ builds_dir = "/builds"
+ cache_dir = "/cache"
+
+ [runners.docker]
+ volumes = ["/builds:/builds", "/cache:/cache"]
+```
+
+This `config.toml`:
+
+- Uses the `docker` executor,
+- Specifies a custom `/builds` directory on disk where all clones will be stored.
+ We host mount the `/builds` directory to make it reusable between subsequent runs
+ and be allowed to override the cloning strategy.
+- Doesn't enable the ability to specify `GIT_CLONE_PATH` as it is enabled by default.
+- Runs at most 4 jobs at once.
+
+### Our `.gitlab-ci.yml`
+
+Once we have the executor configured, we need to fine tune our `.gitlab-ci.yml`.
+
+Our pipeline will be most performant if we use the following `.gitlab-ci.yml`:
+
+```yaml
+variables:
+ GIT_DEPTH: 10
+ GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME
+
+build:
+ script: ls -al
+```
+
+The above configures a:
+
+- Shallow clone of 10, to speed up subsequent `git fetch` commands.
+- Custom clone path to make it possible to re-use worktrees between parent project and all forks
+ because we use the same clone path for all forks.
+
+Why use `$CI_CONCURRENT_ID`? The main reason is to ensure that worktrees used are not conflicting
+between projects. The `$CI_CONCURRENT_ID` represents a unique identifier within the given executor,
+so as long as we use it to construct the path, it is guaranteed that this directory will not conflict
+with other concurrent jobs running.
+
+### Store custom clone options in `config.toml`
+
+Ideally, all job-related configuration should be stored in `.gitlab-ci.yml`.
+However, sometimes it is desirable to make these schemes part of Runner configuration.
+
+In the above example of Forks, making this configuration discoverable for users may be preferred,
+but this brings administrative overhead as the `.gitlab-ci.yml` needs to be updated for each branch.
+In such cases, it might be desirable to keep the `.gitlab-ci.yml` clone path agnostic, but make it
+a configuration of Runner.
+
+We can extend our [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
+with the following specification that will be used by Runner if `.gitlab-ci.yml` will not override it:
+
+```toml
+concurrent = 4
+
+[[runners]]
+ url = "GITLAB_URL"
+ token = "TOKEN"
+ executor = "docker"
+ builds_dir = "/builds"
+ cache_dir = "/cache"
+
+ environment = [
+ "GIT_DEPTH=10",
+ "GIT_CLONE_PATH=$CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME"
+ ]
+
+ [runners.docker]
+ volumes = ["/builds:/builds", "/cache:/cache"]
+```
+
+This makes the cloning configuration to be part of given Runner,
+and does not require us to update each `.gitlab-ci.yml`.
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/pipelines.md b/doc/ci/pipelines.md
index 38cd58f11ac..2ffa3d4edc7 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -295,7 +295,7 @@ You can do this straight from the pipeline graph. Just click on the play button
to execute that particular job.
For example, your pipeline start automatically, but require manual action to
-[deploy to production](environments.md#manually-deploying-to-environments). In the example below, the `production`
+[deploy to production](environments.md#configuring-manual-deployments). In the example below, the `production`
stage has a job with a manual action.
![Pipelines example](img/pipelines.png)
@@ -313,7 +313,7 @@ For example, if you start rolling out new code and:
- Users do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%.
- Users experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline
- and [rolling](environments.md#rolling-back-changes) back to the last stable version.
+ and [rolling](environments.md#retrying-and-rolling-back) back to the last stable version.
![Pipelines example](img/pipeline_incremental_rollout.png)
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 9dbcf9d1155..2fc1e14f02e 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -33,7 +33,7 @@ In this example, you can see a branch was:
## How do Review Apps work?
-The basis of Review Apps in GitLab is [dynamic environments](../environments.md#dynamic-environments), which allow you to dynamically create a new environment for each branch.
+The basis of Review Apps in GitLab is [dynamic environments](../environments.md#configuring-dynamic-environments), which allow you to dynamically create a new environment for each branch.
Access to the Review App is made available as a link on the [merge request](../../user/project/merge_requests.md) relevant to the branch. Review Apps enable you to review all changes proposed by the merge request in live environment.
@@ -60,14 +60,14 @@ To get a better understanding of Review Apps, review documentation on how enviro
1. Learn about [environments](../environments.md) and their role in the development workflow.
1. Learn about [CI variables](../variables/README.md) and how they can be used in your CI jobs.
1. Explore the [`environment` syntax](../yaml/README.md#environment) as defined in `.gitlab-ci.yml`. This will become a primary reference.
-1. Additionally, find out about [manual actions](../environments.md#manually-deploying-to-environments) and how you can use them to deploy to critical environments like production with the push of a button.
+1. Additionally, find out about [manual actions](../environments.md#configuring-manual-deployments) and how you can use them to deploy to critical environments like production with the push of a button.
1. Follow the [example tutorials](#examples). These will guide you through setting up infrastructure and using Review Apps.
### Configuring dynamic environments
Configuring Review Apps dynamic environments depends on your technology stack and infrastructure.
-For more information, see [dynamic environments](../environments.md#dynamic-environments) documentation to understand how to define and create them.
+For more information, see [dynamic environments](../environments.md#configuring-dynamic-environments) documentation to understand how to define and create them.
### Creating and destroying Review Apps
@@ -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/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index ff63d0bd69f..9ed1ec5aa5c 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy.
**Do not** add a passphrase to the SSH key, or the `before_script` will
prompt for it.
-1. Create a new [variable](../variables/README.md#variables).
+1. Create a new [variable](../variables/README.md#gitlab-cicd-environment-variables).
As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste
the content of your _private_ key that you created earlier.
@@ -157,7 +157,7 @@ ssh-keyscan example.com
ssh-keyscan 1.2.3.4
```
-Create a new [variable](../variables/README.md#variables) with
+Create a new [variable](../variables/README.md#gitlab-cicd-environment-variables) with
`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`.
NOTE: **Note:**
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 398b017277f..0e72bdddee7 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -20,7 +20,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new
DANGER: **Danger:**
Passing plain text tokens in public projects is a security issue. Potential
attackers can impersonate the user that exposed their trigger token publicly in
-their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#variables)
+their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#gitlab-cicd-environment-variables)
to protect trigger tokens.
## Adding a new trigger
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 6532d177d2c..61d1a904f76 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -3,7 +3,6 @@ table_display_block: true
---
# GitLab CI/CD environment variables
-{: #variables}
After a brief overview over the use of environment
variables, this document teaches you how to use GitLab CI/CD's
@@ -65,7 +64,7 @@ To get started, choose one of the existing
to be output by the Runner. For example, let's say that you want
a given job you're running through your script to output the
stage that job is running for. In your `.gitlab-ci.yml` file,
-call the variable from your script according to the [syntaxes](#syntax-of-variables-in-job-scripts) available. To
+call the variable from your script according to the [syntaxes](#syntax-of-environment-variables-in-job-scripts) available. To
output the job stage, use the predefined variable `CI_JOB_STAGE`:
```yaml
@@ -145,7 +144,6 @@ settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
Follow the discussion in issue [#13784][ce-13784] for masking the variables.
### Syntax of environment variables in job scripts
-{: #syntax-of-variables-in-job-scripts}
All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables.
@@ -278,7 +276,6 @@ script:
```
### Group-level environment variables
-{: #group-level-variables}
> Introduced in GitLab 9.4.
@@ -297,19 +294,18 @@ Any variables of [subgroups](../../user/group/subgroups/index.md) will be inheri
Once you set them, they will be available for all subsequent pipelines.
## Priority of environment variables
-{: #priority-of-variables}
Variables of different types can take precedence over other
variables, depending on where they are defined.
The order of precedence for variables is (from highest to lowest):
-1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables).
-1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-variables).
-1. Group-level [variables](#group-level-variables) or [protected variables](#protected-variables).
+1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#using-variables).
+1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-environment-variables).
+1. Group-level [variables](#group-level-environment-variables) or [protected variables](#protected-environment-variables).
1. YAML-defined [job-level variables](../yaml/README.md#variables).
1. YAML-defined [global variables](../yaml/README.md#variables).
-1. [Deployment variables](#deployment-variables).
+1. [Deployment variables](#deployment-environment-variables).
1. [Predefined environment variables](predefined_variables.md).
For example, if you define:
@@ -329,7 +325,6 @@ about which variables are [not supported](where_variables_can_be_used.md).
## Advanced use
### Protected environment variables
-{: #protected-variables}
> Introduced in GitLab 9.3.
@@ -345,7 +340,6 @@ Protected variables can be added by going to your project's
Once you set them, they will be available for all subsequent pipelines.
### Deployment environment variables
-{: #deployment-variables}
> Introduced in GitLab 8.15.
@@ -382,7 +376,7 @@ limitations with the current Auto DevOps scripting environment.
[Manually triggered pipelines](../pipelines.md#manually-executing-pipelines) allow you to override the value of a current variable.
For instance, suppose you added a
-[custom variable `$TEST`](#creating-a-custom-variable)
+[custom variable `$TEST`](#creating-a-custom-environment-variable)
as exemplified above and you want to override it in a manual pipeline.
Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**.
Choose the branch you want to run the pipeline for, then add a new variable
@@ -396,7 +390,6 @@ value you set for this specific pipeline:
![Manually overridden variable output](img/override_value_via_manual_pipeline_output.png)
## Environment variables expressions
-{: #variables-expressions}
> Introduced in GitLab 10.7.
@@ -546,6 +539,8 @@ $'\''git'\'' "checkout" "-f" "-q" "dd648b2e48ce6518303b0bb580b2ee32fadaf045"
Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb...
++ export CI=true
++ CI=true
+++ export CI_API_V4_API_URL=https://example.com:3000/api/v4
+++ CI_API_V4_API_URL=https://example.com:3000/api/v4
++ export CI_DEBUG_TRACE=false
++ CI_DEBUG_TRACE=false
++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
@@ -584,6 +579,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ GITLAB_CI=true
++ export CI=true
++ CI=true
+++ export CI_API_V4_API_URL=https://example.com:3000/api/v4
+++ CI_API_V4_API_URL=https://example.com:3000/api/v4
++ export GITLAB_CI=true
++ GITLAB_CI=true
++ export CI_JOB_ID=7046507
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 416627740d2..846c539daab 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. |
@@ -33,7 +36,7 @@ future GitLab releases.**
| `CI_COMMIT_TAG` | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| `CI_COMMIT_TITLE` | 10.8 | all | The title of the commit - the full first line of the message |
| `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
-| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
+| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug tracing](README.md#debug-tracing) is enabled |
| `CI_DEPLOY_PASSWORD` | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| `CI_DEPLOY_USER` | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
| `CI_DISPOSABLE_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index ceca4af1bee..091ddcb0bae 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) |
@@ -111,4 +111,4 @@ They are:
- Script execution shell.
- Not supported:
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
- - In the `only` and `except` [variables expressions](README.md#variables-expressions).
+ - In the `only` and `except` [variables expressions](README.md#environment-variables-expressions).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5690729c370..5e44de13b51 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -319,7 +319,7 @@ There are a few rules that apply to the usage of job policy:
- `only` and `except` are inclusive. If both `only` and `except` are defined
in a job specification, the ref is filtered by `only` and `except`.
-- `only` and `except` allow the use of regular expressions (using [Ruby regexp syntax](https://ruby-doc.org/core/Regexp.html)).
+- `only` and `except` allow the use of regular expressions ([supported regexp syntax](#supported-onlyexcept-regexp-syntax)).
- `only` and `except` allow to specify a repository path to filter jobs for
forks.
@@ -391,6 +391,11 @@ job:
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except `master` and those with names prefixed with `release/`.
+NOTE: **Note:**
+Because `@` is used to denote the beginning of a ref's repository path,
+matching a ref name containing the `@` character in a regular expression
+requires the use of the hex character code match `\x40`.
+
If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by
default. If it doesn't have an `except` rule, it is empty.
@@ -409,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.
@@ -492,11 +518,11 @@ end-to-end:
- $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
```
-Learn more about [variables expressions](../variables/README.md#variables-expressions).
+Learn more about [variables expressions](../variables/README.md#environment-variables-expressions).
#### `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:
@@ -513,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.
@@ -706,7 +756,7 @@ Manual actions are a special type of job that are not executed automatically,
they need to be explicitly started by a user. An example usage of manual actions
would be a deployment to a production environment. Manual actions can be started
from the pipeline, job, environment, and deployment views. Read more at the
-[environments documentation](../environments.md#manually-deploying-to-environments).
+[environments documentation](../environments.md#configuring-manual-deployments).
Manual actions can be either optional or blocking. Blocking manual actions will
block the execution of the pipeline at the stage this action is defined in. It's
@@ -2199,7 +2249,7 @@ Runner itself](../variables/README.md#predefined-environment-variables).
One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called
-[Variables](../variables/README.md#variables)
+[Variables](../variables/README.md#gitlab-cicd-environment-variables)
which can be set in GitLab's UI.
Learn more about [variables and their priority][variables].
@@ -2312,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
@@ -2389,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/documentation/index.md b/doc/development/documentation/index.md
index a4da34a50ce..4a2ff64807e 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -48,29 +48,21 @@ as its markdown rendering engine. See the [GitLab Markdown Guide](https://about.
Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is missing, you are welcome to suggest one via a merge request.
-## Documentation directory structure
+## Documentation types and organization
The documentation is structured based on the GitLab UI structure itself,
separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
-In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
-all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
-
-The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
-been **deprecated** and the majority their docs have been moved to their correct location
-in small iterations. Please do not create new docs in these folders. Organize docs by product area and subject, not type.
+Organize docs by product area and subject, not type. For example, do not create groupings of similar media types
+(e.g. indexes of all articles, videos, etc.).
-### Documentation files
-
-- When you create a new directory, always start with an `index.md` file.
- Do not use another file name and **do not** create `README.md` files.
-- **Do not** use special chars and spaces, or capital letters in file names,
- directory names, branch names, and anything that generates a path.
-- Max screenshot size: 100KB.
-- We do not support videos (yet).
+Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes
+it difficult to browse for the information you need and difficult to maintain up-to-date content.
+Instead, organize content by its subject (e.g. everything related to CI goes together)
+and cross-link between any related content.
-### Location and naming documents
+### Location and naming of files
Our goal is to have a clear hierarchical structure with meaningful URLs
like `docs.gitlab.com/user/project/merge_requests/`. With this pattern,
@@ -78,16 +70,8 @@ you can immediately tell that you are navigating to user-related documentation
about project features; specifically about merge requests. Our site's paths match
those of our repository, so the clear structure also makes documentation easier to update.
-While the documentation is home to a variety of content types, we do not organize by content type.
-For example, do not create groupings of similar media types (e.g. indexes of all articles, videos, etc.).
-Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes
-it difficult to browse for the information you need and difficult to maintain up-to-date content.
-Instead, organize content by its subject (e.g. everything related to CI goes together)
-and cross-link between any related content.
-
-Do not simply link out to GitLab technical blog posts. There should be an up-to-date
-single source of truth on the topic within the documentation, and the top of the
-blog post should be updated to link to that doc.
+In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
+all docs should be linked at least from its higher-level index page if not also from other relevant locations.
The table below shows what kind of documentation goes where.
@@ -102,13 +86,18 @@ The table below shows what kind of documentation goes where.
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
-**General rules & best practices:**
+**Rules and best practices:**
+1. When you create a new directory, always start with an `index.md` file.
+ Do not use another file name and **do not** create `README.md` files.
+1. **Do not** use special chars and spaces, or capital letters in file names,
+ directory names, branch names, and anything that generates a path.
1. When creating a new document and it has more than one word in its name,
make sure to use underscores instead of spaces or dashes (`-`). For example,
a proper naming would be `import_projects_from_github.md`. The same rule
applies to images.
-1. Start a new directory with an `index.md` file.
+1. For image files, do not exceed 100KB.
+1. We do not yet support embedded videos. Please link out.
1. There are four main directories, `user`, `administration`, `api` and `development`.
1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
`profile/`, `dashboard/` and `admin_area/`.
@@ -129,6 +118,9 @@ The table below shows what kind of documentation goes where.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
General user- and admin- related documentation, should be placed accordingly.
+1. The directories `/workflow/`, `/university/`, and `/articles/` have
+been **deprecated** and the majority their docs have been moved to their correct location
+in small iterations.
If you are unsure where a document or a content addition should live, this should
not stop you from authoring and contributing. You can use your best judgment and
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index c0386290785..1cefe48b4c1 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -14,9 +14,8 @@ For programmatic help adhering to the guidelines, see [linting](index.md#linting
## Files
-- [Directory structure](index.md#location-and-naming-documents): place the docs
- in the correct location.
-- [Documentation files](index.md#documentation-files): name the files accordingly.
+See the [Documentation types and organization](index.md#documentation-types-and-organization) section
+on the index page for information on how to structure and name files across the GitLab documentation.
DANGER: **Attention:**
**Do not** use capital letters, spaces, or special chars in file names,
@@ -28,41 +27,43 @@ a test that will fail if it spots a new `README.md` file.
### Markdown
-The [documentation website](https://docs.gitlab.com) had its markdown engine migrated from [Redcarpet to GitLab Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108)
-in October 2018.
+The [documentation website](https://docs.gitlab.com) uses GitLab Kramdown as its Markdown rendering engine [as of October 2018](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108). For a complete Kramdown reference, see the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown)
gem will support all [GFM markup](../../user/markdown.md) in the future. For now,
-use regular markdown markup, following the rules on this style guide. For a complete
-Kramdown reference, check the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
-Use Kramdown markup wisely: do not overuse its specific markup (e.g., `{:.class}`) as it will not render properly in
-[`/help`](index.md#gitlab-help).
+use regular markdown markup, following the rules in the linked style guide.
+
+Note that Kramdown-specific markup (e.g., `{:.class}`) will not render properly in on GitLab instances in [`/help`](index.md#gitlab-help).
## Content
These guidelines help toward the goal of having every user's search of documentation
yield a useful result, and ensuring content is helpful and easy to consume.
-- What to include:
- - Any and all helpful information, processes, and tips for implementing,
- using, and troubleshooting GitLab features. [The documentation is the single source of truth](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot)
- for this information.
- - 'Risky' or niche problem-solving steps. There is no reason to withhold these or
- store them elsewhere; simply include them along with the rest of the docs including all necessary
- detail, such as specific warnings and caveats about potential ramifications.
- - Any content types/sources, if relevant to users or admins. You can freely
- include presentations, videos, etc.; no matter who it was originally written for,
- if it is helpful to any of our audiences, we can include it. If an outside source
- that's under copyright, rephrase, or summarize and link out; do not copy and paste.
- - All applicable subsections as described on the [structure and template](structure.md) page,
- with files organized in the [correct directory](index.md#documentation-directory-structure).
+### Subject matter
+
+[The documentation is the single source of truth (SSOT)](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) for any and all helpful information and processes needed to learn about, implement, use, and troubleshoot GitLab features. Note that this includes problem-solving actions that may address rare cases or be considered 'risky', so long as proper context is provided. See the SSOT link for more detail.
+
+### Media types and sources
+
+Include any media types/sources, if relevant to readers. You can freely include or link presentations, diagrams, videos, etc.; no matter who it was originally composed for, if it is helpful to any of our audiences, we can include it.
+
+ - If you use an image that has a separate source file (for example, a vector or diagram format), link the image to the source file so that it may be reused or updated by anyone.
+ - Do not copy and paste content from other sources unless it is a limited quotation with the source cited. Typically it is better to either rephrase relevant information in your own words or link out to the other source.
+
+### Structure across documents
+
+- Place files in the correct directory per the [Documentation directory structure](index.md#documentation-types-and-organization) guidelines.
+- To avoid duplication, do not include the same information in multiple places. Instead, choose one 'single source of truth (SSOT)' location and link from other relevant locations.
+- When referencing other GitLab products and features, link to their respective docs.
+- When referencing third-party products or technologies, link out to their external sites, documentation, and resources.
+
+### Structure within documents
+
+- Include any and all applicable subsections as described on the [structure and template](structure.md) page,
- To ensure discoverability, link to each doc from its higher-level index page and other related pages.
-- When referencing other GitLab products and features, link to their
- respective docs; when referencing third-party products or technologies,
- link out to their external sites, documentation, and resources.
-- Do not duplicate information.
- Structure content in alphabetical order in tables, lists, etc., unless there is
- a logical reason not to (for example, when mirroring the UI or an ordered sequence).
+ a logical reason not to (for example, when mirroring the UI or an otherwise ordered sequence).
## Language
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index c2e05b2d065..c5a1d915be6 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -19,6 +19,11 @@ CE specs should remain untouched as much as possible and extra specs
should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
+You can force Webpack to act as CE by either deleting the `ee/` directory or by
+setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/config/helpers/is_ee_env.js)
+to something that evaluates as `false`. The same works for running tests
+(for example `IS_GITLAB_EE=0 yarn jest`).
+
[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500
## Separation of EE code
@@ -943,7 +948,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 +967,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 +981,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/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index a34d1fcec89..437ce9abc7d 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -38,7 +38,7 @@ _For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
-### A `index.js` file
+### An `index.js` file
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
@@ -46,7 +46,7 @@ of the new feature should be.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
-Don't forget to follow [these steps][page_specific_javascript].
+Be sure to read about [page-specific JavaScript][page_specific_javascript].
### Bootstrapping Gotchas
#### Providing data from HAML to JavaScript
@@ -240,7 +240,7 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
-[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
+[page_specific_javascript]: ./performance.md#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 7b54fa6289c..7d52cac5f7e 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -83,7 +83,7 @@ In this file, we will write the actions that will call the respective mutations:
export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
- export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error);
+ export const receiveUsersError = ({ commit }, error) => commit(types.RECEIVE_USERS_ERROR, error);
export const fetchUsers = ({ state, dispatch }) => {
dispatch('requestUsers');
@@ -175,7 +175,7 @@ Remember that actions only describe that something happened, they don't describe
state.users = data;
state.isLoading = false;
},
- [types.REQUEST_USERS_ERROR](state, error) {
+ [types.RECEIVE_USERS_ERROR](state, error) {
state.isLoading = false;
},
[types.REQUEST_ADD_USER](state, user) {
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index 27b69ba8278..f8cf2a7fdc6 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -192,7 +192,7 @@ GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thom
To use a custom Gitaly repository in CI, for instance if you want your
GitLab fork to always use your own Gitaly fork, set `GITALY_REPO_URL`
-as a [CI environment variable](../ci/variables/README.md#variables).
+as a [CI environment variable](../ci/variables/README.md#gitlab-cicd-environment-variables).
---
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 7262c04d746..e41148360f2 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -216,8 +216,10 @@ project, one project will do for the entire file. This can be achieved by using
reloads or recreates the model, _only_ if needed. That is, when you changed
properties or destroyed the object.
-There is one gotcha; you can't reference a model defined in a `let` block in a
-`set` block.
+Note that you can't reference a model defined in a `let` block in a `set` block.
+
+Also, `set` is not supported in `:js` specs since those don't use transactions
+to clean up database state after each example.
### Time-sensitive tests
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/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 818a03a6f02..6e2a09fc030 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../user/project/issues/index.md#new-issue'
+redirect_to: '../user/project/issues/index.md#issue-actions'
---
-This document was moved to [another location](../user/project/issues/index.md#new-issue).
+This document was moved to [another location](../user/project/issues/index.md#issue-actions).
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/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 574ba961cb0..32c2e4a5c99 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -632,7 +632,7 @@ directory (repositories, uploads).
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for Omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key,
-[CI/CD variables](../ci/variables/README.md#variables), and
+[CI/CD variables](../ci/variables/README.md#gitlab-cicd-environment-variables), and
variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index fd99b06b863..1df492ba82c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -141,7 +141,7 @@ in any of the following places:
NOTE: **Note**
The Auto DevOps base domain variable (`KUBE_INGRESS_BASE_DOMAIN`) follows the same order of precedence
-as other environment [variables](../../ci/variables/README.md#priority-of-variables).
+as other environment [variables](../../ci/variables/README.md#priority-of-environment-variables).
A wildcard DNS A record matching the base domain(s) is required, for example,
given a base domain of `example.com`, you'd need a DNS entry like:
@@ -669,7 +669,7 @@ repo or by specifying a project variable:
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
-- **Project variable** - Create a [project variable](../../ci/variables/README.md#variables)
+- **Project variable** - Create a [project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart.
### Custom Helm chart per environment **[PREMIUM]**
@@ -699,6 +699,21 @@ renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of
the `production` job to turn it into a manual action instead of deploying
automatically.
+### Using components of Auto-DevOps
+
+If you only require a subset of the features offered by Auto-DevOps, you can include
+individual Auto-DevOps jobs into your own `.gitlab-ci.yml`.
+
+For example, to make use of [Auto Build](#auto-build), you can add the following to
+your `.gitlab-ci.yml`:
+
+```yaml
+include:
+ - template: Jobs/Build.gitlab-ci.yml
+```
+
+Consult the [Auto DevOps template] for information on available jobs.
+
### PostgreSQL database support
In order to support applications that require a database,
@@ -755,7 +770,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
TIP: **Tip:**
Set up the replica variables using a
-[project variable](../../ci/variables/README.md#variables)
+[project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
and scale your application by just redeploying it!
CAUTION: **Caution:**
@@ -940,7 +955,7 @@ required to go from `10%` to `100%`, you can jump to whatever job you want.
You can also scale down by running a lower percentage job, just before hitting
`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll
back by redeploying the old version using the
-[rollback button](../../ci/environments.md#rolling-back-changes) in the
+[rollback button](../../ci/environments.md#retrying-and-rolling-back) in the
environment page.
Below, you can see how the pipeline will look if the rollout or staging
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..74735886350 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -58,7 +58,7 @@ By doing so:
## Issues and merge requests within a group
Issues and merge requests are part of projects. For a given group, view all the
-[issues](../project/issues/index.md#issues-per-group) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group,
+[issues](../project/issues/index.md#issues-list) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group,
together in a single list view.
## Create a new group
@@ -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/index.md b/doc/user/index.md
index d408504249e..626246447f3 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -38,8 +38,8 @@ GitLab is a Git-based platform that integrates a great number of essential tools
- Hosting code in repositories with version control.
- Tracking proposals for new implementations, bug reports, and feedback with a
- fully featured [Issue Tracker](project/issues/index.md#issue-tracker).
-- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-board).
+ fully featured [Issue Tracker](project/issues/index.md#issues-list).
+- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-boards).
- Reviewing code in [Merge Requests](project/merge_requests/index.md) with live-preview changes per
branch with [Review Apps](../ci/review_apps/index.md).
- Building, testing, and deploying with built-in [Continuous Integration](../ci/README.md).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 8239742969a..9891a43aa61 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -578,11 +578,11 @@ Alt-H2
------
```
-### Header IDs and links
+#### Header IDs and links
-All Markdown-rendered headers automatically get IDs, except in comments.
+All Markdown-rendered headers automatically get IDs, which can be linked to, except in comments.
-On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
+On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to use it somewhere else.
The IDs are generated from the content of the header according to the following rules:
@@ -609,8 +609,8 @@ Would generate the following link IDs:
1. `this-header-has-spaces-in-it`
1. `this-header-has-a-in-it`
1. `this-header-has-unicode-in-it-한글`
-1. `this-header-has-spaces-in-it`
1. `this-header-has-spaces-in-it-1`
+1. `this-header-has-spaces-in-it-2`
1. `this-header-has-3-5-in-it-and-parentheses`
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
@@ -715,25 +715,25 @@ Becomes:
There are two ways to create links, inline-style and reference-style.
- [I'm an inline-style link](https://www.google.com)
-
- [I'm a reference-style link][Arbitrary case-insensitive reference text]
-
- [I'm a relative reference to a repository file](LICENSE)
-
- [I am an absolute reference within the repository](/doc/user/markdown.md)
-
- [I link to the Milestones page](/../milestones)
+```markdown
+[I'm an inline-style link](https://www.google.com)
+[I'm a link to a repository file in the same directory](index.md)
+[I am an absolute reference within the repository](/doc/user/index.md)
+[I'm a relative link to the Milestones page](../README.md)
- [You can use numbers for reference-style link definitions][1]
+[I link to a section on a different markdown page, using a header ID](index.md#overview)
+[I link to a different section on the same page, using the header ID](#header-ids-and-links)
- Or leave it empty and use the [link text itself][]
+[I'm a reference-style link][Arbitrary case-insensitive reference text]
+[You can use numbers for reference-style link definitions][1]
+Or leave it empty and use the [link text itself][]
- Some text to show that the reference links can follow later.
+Some text to show that the reference links can follow later.
- [arbitrary case-insensitive reference text]: https://www.mozilla.org
- [1]: http://slashdot.org
- [link text itself]: https://www.reddit.com
+[arbitrary case-insensitive reference text]: https://www.mozilla.org
+[1]: http://slashdot.org
+[link text itself]: https://www.reddit.com
+```
>**Note:**
Relative links do not allow referencing project files in a wiki page or wiki
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 8b3ae19b544..df413a11af0 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -161,8 +161,8 @@ 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)
@@ -207,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 3a9a3b4a423..141fe488357 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -314,13 +314,7 @@ install it manually.
## Installing applications
-NOTE: **Note:**
-Before starting the installation of applications, make sure that time is synchronized
-between your GitLab server and your Kubernetes cluster. Otherwise, installation could fail
-and you may get errors like `Error: remote error: tls: bad certificate`
-in the `stdout` of pods created by GitLab in your Kubernetes cluster.
-
-GitLab provides a one-click install for various applications which can
+GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
be added directly to your configured cluster. Those applications are
needed for [Review Apps](../../../ci/review_apps/index.md) and
[deployments](../../../ci/environments.md). You can install them after you
@@ -378,6 +372,29 @@ Upgrades will reset values back to the values built into the `runner`
chart plus the values set by
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml)
+### Troubleshooting applications
+
+Applications can fail with the following error:
+
+```text
+Error: remote error: tls: bad certificate
+```
+
+To avoid installation errors:
+
+- Before starting the installation of applications, make sure that time is synchronized
+ between your GitLab server and your Kubernetes cluster.
+- Ensure certificates are not out of sync. When installing applications, GitLab expects a new cluster with no previous installation of Tiller.
+
+ You can confirm that the certificates match via `kubectl`:
+
+ ```sh
+ kubectl get configmaps/values-content-configuration-ingress -n gitlab-managed-apps -o \
+ "jsonpath={.data['cert\.pem']}" | base64 -d > a.pem
+ kubectl get secrets/tiller-secret -n gitlab-managed-apps -o "jsonpath={.data['ca\.crt']}" | base64 -d > b.pem
+ diff a.pem b.pem
+ ```
+
## Getting the external endpoint
NOTE: **Note:**
@@ -528,7 +545,7 @@ The result will then be:
## Deployment variables
The Kubernetes cluster integration exposes the following
-[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
+[deployment variables](../../../ci/variables/README.md#deployment-environment-variables) in the
GitLab CI/CD build environment.
| Variable | Description |
@@ -553,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 e6804666e22..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
@@ -214,7 +231,7 @@ The sample function can now be triggered from any HTTP client using a simple `PO
--header "Content-Type: application/json" \
--request POST \
--data '{"GitLab":"FaaS"}' \
- <http://functions-echo.functions-1.functions.example.com/>
+ http://functions-echo.functions-1.functions.example.com
```
2. Using a web-based tool (ie. postman, restlet, etc)
@@ -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/issue_board.md b/doc/user/project/issue_board.md
index ca19ce4d328..ad47b848bea 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -42,7 +42,7 @@ below.
## How it works
The Issue Board builds on GitLab's existing
-[issue tracking functionality](issues/index.md#issue-tracker) and
+[issue tracking functionality](issues/index.md#issues-list) and
leverages the power of [labels](labels.md) by utilizing them as lists of the scrum board.
With the Issue Board you can have a different view of your issues while
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index 40040e44d64..9a147deecd4 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -7,7 +7,7 @@ the information illustrated on the image below.
![New issue from the issues list](img/new_issue.png)
-Read through the [issues functionalities documentation](issues_functionalities.md#issues-functionalities)
+Read through the [issue data and actions documentation](issue_data_and_actions.md#parts-of-an-issue)
to understand these fields one by one.
## New issue from the Issue Tracker
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 675c280a12a..c82b7f100d2 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -1,170 +1,135 @@
# Issues
-The GitLab Issue Tracker is an advanced and complete tool
-for tracking the evolution of a new idea or the process
-of solving a problem.
+Issues are the fundamental medium for collaborating on ideas and planning work in GitLab.
-It allows you, your team, and your collaborators to share
-and discuss proposals before and while implementing them.
+## Overview
-GitLab Issues and the GitLab Issue Tracker are available in all
-[GitLab Products](https://about.gitlab.com/pricing/) as
-part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
+The GitLab issue tracker is an advanced tool for collaboratively developing ideas, solving problems, and planning work.
-## Use cases
+Issues can allow you, your team, and your collaborators to share and discuss proposals before and during their implementation.
+However, they can be used for a variety of other purposes, customized to your needs and workflow.
-Issues can have endless applications. Just to exemplify, these are
-some cases for which creating issues are most used:
+Issues are always associated with a specific project, but if you have multiple projects in a group,
+you can also view all the issues collectively at the group level.
+
+**Common use cases include:**
- Discussing the implementation of a new idea
-- Submitting feature proposals
-- Asking questions
-- Reporting bugs and malfunction
-- Obtaining support
+- Tracking tasks and work status
+- Accepting feature proposals, questions, support requests, or bug reports
- Elaborating new code implementations
See also the blog post "[Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)".
-### Keep private things private
-
-For instance, let's assume you have a public project but want to start a discussion on something
-you don't want to be public. With [Confidential Issues](#confidential-issues),
-you can discuss private matters among the project members, and still keep
-your project public, open to collaboration.
-
-### Streamline collaboration
-
-With [Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
-available in [GitLab Starter](https://about.gitlab.com/pricing/)
-you can streamline collaboration and allow shared responsibilities to be clearly displayed.
-All assignees are shown across your workflows and receive notifications (as they
-would as single assignees), simplifying communication and ownership.
-
-### Consistent collaboration
-
-Create [issue templates](#issue-templates) to make collaboration consistent and
-containing all information you need. For example, you can create a template
-for feature proposals and another one for bug reports.
-
-## Issue Tracker
-
-The Issue Tracker is the collection of opened and closed issues created in a project.
-It is available for all projects, from the moment the project is created.
-
-Find the issue tracker by navigating to your **Project's homepage** > **Issues**.
-
-### Issues per project
-
-When you access your project's issues, GitLab will present them in a list,
-and you can use the tabs available to quickly filter by open and closed issues.
-
-![Project issues list view](img/project_issues_list_view.png)
-
-You can also [search and filter](../../search/index.md#issues-and-merge-requests-per-project) the results more deeply with GitLab's search capacities.
-
-### Issues per group
-
-View issues in all projects in the group, including all projects of all descendant subgroups of the group. Navigate to **Group > Issues** to view these issues. This view also has the open and closed issues tabs.
-
-![Group Issues list view](img/group_issues_list_view.png)
-
-## GitLab Issues Functionalities
-
-The image bellow illustrates how an issue looks like:
+## Parts of an issue
+
+Issues contain a variety of content and metadata, enabling a large range of flexibility in how they are used. Each issue can contain the following attributes, though some items may remain unset.
+
+<table class="borderless-table fixed-table">
+<tr>
+ <td>
+ <ul>
+ <li>Content</li>
+ <ul>
+ <li>Title</li>
+ <li>Description and tasks</li>
+ <li>Comments and other activity</li>
+ </ul>
+ <li>People</li>
+ <ul>
+ <li>Author</li>
+ <li>Assignee(s)</li>
+ </ul>
+ <li>State</li>
+ <ul>
+ <li>Status (open/closed)</li>
+ <li>Confidentiality</li>
+ <li>Tasks (completed vs. outstanding)</li>
+ </ul>
+ </ul>
+ </td>
+ <td>
+ <ul>
+ <li>Planning and tracking</li>
+ <ul>
+ <li>Milestone</li>
+ <li>Due date</li>
+ <li>Weight</li>
+ <li>Time tracking</li>
+ <li>Labels</li>
+ <li>Votes</li>
+ <li>Reaction emoji</li>
+ <li>Linked issues</li>
+ <li>Assigned epic</li>
+ <li>Unique issue number and URL</li>
+ </ul>
+ </ul>
+ </td>
+</tr>
+</table>
+
+## Viewing and managing issues
+
+While you can view and manage the full detail of an issue at its URL, you can also work with multiple issues at a time using the Issues List, Issue Boards, Epics **[ULTIMATE]**, and issue references.
+
+### Issue page
![Issue view](img/issues_main_view.png)
-Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
-
-## New issue
+On an issue’s page, you can view all aspects of the issue, and you can also modify them if you you have the necessary [permissions](../../permissions.md).
-Read through the [documentation on creating issues](create_new_issue.md).
+For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page.
-## Closing issues
+### Issues list
-Learn distinct ways to [close issues](closing_issues.md) in GitLab.
-
-## Moving issues
-
-Read through the [documentation on moving issues](moving_issues.md).
-
-## Deleting issues
+![Project issues list view](img/project_issues_list_view.png)
-Read through the [documentation on deleting issues](deleting_issues.md)
+On the Issues List, you can view all issues in the current project, or from multiple projects when opening the Issues List from the higher-level group context. Filter the issue list by [any search query](../../search/index.md#issues-and-merge-requests-per-project) and/or specific metadata, such as label(s), assignees(s), status, and more. From this view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues.
-## Create a merge request from an issue
+For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page.
-Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request).
+### Issue boards
-## Search for an issue
+![Issue board](img/issue_board.png)
-Learn how to [find an issue](../../search/index.md) by searching for and filtering them.
+Issue boards are Kanban boards with columns that display issues based on their labels or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using highly customizable workflows.
-## Advanced features
+You can reorder issues within a column, or drag an issue card to another column; its associated label or assignee will change to match that of the new column. The entire board can also be filtered to only include issues from a certain milestone or an overarching label.
-### Confidential Issues
+For more information, see the [Issue Boards](../issue_board.md) page.
-Whenever you want to keep the discussion presented in a
-issue within your team only, you can make that
-[issue confidential](confidential_issues.md). Even if your project
-is public, that issue will be preserved. The browser will
-respond with a 404 error whenever someone who is not a project
-member with at least [Reporter level](../../permissions.md#project-members-permissions) tries to
-access that issue's URL.
+### Epics **[ULTIMATE]**
-Learn more about them on the [confidential issues documentation](confidential_issues.md).
+Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones.
-### Issue templates
+For more information, see the [Epics](https://docs.gitlab.com/ee/user/group/epics/) page.
-Create templates for every new issue. They will be available from
-the dropdown menu **Choose a template** when you create a new issue:
+### Related issues **[STARTER]**
-![issue template](img/issue_template.png)
+You can mark two issues as related, so that when viewing each one, the other is always listed in its Related Issues section. This can help display important context, such as past work, dependencies, or duplicates.
-Learn more about them on the [issue templates documentation](../../project/description_templates.md#creating-issue-templates).
+For more information, see [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html).
### Crosslinking issues
-Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
-
-### Issue Board
-
-The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
-enhance your workflow by organizing and prioritizing issues in GitLab.
-
-![Issue board](img/issue_board.png)
-
-Find GitLab Issue Boards by navigating to your **Project's Dashboard** > **Issues** > **Board**.
-
-Read through the documentation for [Issue Boards](../issue_board.md)
-to find out more about this feature.
-
-With [GitLab Starter](https://about.gitlab.com/pricing/), you can also
-create various boards per project with [Multiple Issue Boards](../issue_board.html#multiple-issue-boards-starter).
-
-### Import Issues from CSV
-
-You can import a CSV file containing issue titles and descriptions to create
-a batch of issues simultaneously.
-
-When you navigate to the Issues list page, an import button is displayed.
-
-For further details, see [Importing issues from CSV](csv_import.md)
-
-### External Issue Tracker
-
-Alternatively to GitLab's built-in Issue Tracker, you can also use an [external
-tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine,
-YouTrack, or Bugzilla.
-
-### Issue API
+When you reference an issue from another issue or merge request by including its URL or ID, the referenced issue displays a message in the Activity stream about the reference, with a link to the other issue or MR.
-See the [API documentation](../../../api/issues.md).
+For more information, see [Crosslinking issues](crosslinking_issues.md).
-### Bulk editing issues
+## Issue actions
-See the [bulk editing issues](../../project/bulk_editing.md) page.
+- [Create an issue](create_new_issue.md)
+- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
+- [Close an issue](closing_issues.md)
+- [Move an issue](moving_issues.md)
+- [Delete an issue](deleting_issues.md)
+- [Create a merge request from an issue](issue_data_and_actions.md#18-new-merge-request)
-### Similar issues
+## Advanced issue management
-See the [similar issues](similar_issues.md) page.
+- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues in order to change their status, assignee, milestone, or labels in bulk.
+- [Import issues](csv_import.md)
+- [Export issues](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) **[STARTER]**
+- [Issues API](../../../api/issues.md)
+- Configure an [external issue tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine,
+or Bugzilla.
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issue_data_and_actions.md
index 27b9dc51760..653bd94e513 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -1,8 +1,8 @@
-# GitLab Issues Functionalities
+# Issue Data and Actions
Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
-## Issues Functionalities
+## Parts of an Issue
The image below illustrates what an issue looks like:
@@ -77,7 +77,7 @@ can be changed as many times as needed.
Categorize issues by giving them [labels](../labels.md). They help to
organize workflows, and they enable you to work with the
-[GitLab Issue Board](index.md#issue-board).
+[GitLab Issue Board](index.md#issue-boards).
Group Labels, which allow you to use the same labels for a
group of projects, can be also given to issues. They work exactly the same,
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/index.md b/doc/user/project/merge_requests/index.md
index 01a3a5bbbe1..ba7d05a7ad7 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -219,6 +219,64 @@ apply the patches. The target branch can be specified using the
[`/target_branch` quick action](../quick_actions.md). If the source
branch already exists, the patches will be applied on top of it.
+## Git push options
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26752) in GitLab 11.10.
+
+NOTE: **Note:**
+Git push options are only available with Git 2.10 or newer.
+
+GitLab supports using
+[Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt)
+to perform the following actions against merge requests at the same time
+as pushing changes:
+
+- Create a new merge request for the pushed branch.
+- Set the target of the merge request to a particular branch.
+- Set the merge request to merge when its pipeline succeeds.
+
+### Create a new merge request using git push options
+
+To create a new merge request for a branch, use the
+`merge_request.create` push option:
+
+```sh
+git push -o merge_request.create
+```
+
+### Set the target branch of a merge request using git push options
+
+To update an existing merge request's target branch, use the
+`merge_request.target=<branch_name>` push option:
+
+```sh
+git push -o merge_request.target=branch_name
+```
+
+You can also create a merge request and set its target branch at the
+same time using a `-o` flag per push option:
+
+```sh
+git push -o merge_request.create -o merge_request.target=branch_name
+```
+
+### Set merge when pipeline succeeds using git push options
+
+To set an existing merge request to
+[merge when its pipeline succeeds](merge_when_pipeline_succeeds.md), use
+the `merge_request.merge_when_pipeline_succeeds` push option:
+
+```sh
+git push -o merge_request.merge_when_pipeline_succeeds
+```
+
+You can also create a merge request and set it to merge when its
+pipeline succeeds at the same time using a `-o` flag per push option:
+
+```sh
+git push -o merge_request.create -o merge_request.merge_when_pipeline_succeeds
+```
+
## Find the merge request that introduced a change
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5.
@@ -254,7 +312,7 @@ To prevent merge requests from accidentally being accepted before they're
completely ready, GitLab blocks the "Accept" button for merge requests that
have been marked as a **Work In Progress**.
-[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
+[Learn more about setting a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
## Merge request diff file navigation
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/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 756b8b698c7..fa7ab19ece6 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -149,7 +149,7 @@ verify your domain's ownership with a TXT record:
Once you've set the DNS record, you'll need navigate to your project's
**Setting > Pages** and click **+ New domain** to add your custom domain to
GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssltls-certificates)
-to make your website accessible under HTTPS or leave it blank. If don't add a certificate,
+to make your website accessible under HTTPS or leave it blank. If you don't add a certificate,
your site will be accessible only via HTTP:
![Add new domain](img/add_certificate_to_pages.png)
diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
index f639188684b..5ad500c4d20 100644
--- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
+++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
@@ -134,7 +134,7 @@ Now that your certificate has been issued, let's add it to your Pages site:
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem | pbcopy
```
-1. Copy and paste the public key into the second field **Key (PEM)**:
+1. Copy and paste the private key into the second field **Key (PEM)**:
```bash
sudo cat /etc/letsencrypt/live/example.com/privkey.pem | pbcopy
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 4a989afad4d..8b762307ac4 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -182,4 +182,4 @@ https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?st
## Environment Variables
-[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner.
+[Environment variables](../../../ci/variables/README.html#gitlab-cicd-environment-variables) can be set in an environment to be available to a runner.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 392e72dcc5c..88f4de891a1 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -31,7 +31,7 @@ discussions, and descriptions:
| `/reassign @user1 @user2` | Change assignee | ✓ | ✓ |
| `/milestone %milestone` | Set milestone | ✓ | ✓ |
| `/remove_milestone` | Remove milestone | ✓ | ✓ |
-| `/label ~label1 ~label2` | Add label(s) | ✓ | ✓ |
+| `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ |
| `/unlabel ~label1 ~label2` | Remove all or specific label(s)| ✓ | ✓ |
| `/relabel ~label1 ~label2` | Replace label | ✓ | ✓ |
| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request | ✓ | ✓ |
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 4533305bfd3..f9773086f69 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
@@ -1285,10 +1294,6 @@ module API
expose :id, :name, :slug, :external_url
end
- class Environment < EnvironmentBasic
- expose :project, using: Entities::BasicProjectDetails
- end
-
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at
expose :user, using: Entities::UserBasic
@@ -1296,6 +1301,11 @@ module API
expose :deployable, using: Entities::Job
end
+ class Environment < EnvironmentBasic
+ expose :project, using: Entities::BasicProjectDetails
+ expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
+ end
+
class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
expose :url, as: :html_url
@@ -1389,8 +1399,13 @@ module API
expose :name, :script, :timeout, :when, :allow_failure
end
+ class Port < Grape::Entity
+ expose :number, :protocol, :name
+ end
+
class Image < Grape::Entity
expose :name, :entrypoint
+ expose :ports, using: JobRequest::Port
end
class Service < Image
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 5b0f3b914cb..6cd43923559 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -101,6 +101,21 @@ module API
status 200
present environment, with: Entities::Environment, current_user: current_user
end
+
+ desc 'Get a single environment' do
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ get ':id/environments/:environment_id' do
+ authorize! :read_environment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+ present environment, with: Entities::Environment, current_user: current_user,
+ except: [:project, { last_deployment: [:environment] }],
+ last_deployment: true
+ end
end
end
end
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/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 3fd824877ae..71c30ec99a5 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -43,6 +43,28 @@ module API
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
+ def process_mr_push_options(push_options, project, user, changes)
+ output = {}
+
+ service = ::MergeRequests::PushOptionsHandlerService.new(
+ project,
+ user,
+ changes,
+ push_options
+ ).execute
+
+ if service.errors.present?
+ output[:warnings] = push_options_warning(service.errors.join("\n\n"))
+ end
+
+ output
+ end
+
+ def push_options_warning(warning)
+ options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ')
+ "Error encountered with push options #{options}: #{warning}"
+ end
+
def redis_ping
result = Gitlab::Redis::SharedState.with { |redis| redis.ping }
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index cb9aa849eeb..00f0bbab231 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
@@ -255,19 +256,27 @@ module API
post '/post_receive' do
status 200
+ output = {} # Messages to gitlab-shell
+ user = identify(params[:identifier])
+ project = Gitlab::GlRepository.parse(params[:gl_repository]).first
+ push_options = Gitlab::PushOptions.new(params[:push_options])
+
PostReceive.perform_async(params[:gl_repository], params[:identifier],
- params[:changes], params[:push_options].to_a)
+ params[:changes], push_options.as_json)
+
+ if Feature.enabled?(:mr_push_options, default_enabled: true)
+ mr_options = push_options.get(:merge_request)
+ output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present?
+ end
+
broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
- output = {
- merge_request_urls: merge_request_urls,
+ output.merge!(
broadcast_message: broadcast_message,
- reference_counter_decreased: reference_counter_decreased
- }
-
- project = Gitlab::GlRepository.parse(params[:gl_repository]).first
- user = identify(params[:identifier])
+ reference_counter_decreased: reference_counter_decreased,
+ merge_request_urls: merge_request_urls
+ )
# A user is not guaranteed to be returned; an orphaned write deploy
# key could be used
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index fae20e45bf9..000c00ea9f9 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -230,9 +230,13 @@ module API
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue
- # Setting created_at time only allowed for admins and project/group owners
- unless current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner)
- params.delete(:updated_at)
+ # Setting updated_at only allowed for admins and owners as well
+ if params[:updated_at].present?
+ if current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner)
+ issue.system_note_timestamp = params[:updated_at]
+ else
+ params.delete(:updated_at)
+ end
end
update_params = declared_params(include_missing: false).merge(request: request, api: true)
@@ -307,10 +311,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/pipelines.rb b/lib/api/pipelines.rb
index ac8fe98e55e..f29a18e94cf 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -81,6 +81,19 @@ module API
present pipeline, with: Entities::Pipeline
end
+ desc 'Gets the variables for a given pipeline' do
+ detail 'This feature was introduced in GitLab 11.11'
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id/variables' do
+ authorize! :read_pipeline_variable, pipeline
+
+ present pipeline.variables, with: Entities::Variable
+ end
+
desc 'Deletes a pipeline' do
detail 'This feature was introduced in GitLab 11.6'
http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
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/background_migration.rb b/lib/gitlab/background_migration.rb
index 5251e0fadf9..2e3a4f3b869 100644
--- a/lib/gitlab/background_migration.rb
+++ b/lib/gitlab/background_migration.rb
@@ -58,11 +58,31 @@ module Gitlab
migration_class_for(class_name).new.perform(*arguments)
end
- def self.exists?(migration_class)
+ def self.exists?(migration_class, additional_queues = [])
enqueued = Sidekiq::Queue.new(self.queue)
scheduled = Sidekiq::ScheduledSet.new
- [enqueued, scheduled].each do |queue|
+ enqueued_job?([enqueued, scheduled], migration_class)
+ end
+
+ def self.dead_jobs?(migration_class)
+ dead_set = Sidekiq::DeadSet.new
+
+ enqueued_job?([dead_set], migration_class)
+ end
+
+ def self.retrying_jobs?(migration_class)
+ retry_set = Sidekiq::RetrySet.new
+
+ enqueued_job?([retry_set], migration_class)
+ end
+
+ def self.migration_class_for(class_name)
+ const_get(class_name)
+ end
+
+ def self.enqueued_job?(queues, migration_class)
+ queues.each do |queue|
queue.each do |job|
return true if job.queue == self.queue && job.args.first == migration_class
end
@@ -70,9 +90,5 @@ module Gitlab
false
end
-
- def self.migration_class_for(class_name)
- const_get(class_name)
- end
end
end
diff --git a/lib/gitlab/background_migration/migrate_stage_index.rb b/lib/gitlab/background_migration/migrate_stage_index.rb
index f90f35a913d..f921233460d 100644
--- a/lib/gitlab/background_migration/migrate_stage_index.rb
+++ b/lib/gitlab/background_migration/migrate_stage_index.rb
@@ -21,8 +21,8 @@ module Gitlab
AND stage_idx IS NOT NULL
GROUP BY stage_id, stage_idx
), indexes AS (
- SELECT DISTINCT stage_id, last_value(stage_idx)
- OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index
+ SELECT DISTINCT stage_id, first_value(stage_idx)
+ OVER (PARTITION BY stage_id ORDER BY freq DESC) AS index
FROM freqs
)
diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb
index 4dd932f61d4..1d7bfba75cd 100644
--- a/lib/gitlab/ci/build/image.rb
+++ b/lib/gitlab/ci/build/image.rb
@@ -4,7 +4,7 @@ module Gitlab
module Ci
module Build
class Image
- attr_reader :alias, :command, :entrypoint, :name
+ attr_reader :alias, :command, :entrypoint, :name, :ports
class << self
def from_image(job)
@@ -26,17 +26,25 @@ module Gitlab
def initialize(image)
if image.is_a?(String)
@name = image
+ @ports = []
elsif image.is_a?(Hash)
@alias = image[:alias]
@command = image[:command]
@entrypoint = image[:entrypoint]
@name = image[:name]
+ @ports = build_ports(image).select(&:valid?)
end
end
def valid?
@name.present?
end
+
+ private
+
+ def build_ports(image)
+ image[:ports].to_a.map { |port| ::Gitlab::Ci::Build::Port.new(port) }
+ end
end
end
end
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/build/port.rb b/lib/gitlab/ci/build/port.rb
new file mode 100644
index 00000000000..6c4656ffea2
--- /dev/null
+++ b/lib/gitlab/ci/build/port.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Port
+ DEFAULT_PORT_NAME = 'default_port'.freeze
+ DEFAULT_PORT_PROTOCOL = 'http'.freeze
+
+ attr_reader :number, :protocol, :name
+
+ def initialize(port)
+ @name = DEFAULT_PORT_NAME
+ @protocol = DEFAULT_PORT_PROTOCOL
+
+ case port
+ when Integer
+ @number = port
+ when Hash
+ @number = port[:number]
+ @protocol = port.fetch(:protocol, @protocol)
+ @name = port.fetch(:name, @name)
+ end
+ end
+
+ def valid?
+ @number.present?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 69a3a1aedef..5a13fd18504 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -36,10 +36,12 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
validates :url,
+ type: String,
length: { maximum: 255 },
allow_nil: true
validates :action,
+ type: String,
inclusion: { in: %w[start stop], message: 'should be start or stop' },
allow_nil: true
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index a13a0625e90..0beeb44c272 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -9,24 +9,24 @@ module Gitlab
#
class Image < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Configurable
- ALLOWED_KEYS = %i[name entrypoint].freeze
+ ALLOWED_KEYS = %i[name entrypoint ports].freeze
validations do
validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true
end
- def hash?
- @config.is_a?(Hash)
- end
+ entry :ports, Entry::Ports,
+ description: 'Ports used expose the image'
- def string?
- @config.is_a?(String)
- end
+ attributes :ports
def name
value[:name]
@@ -42,6 +42,14 @@ module Gitlab
{}
end
+
+ def with_image_ports?
+ opt(:with_image_ports)
+ end
+
+ def skip_config_hash_validation?
+ true
+ end
end
end
end
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/config/entry/port.rb b/lib/gitlab/ci/config/entry/port.rb
new file mode 100644
index 00000000000..c239b1225c5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/port.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a configuration of an Image Port.
+ #
+ class Port < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ ALLOWED_KEYS = %i[number protocol name].freeze
+
+ validations do
+ validates :config, hash_or_integer: true
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ validates :number, type: Integer, presence: true
+ validates :protocol, type: String, inclusion: { in: %w[http https], message: 'should be http or https' }, allow_blank: true
+ validates :name, type: String, presence: false, allow_nil: true
+ end
+
+ def number
+ value[:number]
+ end
+
+ def protocol
+ value[:protocol]
+ end
+
+ def name
+ value[:name]
+ end
+
+ def value
+ return { number: @config } if integer?
+ return @config if hash?
+
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/ports.rb b/lib/gitlab/ci/config/entry/ports.rb
new file mode 100644
index 00000000000..01ffcc7dd87
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/ports.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a configuration of the ports of a Docker service.
+ #
+ class Ports < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, type: Array
+ validates :config, port_name_present_and_unique: true
+ validates :config, port_unique: true
+ end
+
+ def compose!(deps = nil)
+ super do
+ @entries = []
+ @config.each do |config|
+ @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port)
+ .value(config || {})
+ .with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+
+ def value
+ @entries.map(&:value)
+ end
+
+ def descendants
+ @entries
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 6df67083310..084fa4047a4 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -10,16 +10,18 @@ module Gitlab
class Service < Image
include ::Gitlab::Config::Entry::Validatable
- ALLOWED_KEYS = %i[name entrypoint command alias].freeze
+ ALLOWED_KEYS = %i[name entrypoint command alias ports].freeze
validations do
validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true
validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true
+ validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
end
def alias
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
index 71475f69218..83baa83711f 100644
--- a/lib/gitlab/ci/config/entry/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -12,6 +12,7 @@ module Gitlab
validations do
validates :config, type: Array
+ validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) }
end
def compose!(deps = nil)
@@ -20,6 +21,7 @@ module Gitlab
@config.each do |config|
@entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
.value(config || {})
+ .with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
index 79bbcc1ed1e..7d6e0704d4a 100644
--- a/lib/gitlab/ci/pipeline/chain/skip.rb
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -8,7 +8,6 @@ module Gitlab
include ::Gitlab::Utils::StrongMemoize
SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i
- SKIP_PUSH_OPTION = 'ci.skip'
def perform!
if skipped?
@@ -35,7 +34,8 @@ module Gitlab
end
def push_option_skips_ci?
- !!(@command.push_options&.include?(SKIP_PUSH_OPTION))
+ @command.push_options.present? &&
+ @command.push_options.deep_symbolize_keys.dig(:ci, :skip).present?
end
end
end
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 d4c70d4b9da..b3eec2b6210 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -72,879 +72,20 @@ stages:
- performance
- cleanup
-build:
- stage: build
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable"
- services:
- - docker:stable-dind
- script:
- - /build/build.sh
- only:
- - branches
- - tags
-
-test:
- services:
- - postgres:latest
- variables:
- POSTGRES_DB: test
- stage: test
- image: gliderlabs/herokuish:latest
- script:
- - setup_test_db
- - cp -R . /tmp/app
- - /bin/herokuish buildpack test
- only:
- - branches
- - tags
- except:
- variables:
- - $TEST_DISABLED
-
-code_quality:
- stage: test
- image: docker:stable
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - setup_docker
- - code_quality
- artifacts:
- paths: [gl-code-quality-report.json]
- only:
- - branches
- - tags
- except:
- variables:
- - $CODE_QUALITY_DISABLED
-
-license_management:
- stage: test
- image:
- name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
- entrypoint: [""]
- allow_failure: true
- script:
- - license_management
- artifacts:
- paths: [gl-license-management-report.json]
- only:
- refs:
- - branches
- - tags
- variables:
- - $GITLAB_FEATURES =~ /\blicense_management\b/
- except:
- variables:
- - $LICENSE_MANAGEMENT_DISABLED
-
-performance:
- stage: performance
- image: docker:stable
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - setup_docker
- - performance
- artifacts:
- paths:
- - performance.json
- - sitespeed-results/
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- variables:
- - $PERFORMANCE_DISABLED
-
-sast:
- stage: test
- image: docker:stable
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - setup_docker
- - sast
- artifacts:
- reports:
- sast: gl-sast-report.json
- only:
- refs:
- - branches
- - tags
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/
- except:
- variables:
- - $SAST_DISABLED
-
-dependency_scanning:
- stage: test
- image: docker:stable
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - setup_docker
- - dependency_scanning
- artifacts:
- reports:
- dependency_scanning: gl-dependency-scanning-report.json
- only:
- refs:
- - branches
- - tags
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/
- except:
- variables:
- - $DEPENDENCY_SCANNING_DISABLED
-
-container_scanning:
- stage: test
- image: docker:stable
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - setup_docker
- - container_scanning
- artifacts:
- paths: [gl-container-scanning-report.json]
- only:
- refs:
- - branches
- - tags
- variables:
- - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/
- except:
- variables:
- - $CONTAINER_SCANNING_DISABLED
-
+include:
+ - template: Jobs/Build.gitlab-ci.yml
+ - template: Jobs/Test.gitlab-ci.yml
+ - template: Jobs/Code-Quality.gitlab-ci.yml
+ - template: Jobs/Deploy.gitlab-ci.yml
+ - template: Jobs/Browser-Performance-Testing.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:
- stage: dast
- allow_failure: true
- image: registry.gitlab.com/gitlab-org/security-products/zaproxy
- variables:
- POSTGRES_DB: "false"
- script:
- - dast
- artifacts:
- paths: [gl-dast-report.json]
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- variables:
- - $GITLAB_FEATURES =~ /\bdast\b/
- except:
- refs:
- - master
- variables:
- - $DAST_DISABLED
-
-review:
- stage: review
- script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy
- - persist_environment_url
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- on_stop: stop_review
- artifacts:
- paths: [environment_url.txt]
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- refs:
- - master
- variables:
- - $REVIEW_DISABLED
-
-stop_review:
- stage: cleanup
- variables:
- GIT_STRATEGY: none
- script:
- - install_dependencies
- - initialize_tiller
- - delete
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
- when: manual
- allow_failure: true
- only:
- refs:
- - branches
- - tags
- kubernetes: active
except:
refs:
- - master
- variables:
- - $REVIEW_DISABLED
-
-# Staging deploys are disabled by default since
-# continuous deployment to production is enabled by default
-# If you prefer to automatically deploy to staging and
-# only manually promote to production, enable this job by setting
-# STAGING_ENABLED.
-
-staging:
- stage: staging
- script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy
- environment:
- name: staging
- url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $STAGING_ENABLED
-
-# Canaries are also disabled by default, but if you want them,
-# and know what the downsides are, you can enable this by setting
-# CANARY_ENABLED.
-
-canary:
- stage: canary
- script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy canary
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- when: manual
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $CANARY_ENABLED
-
-.production: &production_template
- stage: production
- script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy
- - delete canary
- - delete rollout
- - persist_environment_url
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- artifacts:
- paths: [environment_url.txt]
-
-production:
- <<: *production_template
- only:
- refs:
- - master
- kubernetes: active
- except:
- variables:
- - $STAGING_ENABLED
- - $CANARY_ENABLED
- - $INCREMENTAL_ROLLOUT_ENABLED
- - $INCREMENTAL_ROLLOUT_MODE
-
-production_manual:
- <<: *production_template
- when: manual
- allow_failure: false
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $STAGING_ENABLED
- - $CANARY_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_ENABLED
- - $INCREMENTAL_ROLLOUT_MODE
-
-# This job implements incremental rollout on for every push to `master`.
-
-.rollout: &rollout_template
- script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy rollout $ROLLOUT_PERCENTAGE
- - scale stable $((100-ROLLOUT_PERCENTAGE))
- - delete canary
- - persist_environment_url
- environment:
- name: production
- url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- artifacts:
- paths: [environment_url.txt]
-
-.manual_rollout_template: &manual_rollout_template
- <<: *rollout_template
- stage: production
- when: manual
- # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4)
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "manual"
- - $INCREMENTAL_ROLLOUT_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "timed"
-
-.timed_rollout_template: &timed_rollout_template
- <<: *rollout_template
- when: delayed
- start_in: 5 minutes
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "timed"
-
-timed rollout 10%:
- <<: *timed_rollout_template
- stage: incremental rollout 10%
- variables:
- ROLLOUT_PERCENTAGE: 10
-
-timed rollout 25%:
- <<: *timed_rollout_template
- stage: incremental rollout 25%
- variables:
- ROLLOUT_PERCENTAGE: 25
-
-timed rollout 50%:
- <<: *timed_rollout_template
- stage: incremental rollout 50%
- variables:
- ROLLOUT_PERCENTAGE: 50
-
-timed rollout 100%:
- <<: *timed_rollout_template
- <<: *production_template
- stage: incremental rollout 100%
- variables:
- ROLLOUT_PERCENTAGE: 100
-
-rollout 10%:
- <<: *manual_rollout_template
- variables:
- ROLLOUT_PERCENTAGE: 10
-
-rollout 25%:
- <<: *manual_rollout_template
- variables:
- ROLLOUT_PERCENTAGE: 25
-
-rollout 50%:
- <<: *manual_rollout_template
- variables:
- ROLLOUT_PERCENTAGE: 50
-
-rollout 100%:
- <<: *manual_rollout_template
- <<: *production_template
- allow_failure: false
-
-# ---------------------------------------------------------------------------
-
-.auto_devops: &auto_devops |
- # Auto DevOps variables and functions
- [[ "$TRACE" ]] && set -x
- auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB}
- export DATABASE_URL=${DATABASE_URL-$auto_database_url}
- if [[ -z "$CI_COMMIT_TAG" ]]; then
- export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
- export CI_APPLICATION_TAG=$CI_COMMIT_SHA
- else
- export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE
- export CI_APPLICATION_TAG=$CI_COMMIT_TAG
- fi
- export TILLER_NAMESPACE=$KUBE_NAMESPACE
- # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
-
- function registry_login() {
- if [[ -n "$CI_REGISTRY_USER" ]]; then
- echo "Logging to GitLab Container Registry with CI credentials..."
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- echo ""
- fi
- }
-
- function container_scanning() {
- registry_login
-
- docker run -d --name db arminc/clair-db:latest
- docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
- apk add -U wget ca-certificates
- docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
- mv clair-scanner_linux_amd64 clair-scanner
- chmod +x clair-scanner
- touch clair-whitelist.yml
- retries=0
- echo "Waiting for clair daemon to start"
- while( ! wget -T 10 -q -O /dev/null http://${DOCKER_SERVICE}:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- ./clair-scanner -c http://${DOCKER_SERVICE}:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
- }
-
- function code_quality() {
- docker run --env SOURCE_CODE="$PWD" \
- --volume "$PWD":/code \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- }
-
- function license_management() {
- /run.sh analyze .
- }
-
- function sast() {
- case "$CI_SERVER_VERSION" in
- *-ee)
-
- # Deprecation notice for CONFIDENCE_LEVEL variable
- if [ -z "$SAST_CONFIDENCE_LEVEL" -a "$CONFIDENCE_LEVEL" ]; then
- SAST_CONFIDENCE_LEVEL="$CONFIDENCE_LEVEL"
- echo "WARNING: CONFIDENCE_LEVEL is deprecated and MUST be replaced with SAST_CONFIDENCE_LEVEL"
- fi
-
- docker run --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}" \
- --volume "$PWD:/code" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
- ;;
- *)
- echo "GitLab EE is required"
- ;;
- esac
- }
-
- function dependency_scanning() {
- case "$CI_SERVER_VERSION" in
- *-ee)
- docker run --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}" \
- --volume "$PWD:/code" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
- ;;
- *)
- echo "GitLab EE is required"
- ;;
- esac
- }
-
- function get_replicas() {
- track="${1:-stable}"
- percentage="${2:-100}"
-
- env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
- env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
-
- if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then
- # for stable track get number of replicas from `PRODUCTION_REPLICAS`
- eval new_replicas=\$${env_slug}_REPLICAS
- if [[ -z "$new_replicas" ]]; then
- new_replicas=$REPLICAS
- fi
- else
- # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
- eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
- if [[ -z "$new_replicas" ]]; then
- eval new_replicas=\${env_track}_REPLICAS
- fi
- fi
-
- replicas="${new_replicas:-1}"
- replicas="$(($replicas * $percentage / 100))"
-
- # always return at least one replicas
- if [[ $replicas -gt 0 ]]; then
- echo "$replicas"
- else
- echo 1
- fi
- }
-
- # Extracts variables prefixed with K8S_SECRET_
- # and creates a Kubernetes secret.
- #
- # e.g. If we have the following environment variables:
- # K8S_SECRET_A=value1
- # K8S_SECRET_B=multi\ word\ value
- #
- # Then we will create a secret with the following key-value pairs:
- # data:
- # A: dmFsdWUxCg==
- # B: bXVsdGkgd29yZCB2YWx1ZQo=
- function create_application_secret() {
- track="${1-stable}"
- export APPLICATION_SECRET_NAME=$(application_secret_name "$track")
-
- env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables
-
- kubectl create secret \
- -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \
- --from-env-file k8s_prefixed_variables -o yaml --dry-run |
- kubectl replace -n "$KUBE_NAMESPACE" --force -f -
-
- export APPLICATION_SECRET_CHECKSUM=$(cat k8s_prefixed_variables | sha256sum | cut -d ' ' -f 1)
-
- rm k8s_prefixed_variables
- }
-
- function deploy_name() {
- name="$CI_ENVIRONMENT_SLUG"
- track="${1-stable}"
-
- if [[ "$track" != "stable" ]]; then
- name="$name-$track"
- fi
-
- echo $name
- }
-
- function application_secret_name() {
- track="${1-stable}"
- name=$(deploy_name "$track")
-
- echo "${name}-secret"
- }
-
- function deploy() {
- track="${1-stable}"
- percentage="${2:-100}"
- name=$(deploy_name "$track")
-
- replicas="1"
- service_enabled="true"
- postgres_enabled="$POSTGRES_ENABLED"
-
- # if track is different than stable,
- # re-use all attached resources
- if [[ "$track" != "stable" ]]; then
- service_enabled="false"
- postgres_enabled="false"
- fi
-
- replicas=$(get_replicas "$track" "$percentage")
-
- if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then
- secret_name='gitlab-registry'
- else
- secret_name=''
- fi
-
- create_application_secret "$track"
-
- env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]')
- eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS
- if [ -n "$env_ADDITIONAL_HOSTS" ]; then
- additional_hosts="{$env_ADDITIONAL_HOSTS}"
- elif [ -n "$ADDITIONAL_HOSTS" ]; then
- additional_hosts="{$ADDITIONAL_HOSTS}"
- fi
-
- if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then
- echo "Deploying first release with database initialization..."
- helm upgrade --install \
- --wait \
- --set service.enabled="$service_enabled" \
- --set gitlab.app="$CI_PROJECT_PATH_SLUG" \
- --set gitlab.env="$CI_ENVIRONMENT_SLUG" \
- --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
- --set image.repository="$CI_APPLICATION_REPOSITORY" \
- --set image.tag="$CI_APPLICATION_TAG" \
- --set image.pullPolicy=IfNotPresent \
- --set image.secrets[0].name="$secret_name" \
- --set application.track="$track" \
- --set application.database_url="$DATABASE_URL" \
- --set application.secretName="$APPLICATION_SECRET_NAME" \
- --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
- --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \
- --set service.url="$CI_ENVIRONMENT_URL" \
- --set service.additionalHosts="$additional_hosts" \
- --set replicaCount="$replicas" \
- --set postgresql.enabled="$postgres_enabled" \
- --set postgresql.nameOverride="postgres" \
- --set postgresql.postgresUser="$POSTGRES_USER" \
- --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
- --set postgresql.postgresDatabase="$POSTGRES_DB" \
- --set postgresql.imageTag="$POSTGRES_VERSION" \
- --set application.initializeCommand="$DB_INITIALIZE" \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
-
- echo "Deploying second release..."
- helm upgrade --reuse-values \
- --wait \
- --set application.initializeCommand="" \
- --set application.migrateCommand="$DB_MIGRATE" \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
- else
- echo "Deploying new release..."
- helm upgrade --install \
- --wait \
- --set service.enabled="$service_enabled" \
- --set gitlab.app="$CI_PROJECT_PATH_SLUG" \
- --set gitlab.env="$CI_ENVIRONMENT_SLUG" \
- --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
- --set image.repository="$CI_APPLICATION_REPOSITORY" \
- --set image.tag="$CI_APPLICATION_TAG" \
- --set image.pullPolicy=IfNotPresent \
- --set image.secrets[0].name="$secret_name" \
- --set application.track="$track" \
- --set application.database_url="$DATABASE_URL" \
- --set application.secretName="$APPLICATION_SECRET_NAME" \
- --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
- --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \
- --set service.url="$CI_ENVIRONMENT_URL" \
- --set service.additionalHosts="$additional_hosts" \
- --set replicaCount="$replicas" \
- --set postgresql.enabled="$postgres_enabled" \
- --set postgresql.nameOverride="postgres" \
- --set postgresql.postgresUser="$POSTGRES_USER" \
- --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
- --set postgresql.postgresDatabase="$POSTGRES_DB" \
- --set application.migrateCommand="$DB_MIGRATE" \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
- fi
-
- kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name"
- }
-
- function scale() {
- track="${1-stable}"
- percentage="${2-100}"
- name=$(deploy_name "$track")
-
- replicas=$(get_replicas "$track" "$percentage")
-
- if [[ -n "$(helm ls -q "^$name$")" ]]; then
- helm upgrade --reuse-values \
- --wait \
- --set replicaCount="$replicas" \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
- fi
- }
-
- function install_dependencies() {
- apk add -U openssl curl tar gzip bash ca-certificates git
- curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
- curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
- apk add glibc-2.28-r0.apk
- rm glibc-2.28-r0.apk
-
- curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
- mv linux-amd64/helm /usr/bin/
- mv linux-amd64/tiller /usr/bin/
- helm version --client
- tiller -version
-
- curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl"
- chmod +x /usr/bin/kubectl
- kubectl version --client
- }
-
- # With the Kubernetes executor, 'localhost' must be used instead
- # https://docs.gitlab.com/runner/executors/kubernetes.html
- function setup_docker() {
- if ! docker info &>/dev/null; then
- if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
- export DOCKER_HOST='tcp://localhost:2375'
- export DOCKER_SERVICE="localhost"
- else
- export DOCKER_SERVICE="docker"
- fi
- fi
- }
-
- function setup_test_db() {
- if [ -z ${KUBERNETES_PORT+x} ]; then
- DB_HOST=postgres
- else
- DB_HOST=localhost
- fi
- export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}"
- }
-
- function download_chart() {
- if [[ ! -d chart ]]; then
- auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app}
- auto_chart_name=$(basename $auto_chart)
- auto_chart_name=${auto_chart_name%.tgz}
- auto_chart_name=${auto_chart_name%.tar.gz}
- else
- auto_chart="chart"
- auto_chart_name="chart"
- fi
-
- helm init --client-only
- helm repo add gitlab ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io}
- if [[ ! -d "$auto_chart" ]]; then
- helm fetch ${auto_chart} --untar
- fi
- if [ "$auto_chart_name" != "chart" ]; then
- mv ${auto_chart_name} chart
- fi
-
- helm dependency update chart/
- helm dependency build chart/
- }
-
- function ensure_namespace() {
- kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
- }
-
-
- # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN
- function ensure_kube_ingress_base_domain() {
- if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ] && [ -n "$AUTO_DEVOPS_DOMAIN" ] ; then
- export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN
- fi
- }
-
- function check_kube_domain() {
- ensure_kube_ingress_base_domain
-
- if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then
- echo "In order to deploy or use Review Apps,"
- echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set"
- echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings"
- echo "or by defining a variable at group or project level."
- echo "You can also manually add it in .gitlab-ci.yml"
- echo "AUTO_DEVOPS_DOMAIN support will be dropped on 12.0"
- false
- else
- true
- fi
- }
-
- function initialize_tiller() {
- echo "Checking Tiller..."
-
- export HELM_HOST="localhost:44134"
- tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 &
- echo "Tiller is listening on ${HELM_HOST}"
-
- if ! helm version --debug; then
- echo "Failed to init Tiller."
- return 1
- fi
- echo ""
- }
-
- function create_secret() {
- echo "Create secret..."
- if [[ "$CI_PROJECT_VISIBILITY" == "public" ]]; then
- return
- fi
-
- kubectl create secret -n "$KUBE_NAMESPACE" \
- docker-registry gitlab-registry \
- --docker-server="$CI_REGISTRY" \
- --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \
- --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \
- --docker-email="$GITLAB_USER_EMAIL" \
- -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
- }
-
- function dast() {
- export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
-
- mkdir /zap/wrk/
- /zap/zap-baseline.py -J gl-dast-report.json -t "$CI_ENVIRONMENT_URL" || true
- cp /zap/wrk/gl-dast-report.json .
- }
-
- function performance() {
- export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
-
- mkdir gitlab-exporter
- wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
-
- mkdir sitespeed-results
-
- if [ -f .gitlab-urls.txt ]
- then
- sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
- else
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
- fi
-
- mv sitespeed-results/data/performance.json performance.json
- }
-
- function persist_environment_url() {
- echo $CI_ENVIRONMENT_URL > environment_url.txt
- }
-
- function delete() {
- track="${1-stable}"
- name=$(deploy_name "$track")
-
- if [[ -n "$(helm ls -q "^$name$")" ]]; then
- helm delete --purge "$name"
- fi
-
- secret_name=$(application_secret_name "$track")
- kubectl delete secret --ignore-not-found -n "$KUBE_NAMESPACE" "$secret_name"
- }
-
-before_script:
- - *auto_devops
+ - master \ No newline at end of file
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
new file mode 100644
index 00000000000..546c4affb4e
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
@@ -0,0 +1,38 @@
+performance:
+ stage: performance
+ image: docker:stable
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - |
+ if ! docker info &>/dev/null; then
+ if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
+ export DOCKER_HOST='tcp://localhost:2375'
+ fi
+ fi
+ - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+ - mkdir gitlab-exporter
+ - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
+ - mkdir sitespeed-results
+ - |
+ if [ -f .gitlab-urls.txt ]
+ then
+ sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt
+ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt
+ else
+ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
+ fi
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - performance.json
+ - sitespeed-results/
+ only:
+ refs:
+ - branches
+ - tags
+ kubernetes: active
+ except:
+ variables:
+ - $PERFORMANCE_DISABLED
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
new file mode 100644
index 00000000000..18f7290e1d9
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -0,0 +1,18 @@
+build:
+ stage: build
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable"
+ services:
+ - docker:stable-dind
+ script:
+ - |
+ if [[ -z "$CI_COMMIT_TAG" ]]; then
+ export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG}
+ export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA}
+ else
+ export CI_APPLICATION_REPOSITORY=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE}
+ export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
+ fi
+ - /build/build.sh
+ only:
+ - branches
+ - tags
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
new file mode 100644
index 00000000000..b09a24d8e22
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -0,0 +1,27 @@
+code_quality:
+ stage: test
+ image: docker:stable
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export CQ_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
+ - |
+ docker run --env SOURCE_CODE="$PWD" \
+ --volume "$PWD":/code \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$CQ_VERSION" /code
+ artifacts:
+ paths: [gl-code-quality-report.json]
+ only:
+ - branches
+ - tags
+ except:
+ variables:
+ - $CODE_QUALITY_DISABLED
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
new file mode 100644
index 00000000000..1e9591e113b
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -0,0 +1,575 @@
+review:
+ stage: review
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - initialize_tiller
+ - create_secret
+ - deploy
+ - persist_environment_url
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ on_stop: stop_review
+ artifacts:
+ paths: [environment_url.txt]
+ only:
+ refs:
+ - branches
+ - tags
+ kubernetes: active
+ except:
+ refs:
+ - master
+ variables:
+ - $REVIEW_DISABLED
+
+stop_review:
+ stage: cleanup
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - install_dependencies
+ - initialize_tiller
+ - delete
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ when: manual
+ allow_failure: true
+ only:
+ refs:
+ - branches
+ - tags
+ kubernetes: active
+ except:
+ refs:
+ - master
+ variables:
+ - $REVIEW_DISABLED
+
+# Staging deploys are disabled by default since
+# continuous deployment to production is enabled by default
+# If you prefer to automatically deploy to staging and
+# only manually promote to production, enable this job by setting
+# STAGING_ENABLED.
+
+staging:
+ stage: staging
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - initialize_tiller
+ - create_secret
+ - deploy
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $STAGING_ENABLED
+
+# Canaries are disabled by default, but if you want them,
+# and know what the downsides are, you can enable this by setting
+# CANARY_ENABLED.
+
+canary:
+ stage: canary
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - initialize_tiller
+ - create_secret
+ - deploy canary
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $CANARY_ENABLED
+
+.production: &production_template
+ stage: production
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - initialize_tiller
+ - create_secret
+ - deploy
+ - delete canary
+ - delete rollout
+ - persist_environment_url
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ artifacts:
+ paths: [environment_url.txt]
+
+production:
+ <<: *production_template
+ only:
+ refs:
+ - master
+ kubernetes: active
+ except:
+ variables:
+ - $STAGING_ENABLED
+ - $CANARY_ENABLED
+ - $INCREMENTAL_ROLLOUT_ENABLED
+ - $INCREMENTAL_ROLLOUT_MODE
+
+production_manual:
+ <<: *production_template
+ when: manual
+ allow_failure: false
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $STAGING_ENABLED
+ - $CANARY_ENABLED
+ except:
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+ - $INCREMENTAL_ROLLOUT_MODE
+
+# This job implements incremental rollout on for every push to `master`.
+
+.rollout: &rollout_template
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - initialize_tiller
+ - create_secret
+ - deploy rollout $ROLLOUT_PERCENTAGE
+ - scale stable $((100-ROLLOUT_PERCENTAGE))
+ - delete canary
+ - persist_environment_url
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ artifacts:
+ paths: [environment_url.txt]
+
+.manual_rollout_template: &manual_rollout_template
+ <<: *rollout_template
+ stage: production
+ when: manual
+ # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4)
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_MODE == "manual"
+ - $INCREMENTAL_ROLLOUT_ENABLED
+ except:
+ variables:
+ - $INCREMENTAL_ROLLOUT_MODE == "timed"
+
+.timed_rollout_template: &timed_rollout_template
+ <<: *rollout_template
+ when: delayed
+ start_in: 5 minutes
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_MODE == "timed"
+
+timed rollout 10%:
+ <<: *timed_rollout_template
+ stage: incremental rollout 10%
+ variables:
+ ROLLOUT_PERCENTAGE: 10
+
+timed rollout 25%:
+ <<: *timed_rollout_template
+ stage: incremental rollout 25%
+ variables:
+ ROLLOUT_PERCENTAGE: 25
+
+timed rollout 50%:
+ <<: *timed_rollout_template
+ stage: incremental rollout 50%
+ variables:
+ ROLLOUT_PERCENTAGE: 50
+
+timed rollout 100%:
+ <<: *timed_rollout_template
+ <<: *production_template
+ stage: incremental rollout 100%
+ variables:
+ ROLLOUT_PERCENTAGE: 100
+
+rollout 10%:
+ <<: *manual_rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 10
+
+rollout 25%:
+ <<: *manual_rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 25
+
+rollout 50%:
+ <<: *manual_rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 50
+
+rollout 100%:
+ <<: *manual_rollout_template
+ <<: *production_template
+ allow_failure: false
+
+.deploy_helpers: &deploy_helpers |
+ [[ "$TRACE" ]] && set -x
+ auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB}
+ export DATABASE_URL=${DATABASE_URL-$auto_database_url}
+ export TILLER_NAMESPACE=$KUBE_NAMESPACE
+ # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
+
+ function get_replicas() {
+ track="${1:-stable}"
+ percentage="${2:-100}"
+
+ env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
+ env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
+
+ if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then
+ # for stable track get number of replicas from `PRODUCTION_REPLICAS`
+ eval new_replicas=\$${env_slug}_REPLICAS
+ if [[ -z "$new_replicas" ]]; then
+ new_replicas=$REPLICAS
+ fi
+ else
+ # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
+ eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
+ if [[ -z "$new_replicas" ]]; then
+ eval new_replicas=\${env_track}_REPLICAS
+ fi
+ fi
+
+ replicas="${new_replicas:-1}"
+ replicas="$(($replicas * $percentage / 100))"
+
+ # always return at least one replicas
+ if [[ $replicas -gt 0 ]]; then
+ echo "$replicas"
+ else
+ echo 1
+ fi
+ }
+
+ # Extracts variables prefixed with K8S_SECRET_
+ # and creates a Kubernetes secret.
+ #
+ # e.g. If we have the following environment variables:
+ # K8S_SECRET_A=value1
+ # K8S_SECRET_B=multi\ word\ value
+ #
+ # Then we will create a secret with the following key-value pairs:
+ # data:
+ # A: dmFsdWUxCg==
+ # B: bXVsdGkgd29yZCB2YWx1ZQo=
+ function create_application_secret() {
+ track="${1-stable}"
+ export APPLICATION_SECRET_NAME=$(application_secret_name "$track")
+
+ env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables
+
+ kubectl create secret \
+ -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \
+ --from-env-file k8s_prefixed_variables -o yaml --dry-run |
+ kubectl replace -n "$KUBE_NAMESPACE" --force -f -
+
+ export APPLICATION_SECRET_CHECKSUM=$(cat k8s_prefixed_variables | sha256sum | cut -d ' ' -f 1)
+
+ rm k8s_prefixed_variables
+ }
+
+ function deploy_name() {
+ name="$CI_ENVIRONMENT_SLUG"
+ track="${1-stable}"
+
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ fi
+
+ echo $name
+ }
+
+ function application_secret_name() {
+ track="${1-stable}"
+ name=$(deploy_name "$track")
+
+ echo "${name}-secret"
+ }
+
+ function deploy() {
+ track="${1-stable}"
+ percentage="${2:-100}"
+ name=$(deploy_name "$track")
+
+ if [[ -z "$CI_COMMIT_TAG" ]]; then
+ image_repository=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG}
+ image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA}
+ else
+ image_repository=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE}
+ image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
+ fi
+
+ replicas="1"
+ service_enabled="true"
+ postgres_enabled="$POSTGRES_ENABLED"
+
+ # if track is different than stable,
+ # re-use all attached resources
+ if [[ "$track" != "stable" ]]; then
+ service_enabled="false"
+ postgres_enabled="false"
+ fi
+
+ replicas=$(get_replicas "$track" "$percentage")
+
+ if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then
+ secret_name='gitlab-registry'
+ else
+ secret_name=''
+ fi
+
+ create_application_secret "$track"
+
+ env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]')
+ eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS
+ if [ -n "$env_ADDITIONAL_HOSTS" ]; then
+ additional_hosts="{$env_ADDITIONAL_HOSTS}"
+ elif [ -n "$ADDITIONAL_HOSTS" ]; then
+ additional_hosts="{$ADDITIONAL_HOSTS}"
+ fi
+
+ if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then
+ echo "Deploying first release with database initialization..."
+ helm upgrade --install \
+ --wait \
+ --set service.enabled="$service_enabled" \
+ --set gitlab.app="$CI_PROJECT_PATH_SLUG" \
+ --set gitlab.env="$CI_ENVIRONMENT_SLUG" \
+ --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
+ --set image.repository="$image_repository" \
+ --set image.tag="$image_tag" \
+ --set image.pullPolicy=IfNotPresent \
+ --set image.secrets[0].name="$secret_name" \
+ --set application.track="$track" \
+ --set application.database_url="$DATABASE_URL" \
+ --set application.secretName="$APPLICATION_SECRET_NAME" \
+ --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
+ --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \
+ --set service.url="$CI_ENVIRONMENT_URL" \
+ --set service.additionalHosts="$additional_hosts" \
+ --set replicaCount="$replicas" \
+ --set postgresql.enabled="$postgres_enabled" \
+ --set postgresql.nameOverride="postgres" \
+ --set postgresql.postgresUser="$POSTGRES_USER" \
+ --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
+ --set postgresql.postgresDatabase="$POSTGRES_DB" \
+ --set postgresql.imageTag="$POSTGRES_VERSION" \
+ --set application.initializeCommand="$DB_INITIALIZE" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+
+ echo "Deploying second release..."
+ helm upgrade --reuse-values \
+ --wait \
+ --set application.initializeCommand="" \
+ --set application.migrateCommand="$DB_MIGRATE" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+ else
+ echo "Deploying new release..."
+ helm upgrade --install \
+ --wait \
+ --set service.enabled="$service_enabled" \
+ --set gitlab.app="$CI_PROJECT_PATH_SLUG" \
+ --set gitlab.env="$CI_ENVIRONMENT_SLUG" \
+ --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
+ --set image.repository="$image_repository" \
+ --set image.tag="$image_tag" \
+ --set image.pullPolicy=IfNotPresent \
+ --set image.secrets[0].name="$secret_name" \
+ --set application.track="$track" \
+ --set application.database_url="$DATABASE_URL" \
+ --set application.secretName="$APPLICATION_SECRET_NAME" \
+ --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
+ --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \
+ --set service.url="$CI_ENVIRONMENT_URL" \
+ --set service.additionalHosts="$additional_hosts" \
+ --set replicaCount="$replicas" \
+ --set postgresql.enabled="$postgres_enabled" \
+ --set postgresql.nameOverride="postgres" \
+ --set postgresql.postgresUser="$POSTGRES_USER" \
+ --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
+ --set postgresql.postgresDatabase="$POSTGRES_DB" \
+ --set application.migrateCommand="$DB_MIGRATE" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+ fi
+
+ kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name"
+ }
+
+ function scale() {
+ track="${1-stable}"
+ percentage="${2-100}"
+ name=$(deploy_name "$track")
+
+ replicas=$(get_replicas "$track" "$percentage")
+
+ if [[ -n "$(helm ls -q "^$name$")" ]]; then
+ helm upgrade --reuse-values \
+ --wait \
+ --set replicaCount="$replicas" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+ fi
+ }
+
+ function install_dependencies() {
+ apk add -U openssl curl tar gzip bash ca-certificates git
+ curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
+ curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
+ apk add glibc-2.28-r0.apk
+ rm glibc-2.28-r0.apk
+
+ curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
+ mv linux-amd64/helm /usr/bin/
+ mv linux-amd64/tiller /usr/bin/
+ helm version --client
+ tiller -version
+
+ curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl"
+ chmod +x /usr/bin/kubectl
+ kubectl version --client
+ }
+
+ function download_chart() {
+ if [[ ! -d chart ]]; then
+ auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app}
+ auto_chart_name=$(basename $auto_chart)
+ auto_chart_name=${auto_chart_name%.tgz}
+ auto_chart_name=${auto_chart_name%.tar.gz}
+ else
+ auto_chart="chart"
+ auto_chart_name="chart"
+ fi
+
+ helm init --client-only
+ helm repo add gitlab ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io}
+ if [[ ! -d "$auto_chart" ]]; then
+ helm fetch ${auto_chart} --untar
+ fi
+ if [ "$auto_chart_name" != "chart" ]; then
+ mv ${auto_chart_name} chart
+ fi
+
+ helm dependency update chart/
+ helm dependency build chart/
+ }
+
+ function ensure_namespace() {
+ kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
+ }
+
+ # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN
+ function ensure_kube_ingress_base_domain() {
+ if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ] && [ -n "$AUTO_DEVOPS_DOMAIN" ] ; then
+ export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN
+ fi
+ }
+
+ function check_kube_domain() {
+ ensure_kube_ingress_base_domain
+
+ if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then
+ echo "In order to deploy or use Review Apps,"
+ echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set"
+ echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings"
+ echo "or by defining a variable at group or project level."
+ echo "You can also manually add it in .gitlab-ci.yml"
+ echo "AUTO_DEVOPS_DOMAIN support will be dropped on 12.0"
+ false
+ else
+ true
+ fi
+ }
+
+ function initialize_tiller() {
+ echo "Checking Tiller..."
+
+ export HELM_HOST="localhost:44134"
+ tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 &
+ echo "Tiller is listening on ${HELM_HOST}"
+
+ if ! helm version --debug; then
+ echo "Failed to init Tiller."
+ return 1
+ fi
+ echo ""
+ }
+
+ function create_secret() {
+ echo "Create secret..."
+ if [[ "$CI_PROJECT_VISIBILITY" == "public" ]]; then
+ return
+ fi
+
+ kubectl create secret -n "$KUBE_NAMESPACE" \
+ docker-registry gitlab-registry \
+ --docker-server="$CI_REGISTRY" \
+ --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \
+ --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \
+ --docker-email="$GITLAB_USER_EMAIL" \
+ -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
+ }
+
+ function persist_environment_url() {
+ echo $CI_ENVIRONMENT_URL > environment_url.txt
+ }
+
+ function delete() {
+ track="${1-stable}"
+ name=$(deploy_name "$track")
+
+ if [[ -n "$(helm ls -q "^$name$")" ]]; then
+ helm delete --purge "$name"
+ fi
+
+ secret_name=$(application_secret_name "$track")
+ kubectl delete secret --ignore-not-found -n "$KUBE_NAMESPACE" "$secret_name"
+ }
+
+before_script:
+ - *deploy_helpers
diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
new file mode 100644
index 00000000000..b9fe838d1da
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
@@ -0,0 +1,23 @@
+test:
+ services:
+ - postgres:latest
+ variables:
+ POSTGRES_DB: test
+ stage: test
+ image: gliderlabs/herokuish:latest
+ script:
+ - |
+ if [ -z ${KUBERNETES_PORT+x} ]; then
+ DB_HOST=postgres
+ else
+ DB_HOST=localhost
+ fi
+ - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}"
+ - cp -R . /tmp/app
+ - /bin/herokuish buildpack test
+ only:
+ - branches
+ - tags
+ except:
+ variables:
+ - $TEST_DISABLED
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 0b7a531682b..eef361c19e9 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -28,6 +28,12 @@ container_scanning:
- docker:stable-dind
script:
- if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then { export DOCKER_SERVICE="localhost" ; export DOCKER_HOST="tcp://${DOCKER_SERVICE}:2375" ; } fi
+ - |
+ if [[ -n "$CI_REGISTRY_USER" ]]; then
+ echo "Logging to GitLab Container Registry with CI credentials..."
+ docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
+ echo ""
+ fi
- docker run -d --name db arminc/clair-db:latest
- docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION}
- apk add -U wget ca-certificates
@@ -36,7 +42,6 @@ container_scanning:
- mv clair-scanner_linux_amd64 clair-scanner
- chmod +x clair-scanner
- touch clair-whitelist.yml
- - while( ! wget -q -O /dev/null http://${DOCKER_SERVICE}:6060/v1/namespaces ) ; do sleep 1 ; done
- retries=0
- echo "Waiting for clair daemon to start"
- while( ! wget -T 10 -q -O /dev/null http://${DOCKER_SERVICE}:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index ef6d7866e85..2a90cc9a06c 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -4,9 +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
-variables:
- DAST_WEBSITE: http://example.com # Please edit to be your website to scan for vulnerabilities
-
stages:
- build
- test
@@ -22,8 +19,15 @@ dast:
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 \
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index fd666541d41..7f80a6e9285 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -15,6 +15,12 @@ dependency_scanning:
script:
- export DS_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
+ - |
docker run \
--env DS_ANALYZER_IMAGES \
--env DS_ANALYZER_IMAGE_PREFIX \
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 034fba5499c..b941e89991e 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -15,6 +15,12 @@ sast:
script:
- export SAST_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
+ - |
docker run \
--env SAST_ANALYZER_IMAGES \
--env SAST_ANALYZER_IMAGE_PREFIX \
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/configurable.rb b/lib/gitlab/config/entry/configurable.rb
index 37ba16dba25..6667a5d3d33 100644
--- a/lib/gitlab/config/entry/configurable.rb
+++ b/lib/gitlab/config/entry/configurable.rb
@@ -21,7 +21,7 @@ module Gitlab
include Validatable
validations do
- validates :config, type: Hash
+ validates :config, type: Hash, unless: :skip_config_hash_validation?
end
end
@@ -30,6 +30,10 @@ module Gitlab
return unless valid?
self.class.nodes.each do |key, factory|
+ # If we override the config type validation
+ # we can end with different config types like String
+ next unless config.is_a?(Hash)
+
factory
.value(config[key])
.with(key: key, parent: self)
@@ -45,6 +49,10 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
+ def skip_config_hash_validation?
+ false
+ end
+
class_methods do
def nodes
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
index 79f9ff32514..3c06b1e0d24 100644
--- a/lib/gitlab/config/entry/factory.rb
+++ b/lib/gitlab/config/entry/factory.rb
@@ -61,7 +61,7 @@ module Gitlab
end
def fabricate(entry, value = nil)
- entry.new(value, @metadata).tap do |node|
+ entry.new(value, @metadata) do |node|
node.key = @attributes[:key]
node.parent = @attributes[:parent]
node.default = @attributes[:default]
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
index 9999ab4ff95..e014f15fbd8 100644
--- a/lib/gitlab/config/entry/node.rb
+++ b/lib/gitlab/config/entry/node.rb
@@ -17,6 +17,8 @@ module Gitlab
@metadata = metadata
@entries = {}
+ yield(self) if block_given?
+
self.class.aspects.to_a.each do |aspect|
instance_exec(&aspect)
end
@@ -44,6 +46,12 @@ module Gitlab
@parent ? @parent.ancestors + [@parent] : []
end
+ def opt(key)
+ opt = metadata[key]
+ opt = @parent.opt(key) if opt.nil? && @parent
+ opt
+ end
+
def valid?
errors.none?
end
@@ -85,6 +93,18 @@ module Gitlab
"#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
end
+ def hash?
+ @config.is_a?(Hash)
+ end
+
+ def string?
+ @config.is_a?(String)
+ end
+
+ def integer?
+ @config.is_a?(Integer)
+ end
+
def self.default(**)
end
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
index 5fbf7565e2a..a56a89adb35 100644
--- a/lib/gitlab/config/entry/simplifiable.rb
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -19,7 +19,10 @@ module Gitlab
entry = self.class.entry_class(strategy)
- super(@subject = entry.new(config, metadata))
+ @subject = entry.new(config, metadata)
+
+ yield(@subject) if block_given?
+ super(@subject)
end
def self.strategy(name, **opts)
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index d348e11b753..df34d254c65 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -15,6 +15,17 @@ module Gitlab
end
end
+ class DisallowedKeysValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ present_keys = value.try(:keys).to_a & options[:in]
+
+ if present_keys.any?
+ record.errors.add(attribute, "contains disallowed keys: " +
+ present_keys.join(', '))
+ end
+ end
+ end
+
class AllowedValuesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless options[:in].include?(value.to_s)
@@ -43,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
@@ -118,6 +137,12 @@ module Gitlab
end
end
+ protected
+
+ def fallback
+ false
+ end
+
private
def matches_syntax?(value)
@@ -126,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
@@ -151,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)
@@ -186,6 +219,97 @@ module Gitlab
end
end
end
+
+ class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return unless value.is_a?(Array)
+
+ ports_size = value.count
+ return if ports_size <= 1
+
+ named_ports = value.select { |e| e.is_a?(Hash) }.map { |e| e[:name] }.compact.map(&:downcase)
+
+ if ports_size != named_ports.size
+ record.errors.add(attribute, 'when there is more than one port, a unique name should be added')
+ end
+
+ if ports_size != named_ports.uniq.size
+ record.errors.add(attribute, 'each port name must be different')
+ end
+ end
+ end
+
+ class PortUniqueValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ value = ports(value)
+ return unless value.is_a?(Array)
+
+ ports_size = value.count
+ return if ports_size <= 1
+
+ if transform_ports(value).size != ports_size
+ record.errors.add(attribute, 'each port number can only be referenced once')
+ end
+ end
+
+ private
+
+ def ports(current_data)
+ current_data
+ end
+
+ def transform_ports(raw_ports)
+ raw_ports.map do |port|
+ case port
+ when Integer
+ port
+ when Hash
+ port[:number]
+ end
+ end.uniq
+ end
+ end
+
+ class JobPortUniqueValidator < PortUniqueValidator
+ private
+
+ def ports(current_data)
+ return unless current_data.is_a?(Hash)
+
+ (image_ports(current_data) + services_ports(current_data)).compact
+ end
+
+ def image_ports(current_data)
+ return [] unless current_data[:image].is_a?(Hash)
+
+ current_data.dig(:image, :ports).to_a
+ end
+
+ def services_ports(current_data)
+ current_data.dig(:services).to_a.flat_map { |service| service.is_a?(Hash) ? service[:ports] : nil }
+ end
+ end
+
+ class ServicesWithPortsAliasUniqueValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ current_aliases = aliases(value)
+ return if current_aliases.empty?
+
+ unless aliases_unique?(current_aliases)
+ record.errors.add(:config, 'alias must be unique in services with ports')
+ end
+ end
+
+ private
+
+ def aliases(value)
+ value.select { |s| s.is_a?(Hash) && s[:ports] }.pluck(:alias) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ def aliases_unique?(aliases)
+ aliases.size == aliases.uniq.size
+ end
+ end
end
end
end
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/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index ea08b5f7eae..af385d7d4ca 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -32,10 +32,7 @@ module Gitlab
}
],
total_commits_count: 1,
- push_options: [
- "ci.skip",
- "custom option"
- ]
+ push_options: { ci: { skip: true } }
}.freeze
# Produce a hash of post-receive data
@@ -57,11 +54,11 @@ module Gitlab
# },
# commits: Array,
# total_commits_count: Fixnum,
- # push_options: Array
+ # push_options: Hash
# }
#
# rubocop:disable Metrics/ParameterLists
- def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: [])
+ def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {})
commits = Array(commits)
# Total commits count
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/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index 426436c2164..d98b85fecc4 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -5,7 +5,7 @@ module Gitlab
include Gitlab::Identifier
attr_reader :project, :identifier, :changes, :push_options
- def initialize(project, identifier, changes, push_options)
+ def initialize(project, identifier, changes, push_options = {})
@project = project
@identifier = identifier
@changes = deserialize_changes(changes)
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/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 6d48c6a15b4..6aad7955415 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -54,6 +54,11 @@ module Gitlab
project.repository.fetch_as_mirror(project.import_url, refmap: refmap, forced: true, remote_name: 'github')
project.change_head(default_branch) if default_branch
+
+ # The initial fetch can bring in lots of loose refs and objects.
+ # Running a `git gc` will make importing pull requests faster.
+ Projects::HousekeepingService.new(project, :gc).execute
+
true
rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
fail_import("Failed to import the repository: #{e.message}")
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/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb
index b3fe1fc0685..4bc39868389 100644
--- a/lib/gitlab/import/merge_request_helpers.rb
+++ b/lib/gitlab/import/merge_request_helpers.rb
@@ -22,7 +22,7 @@ module Gitlab
# additional work that is strictly necessary.
merge_request_id = insert_and_return_id(attributes, project.merge_requests)
- merge_request = project.merge_requests.reload.find(merge_request_id)
+ merge_request = project.merge_requests.reset.find(merge_request_id)
[merge_request, false]
end
diff --git a/lib/gitlab/legacy_github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb
index 8c0c17780ca..746786b5a66 100644
--- a/lib/gitlab/legacy_github_import/release_formatter.rb
+++ b/lib/gitlab/legacy_github_import/release_formatter.rb
@@ -7,6 +7,7 @@ module Gitlab
{
project: project,
tag: raw_data.tag_name,
+ name: raw_data.name,
description: raw_data.body,
created_at: raw_data.created_at,
updated_at: raw_data.created_at
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/push_options.rb b/lib/gitlab/push_options.rb
new file mode 100644
index 00000000000..810aba436cc
--- /dev/null
+++ b/lib/gitlab/push_options.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class PushOptions
+ VALID_OPTIONS = HashWithIndifferentAccess.new({
+ merge_request: {
+ keys: [:create, :merge_when_pipeline_succeeds, :target]
+ },
+ ci: {
+ keys: [:skip]
+ }
+ }).freeze
+
+ NAMESPACE_ALIASES = HashWithIndifferentAccess.new({
+ mr: :merge_request
+ }).freeze
+
+ OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/
+
+ attr_reader :options
+
+ def initialize(options = [])
+ @options = parse_options(options)
+ end
+
+ def get(*args)
+ options.dig(*args)
+ end
+
+ # Allow #to_json serialization
+ def as_json(*_args)
+ options
+ end
+
+ private
+
+ def parse_options(raw_options)
+ options = HashWithIndifferentAccess.new
+
+ Array.wrap(raw_options).each do |option|
+ namespace, key, value = parse_option(option)
+
+ next if [namespace, key].any?(&:nil?)
+
+ options[namespace] ||= HashWithIndifferentAccess.new
+ options[namespace][key] = value
+ end
+
+ options
+ end
+
+ def parse_option(option)
+ parts = OPTION_MATCHER.match(option)
+ return unless parts
+
+ namespace, key, value = parts.values_at(:namespace, :key, :value).map(&:strip)
+ namespace = NAMESPACE_ALIASES[namespace] if NAMESPACE_ALIASES[namespace]
+ value = value.presence || true
+
+ return unless valid_option?(namespace, key)
+
+ [namespace, key, value]
+ end
+
+ def valid_option?(namespace, key)
+ keys = VALID_OPTIONS.dig(namespace, :keys)
+ keys && keys.include?(key.to_sym)
+ 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/sentry/client.rb b/lib/sentry/client.rb
index bb1aa2a7a10..4022e8ff946 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -47,9 +47,11 @@ module Sentry
end
def http_get(url, params = {})
- resp = Gitlab::HTTP.get(url, **request_params.merge(params))
+ response = handle_request_exceptions do
+ Gitlab::HTTP.get(url, **request_params.merge(params))
+ end
- handle_response(resp)
+ handle_response(response)
end
def get_issues(issue_status:, limit:)
@@ -63,14 +65,36 @@ module Sentry
http_get(projects_api_url)
end
+ def handle_request_exceptions
+ yield
+ rescue HTTParty::Error => e
+ Gitlab::Sentry.track_acceptable_exception(e)
+ raise_error 'Error when connecting to Sentry'
+ rescue Net::OpenTimeout
+ raise_error 'Connection to Sentry timed out'
+ rescue SocketError
+ raise_error 'Received SocketError when trying to connect to Sentry'
+ rescue OpenSSL::SSL::SSLError
+ raise_error 'Sentry returned invalid SSL data'
+ rescue Errno::ECONNREFUSED
+ raise_error 'Connection refused'
+ rescue => e
+ Gitlab::Sentry.track_acceptable_exception(e)
+ raise_error "Sentry request failed due to #{e.class}"
+ end
+
def handle_response(response)
unless response.code == 200
- raise Client::Error, "Sentry response status code: #{response.code}"
+ raise_error "Sentry response status code: #{response.code}"
end
response
end
+ def raise_error(message)
+ raise Client::Error, message
+ end
+
def projects_api_url
projects_url = URI(@url)
projects_url.path = '/api/0/projects/'
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 fcbd34a05d5..aa116a44254 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16,9 +16,15 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+msgid " Please sign in."
+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 +132,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 +144,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 +244,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 +267,9 @@ msgid_plural "%d pipelines"
msgstr[0] ""
msgstr[1] ""
+msgid "1 week"
+msgstr ""
+
msgid "1st contribution!"
msgstr ""
@@ -261,6 +279,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 +303,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 +345,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 ""
@@ -327,6 +354,12 @@ msgstr ""
msgid "A member of GitLab's abuse team will review your report as soon as possible."
msgstr ""
+msgid "A merge commit is created for every merge, and merging is allowed as long as there are no conflicts."
+msgstr ""
+
+msgid "A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. This way you could make sure that if this merge request would build, after merging to target branch it would also build."
+msgstr ""
+
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr ""
@@ -351,6 +384,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
+msgid "API Help"
+msgstr ""
+
msgid "About GitLab"
msgstr ""
@@ -375,6 +411,9 @@ msgstr ""
msgid "Access Tokens"
msgstr ""
+msgid "Access denied for your LDAP account."
+msgstr ""
+
msgid "Access denied! Please verify you can add deploy keys to this repository."
msgstr ""
@@ -447,6 +486,9 @@ msgstr ""
msgid "Add license"
msgstr ""
+msgid "Add list"
+msgstr ""
+
msgid "Add new application"
msgstr ""
@@ -480,6 +522,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 ""
@@ -969,12 +1014,18 @@ msgstr ""
msgid "Authentication Log"
msgstr ""
+msgid "Authentication failed: %{error_message}"
+msgstr ""
+
msgid "Authentication log"
msgstr ""
msgid "Authentication method"
msgstr ""
+msgid "Authentication method updated"
+msgstr ""
+
msgid "Authentication via U2F device failed."
msgstr ""
@@ -993,6 +1044,9 @@ msgstr ""
msgid "Authorize %{link_to_client} to use your account?"
msgstr ""
+msgid "Authorized %{new_chat_name}"
+msgstr ""
+
msgid "Authorized At"
msgstr ""
@@ -1035,6 +1089,9 @@ msgstr ""
msgid "Automatically marked as default internal user"
msgstr ""
+msgid "Automatically resolve merge request diff discussions when they become outdated"
+msgstr ""
+
msgid "Automatically resolved"
msgstr ""
@@ -1425,6 +1482,12 @@ msgstr ""
msgid "Cannot be merged automatically"
msgstr ""
+msgid "Cannot create the abuse report. The user has been deleted."
+msgstr ""
+
+msgid "Cannot create the abuse report. This user has been blocked."
+msgstr ""
+
msgid "Cannot modify managed Kubernetes cluster"
msgstr ""
@@ -1506,9 +1569,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 +1590,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 +1680,9 @@ msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
+msgid "CiVariables|This variable will not be masked"
+msgstr ""
+
msgid "CiVariable|* (All environments)"
msgstr ""
@@ -1629,9 +1692,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 +1779,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 +1842,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 +1866,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 +2058,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 +2073,6 @@ msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
-msgid "ClusterIntegration|Project namespace"
-msgstr ""
-
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -2064,7 +2139,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 +2175,6 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Token"
-msgstr ""
-
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
@@ -2471,12 +2546,18 @@ msgstr ""
msgid "Copy token to clipboard"
msgstr ""
+msgid "Could not authorize chat nickname. Try again!"
+msgstr ""
+
msgid "Could not connect to FogBugz, check your URL"
msgstr ""
msgid "Could not create Wiki Repository at this time. Please try again later."
msgstr ""
+msgid "Could not delete chat nickname %{chat_name}."
+msgstr ""
+
msgid "Could not remove the trigger."
msgstr ""
@@ -2486,6 +2567,9 @@ msgstr ""
msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
+msgid "Could not revoke personal access token %{personal_access_token_name}."
+msgstr ""
+
msgid "Coverage"
msgstr ""
@@ -2498,10 +2582,10 @@ msgstr ""
msgid "Create New Domain"
msgstr ""
-msgid "Create a new branch"
+msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr ""
-msgid "Create a new branch and merge request"
+msgid "Create a new branch"
msgstr ""
msgid "Create a new issue"
@@ -2753,6 +2837,12 @@ msgstr ""
msgid "Deleted"
msgstr ""
+msgid "Deleted chat nickname: %{chat_name}!"
+msgstr ""
+
+msgid "Denied authorization of chat nickname %{user_name}."
+msgstr ""
+
msgid "Deny"
msgstr ""
@@ -3004,19 +3094,10 @@ msgstr ""
msgid "Download asset"
msgstr ""
-msgid "Download tar"
-msgstr ""
-
-msgid "Download tar.bz2"
-msgstr ""
-
-msgid "Download tar.gz"
-msgstr ""
-
-msgid "Download zip"
+msgid "Download source code"
msgstr ""
-msgid "DownloadArtifacts|Download"
+msgid "Download this directory"
msgstr ""
msgid "DownloadCommit|Email Patches"
@@ -3166,6 +3247,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 +3265,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 +3649,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
+msgid "Failed to create resources"
+msgstr ""
+
msgid "Failed to deploy to"
msgstr ""
@@ -3595,6 +3679,15 @@ msgstr ""
msgid "Failed to remove user key."
msgstr ""
+msgid "Failed to save new settings"
+msgstr ""
+
+msgid "Failed to save preferences (%{error_message})."
+msgstr ""
+
+msgid "Failed to save preferences."
+msgstr ""
+
msgid "Failed to update issues, please try again."
msgstr ""
@@ -3604,6 +3697,9 @@ msgstr ""
msgid "Failure"
msgstr ""
+msgid "Fast-forward merge"
+msgstr ""
+
msgid "Fast-forward merge without a merge commit"
msgstr ""
@@ -4529,6 +4625,9 @@ msgstr ""
msgid "Job"
msgstr ""
+msgid "Job Failed #%{build_id}"
+msgstr ""
+
msgid "Job ID"
msgstr ""
@@ -4589,6 +4688,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 +4876,9 @@ msgstr ""
msgid "Leave"
msgstr ""
+msgid "Leave edit mode? All unsaved changes will be lost."
+msgstr ""
+
msgid "Leave group"
msgstr ""
@@ -4923,6 +5028,9 @@ msgstr ""
msgid "Markdown"
msgstr ""
+msgid "Markdown Help"
+msgstr ""
+
msgid "Markdown enabled"
msgstr ""
@@ -4965,9 +5073,15 @@ msgstr ""
msgid "Merge Requests"
msgstr ""
+msgid "Merge commit"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
+msgid "Merge commit with semi-linear history"
+msgstr ""
+
msgid "Merge events"
msgstr ""
@@ -4977,6 +5091,9 @@ msgstr ""
msgid "Merge in progress"
msgstr ""
+msgid "Merge method"
+msgstr ""
+
msgid "Merge request"
msgstr ""
@@ -5079,6 +5196,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,12 +5516,18 @@ msgstr ""
msgid "No files found."
msgstr ""
+msgid "No job trace"
+msgstr ""
+
msgid "No labels with such name or description"
msgstr ""
msgid "No license. All rights reserved"
msgstr ""
+msgid "No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. When fast-forward merge is not possible, the user is given the option to rebase."
+msgstr ""
+
msgid "No merge requests found"
msgstr ""
@@ -5459,6 +5588,9 @@ msgstr ""
msgid "Not now"
msgstr ""
+msgid "Not ready yet. Try again later."
+msgstr ""
+
msgid "Not started"
msgstr ""
@@ -5587,6 +5719,12 @@ msgstr ""
msgid "Only admins"
msgstr ""
+msgid "Only allow merge requests to be merged if all discussions are resolved"
+msgstr ""
+
+msgid "Only allow merge requests to be merged if the pipeline succeeds"
+msgstr ""
+
msgid "Only mirror protected branches"
msgstr ""
@@ -5617,6 +5755,9 @@ msgstr ""
msgid "Open in Xcode"
msgstr ""
+msgid "Open raw"
+msgstr ""
+
msgid "Open sidebar"
msgstr ""
@@ -5704,6 +5845,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Password authentication is unavailable."
+msgstr ""
+
msgid "Past due"
msgstr ""
@@ -5740,6 +5884,9 @@ msgstr ""
msgid "Permissions"
msgstr ""
+msgid "Permissions Help"
+msgstr ""
+
msgid "Permissions, LFS, 2FA"
msgstr ""
@@ -5764,6 +5911,9 @@ msgstr ""
msgid "Pipeline triggers"
msgstr ""
+msgid "Pipeline: %{status}"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -5833,6 +5983,9 @@ msgstr ""
msgid "Pipelines for last year"
msgstr ""
+msgid "Pipelines need to be configured to enable this feature."
+msgstr ""
+
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
@@ -5881,12 +6034,6 @@ msgstr ""
msgid "Pipeline|Coverage"
msgstr ""
-msgid "Pipeline|Create for"
-msgstr ""
-
-msgid "Pipeline|Create pipeline"
-msgstr ""
-
msgid "Pipeline|Duration"
msgstr ""
@@ -5899,6 +6046,9 @@ msgstr ""
msgid "Pipeline|Run Pipeline"
msgstr ""
+msgid "Pipeline|Run for"
+msgstr ""
+
msgid "Pipeline|Search branches"
msgstr ""
@@ -5965,12 +6115,18 @@ msgstr ""
msgid "Please choose a group URL with no special characters."
msgstr ""
+msgid "Please complete your profile with email address"
+msgstr ""
+
msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
msgstr ""
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr ""
+msgid "Please create a password for your new account."
+msgstr ""
+
msgid "Please create a username with only alphanumeric characters."
msgstr ""
@@ -6004,6 +6160,9 @@ msgstr ""
msgid "Preferences"
msgstr ""
+msgid "Preferences saved."
+msgstr ""
+
msgid "Preferences|Navigation theme"
msgstr ""
@@ -6016,9 +6175,15 @@ msgstr ""
msgid "Preview"
msgstr ""
+msgid "Preview changes"
+msgstr ""
+
msgid "Preview payload"
msgstr ""
+msgid "Previous Artifacts"
+msgstr ""
+
msgid "Prioritize"
msgstr ""
@@ -6190,6 +6355,9 @@ msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr ""
+msgid "Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
+msgstr ""
+
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr ""
@@ -6334,9 +6502,6 @@ msgstr ""
msgid "Project avatar"
msgstr ""
-msgid "Project avatar in repository: %{link}"
-msgstr ""
-
msgid "Project details"
msgstr ""
@@ -6373,6 +6538,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 +6724,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 +6757,9 @@ msgstr ""
msgid "README"
msgstr ""
+msgid "Rake Tasks Help"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -6669,6 +6855,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 +6879,9 @@ msgstr ""
msgid "Reopen milestone"
msgstr ""
+msgid "Replace"
+msgstr ""
+
msgid "Reply to comment"
msgstr ""
@@ -6822,6 +7017,9 @@ msgstr ""
msgid "Retry this job"
msgstr ""
+msgid "Retry this job in order to create the necessary resources."
+msgstr ""
+
msgid "Retry verification"
msgstr ""
@@ -6854,6 +7052,9 @@ msgstr ""
msgid "Revoked impersonation token %{token_name}!"
msgstr ""
+msgid "Revoked personal access token %{personal_access_token_name}!"
+msgstr ""
+
msgid "Run untagged jobs"
msgstr ""
@@ -6911,6 +7112,9 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "SSH Keys Help"
+msgstr ""
+
msgid "SSH host keys"
msgstr ""
@@ -6965,6 +7169,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 +7220,9 @@ msgstr ""
msgid "Search users"
msgstr ""
+msgid "Search your projects"
+msgstr ""
+
msgid "SearchAutocomplete|All GitLab"
msgstr ""
@@ -7037,6 +7247,9 @@ msgstr ""
msgid "SearchAutocomplete|in this project"
msgstr ""
+msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\""
+msgstr ""
+
msgid "Secret"
msgstr ""
@@ -7127,9 +7340,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 +7373,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 ""
@@ -7280,6 +7505,9 @@ msgstr ""
msgid "Show latest version"
msgstr ""
+msgid "Show link to create/view merge request when pushing from the command line"
+msgstr ""
+
msgid "Show parent pages"
msgstr ""
@@ -7315,6 +7543,12 @@ msgstr ""
msgid "Sign-up restrictions"
msgstr ""
+msgid "Signing in using %{label} has been disabled"
+msgstr ""
+
+msgid "Signing in using your %{label} account without a pre-existing GitLab account is not allowed."
+msgstr ""
+
msgid "Similar issues"
msgstr ""
@@ -7387,6 +7621,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 +7642,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 +7843,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 ""
@@ -7702,6 +7951,9 @@ msgstr ""
msgid "Successfully confirmed"
msgstr ""
+msgid "Successfully deleted U2F device."
+msgstr ""
+
msgid "Successfully removed email."
msgstr ""
@@ -7732,6 +7984,9 @@ msgstr ""
msgid "System Hooks"
msgstr ""
+msgid "System Hooks Help"
+msgstr ""
+
msgid "System Info"
msgstr ""
@@ -7864,6 +8119,14 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "Thank you for your report. A GitLab administrator will look into it shortly."
+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,12 +8169,27 @@ 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 ""
msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
+msgid "The invitation could not be accepted."
+msgstr ""
+
+msgid "The invitation could not be declined."
+msgstr ""
+
msgid "The invitation has already been accepted."
msgstr ""
@@ -7984,6 +8262,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 +8523,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 ""
@@ -8278,9 +8565,6 @@ msgstr ""
msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
msgstr ""
-msgid "This source diff could not be displayed because it is too large."
-msgstr ""
-
msgid "This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like \"1 hour\". Values without specification represent seconds."
msgstr ""
@@ -8353,9 +8637,6 @@ msgstr ""
msgid "Timeago|%s months remaining"
msgstr ""
-msgid "Timeago|%s seconds ago"
-msgstr ""
-
msgid "Timeago|%s seconds remaining"
msgstr ""
@@ -8840,6 +9121,9 @@ msgstr ""
msgid "User map"
msgstr ""
+msgid "User settings"
+msgstr ""
+
msgid "User was successfully created."
msgstr ""
@@ -8948,6 +9232,9 @@ msgstr ""
msgid "Value"
msgstr ""
+msgid "Variables"
+msgstr ""
+
msgid "Various container registry settings."
msgstr ""
@@ -8990,6 +9277,9 @@ msgstr ""
msgid "View it on GitLab"
msgstr ""
+msgid "View job trace"
+msgstr ""
+
msgid "View jobs"
msgstr ""
@@ -9065,12 +9355,23 @@ 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 fast-forward merge is not possible, the user is given the option to rebase."
+msgstr ""
+
+msgid "When this merge request is accepted"
+msgid_plural "When these merge requests are accepted"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "When:"
msgstr ""
@@ -9197,6 +9498,9 @@ msgstr ""
msgid "Wiki|Create page"
msgstr ""
+msgid "Wiki|Created date"
+msgstr ""
+
msgid "Wiki|Edit Page"
msgstr ""
@@ -9215,6 +9519,9 @@ msgstr ""
msgid "Wiki|Pages"
msgstr ""
+msgid "Wiki|Title"
+msgstr ""
+
msgid "Wiki|Wiki Pages"
msgstr ""
@@ -9224,6 +9531,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
+msgid "Workflow Help"
+msgstr ""
+
msgid "Write"
msgstr ""
@@ -9236,6 +9546,9 @@ msgstr ""
msgid "Write milestone description..."
msgstr ""
+msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
+msgstr ""
+
msgid "Yes"
msgstr ""
@@ -9362,6 +9675,15 @@ msgstr ""
msgid "You don't have any deployments right now."
msgstr ""
+msgid "You have been granted %{member_human_access} access to %{label}."
+msgstr ""
+
+msgid "You have been unsubscribed from this thread."
+msgstr ""
+
+msgid "You have declined the invitation to join %{label}."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -9428,6 +9750,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 ""
@@ -9512,6 +9840,12 @@ msgstr ""
msgid "Your name"
msgstr ""
+msgid "Your new personal access token has been created."
+msgstr ""
+
+msgid "Your password reset token has expired."
+msgstr ""
+
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr ""
@@ -9930,6 +10264,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 29a3b3d0459..115f6fd1e8d 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",
@@ -16,12 +17,12 @@
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
"prettier-all-save": "node ./scripts/frontend/prettier.js save-all",
- "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* --custom-formatter node_modules/stylelint-error-string-formatter",
+ "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* ee/app/assets/stylesheets/**/*.* !**/vendors/** --custom-formatter node_modules/stylelint-error-string-formatter",
"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",
@@ -32,8 +33,8 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.9.0",
- "@gitlab/svgs": "^1.58.0",
- "@gitlab/ui": "^3.0.0",
+ "@gitlab/svgs": "^1.59.0",
+ "@gitlab/ui": "^3.2.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-upload-client": "^10.0.0",
@@ -56,11 +57,13 @@
"d3-array": "^1.2.1",
"d3-axis": "^1.0.8",
"d3-brush": "^1.0.4",
+ "d3-ease": "^1.0.3",
"d3-scale": "^1.0.7",
"d3-selection": "^1.2.0",
"d3-shape": "^1.2.0",
"d3-time": "^1.0.8",
"d3-time-format": "^2.1.1",
+ "d3-transition": "^1.1.1",
"dateformat": "^3.0.3",
"deckar01-task_list": "^2.2.0",
"diff": "^3.4.0",
@@ -107,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",
@@ -136,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/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index bb1f775da75..0971e551db1 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -40,7 +40,7 @@ module QA
Resource::CiVariable.fabricate! do |resource|
resource.project = @project
resource.key = 'K8S_SECRET_OPTIONAL_MESSAGE'
- resource.value = 'You can see this application secret'
+ resource.value = 'you_can_see_this_variable'
end
# Connect K8s cluster
@@ -99,7 +99,7 @@ module QA
Page::Project::Operations::Environments::Show.perform do |show|
show.view_deployment do
expect(page).to have_content('Hello World!')
- expect(page).to have_content('You can see this application secret')
+ expect(page).to have_content('you_can_see_this_variable')
end
end
end
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/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index be13c3fb683..a235fddabca 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -34,7 +34,7 @@ RSpec.configure do |config|
config.display_try_failure_messages = true
config.around do |example|
- retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 3
+ retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2
example.run_with_retry retry: retry_times
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..485e3e21c4d 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -10,6 +10,8 @@ describe Projects::BlobController do
context 'with file path' do
before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
get(:show,
params: {
namespace_id: project.namespace,
@@ -285,7 +287,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..168c0168bba 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,13 +412,50 @@ 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)
expect(json_response['data']).to eq({})
expect(json_response['last_update']).to eq(42)
end
+
+ context 'when time params are provided' do
+ it 'returns a metrics JSON document' do
+ additional_metrics(start: '1554702993.5398998', end: '1554717396.996232')
+
+ expect(response).to be_ok
+ expect(json_response['success']).to be(true)
+ expect(json_response['data']).to eq({})
+ expect(json_response['last_update']).to eq(42)
+ end
+ 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
@@ -500,4 +537,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/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 62f2af947e4..0d0fa5d9f45 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -44,11 +44,15 @@ describe Projects::RefsController do
end
it 'renders JS' do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
xhr_get(:js)
expect(response).to be_success
end
it 'renders JSON' do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
xhr_get(:json)
expect(response).to be_success
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/tags/releases_controller_spec.rb b/spec/controllers/projects/tags/releases_controller_spec.rb
index 29f206c574b..66eff4844c2 100644
--- a/spec/controllers/projects/tags/releases_controller_spec.rb
+++ b/spec/controllers/projects/tags/releases_controller_spec.rb
@@ -18,40 +18,85 @@ describe Projects::Tags::ReleasesController do
tag_id = release.tag
project.releases.destroy_all # rubocop: disable DestroyAll
- get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
+ response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).not_to be_persisted
+ expect(response).to have_http_status(:ok)
end
it 'retrieves an existing release' do
- get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag }
+ response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).to be_persisted
+ expect(response).to have_http_status(:ok)
end
end
describe 'PUT #update' do
it 'updates release note description' do
- update_release('description updated')
+ response = update_release(release.tag, "description updated")
- release = project.releases.find_by_tag(tag)
+ release = project.releases.find_by(tag: tag)
expect(release.description).to eq("description updated")
+ expect(response).to have_http_status(:found)
end
- it 'deletes release note when description is null' do
- expect { update_release('') }.to change(project.releases, :count).by(-1)
+ it 'creates a release if one does not exist' do
+ tag_without_release = create_new_tag
+
+ expect do
+ update_release(tag_without_release.name, "a new release")
+ end.to change { project.releases.count }.by(1)
+
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'sets the release name, sha, and author for a new release' do
+ tag_without_release = create_new_tag
+
+ response = update_release(tag_without_release.name, "a new release")
+
+ release = project.releases.find_by(tag: tag_without_release.name)
+ expect(release.name).to eq(tag_without_release.name)
+ expect(release.sha).to eq(tag_without_release.target_commit.sha)
+ expect(release.author.id).to eq(user.id)
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'deletes release when description is empty' do
+ initial_releases_count = project.releases.count
+
+ response = update_release(release.tag, "")
+
+ expect(initial_releases_count).to eq(1)
+ expect(project.releases.count).to eq(0)
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'does nothing when description is empty and the tag does not have a release' do
+ tag_without_release = create_new_tag
+
+ expect do
+ update_release(tag_without_release.name, "")
+ end.not_to change { project.releases.count }
+
+ expect(response).to have_http_status(:found)
end
end
- def update_release(description)
+ def create_new_tag
+ project.repository.add_tag(user, 'mytag', 'master')
+ end
+
+ def update_release(tag_id, description)
put :update, params: {
namespace_id: project.namespace.to_param,
project_id: project,
- tag_id: release.tag,
+ tag_id: tag_id,
release: { description: description }
}
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index b15a2bc84a5..78201498eaa 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -16,6 +16,8 @@ describe Projects::TreeController do
render_views
before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
get(:show,
params: {
namespace_id: project.namespace.to_param,
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..b358c6b9c34 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Issue Boards', :js do
include BoardHelpers
+ include FilteredSearchHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -129,6 +130,7 @@ describe 'Issue Boards', :js do
click_link 'Unassigned'
end
+ close_dropdown_menu_if_visible
wait_for_requests
expect(page).to have_content('No assignee')
@@ -343,6 +345,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/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index a0ae6720a9f..a19101366a0 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -13,7 +13,7 @@ describe 'Blob shortcuts', :js do
end
shared_examples "quotes the selected text" do
- it "quotes the selected text" do
+ it "quotes the selected text", :quarantine do
select_element('.note-text')
find('body').native.send_key('r')
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..7a6f76cb382 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)
@@ -139,7 +139,7 @@ describe 'Dropdown milestone', :js do
expect_filtered_search_input_empty
end
- it 'fills in the milestone name when the milestone is partially filled' do
+ it 'fills in the milestone name when the milestone is partially filled', :quarantine do
filtered_search.send_keys('v')
click_milestone(milestone.title)
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..68af8303c2f 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -42,7 +42,8 @@ describe 'Issues > User uses quick actions', :js do
describe 'issue-only commands' do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
before do
project.add_maintainer(user)
@@ -55,123 +56,11 @@ describe 'Issues > User uses quick actions', :js do
wait_for_requests
end
- describe 'adding a due date from note' do
- let(:issue) { create(:issue, project: project) }
-
- it_behaves_like 'due quick action available and date can be added'
-
- 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 'due quick action not available'
- 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) }
-
- it 'does not recognize the command nor create a note' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- end
- end
-
- describe 'mark issue as duplicate' 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 a duplicate' do
- add_note("/duplicate ##{original_issue.to_reference}")
-
- expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
- expect(page).to have_content 'Commands applied'
- expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
-
- expect(issue.reload).to be_closed
- 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 a duplicate' do
- add_note("/duplicate ##{original_issue.to_reference}")
-
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
-
- expect(issue.reload).to be_open
- end
- 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
+ it_behaves_like 'confidential quick action'
+ it_behaves_like 'remove_due_date quick action'
+ it_behaves_like 'duplicate quick action'
+ it_behaves_like 'create_merge_request quick action'
+ it_behaves_like 'due quick action'
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
@@ -292,63 +181,5 @@ describe 'Issues > User uses quick actions', :js do
end
end
end
-
- describe 'create a merge request starting from an issue' do
- let(:project) { create(:project, :public, :repository) }
- let(:issue) { create(:issue, project: project) }
-
- def expect_mr_quickaction(success)
- expect(page).to have_content 'Commands applied'
-
- if success
- expect(page).to have_content 'created merge request'
- else
- expect(page).not_to have_content 'created merge request'
- end
- end
-
- it "doesn't create a merge request when the branch name is invalid" do
- add_note("/create_merge_request invalid branch name")
-
- wait_for_requests
-
- expect_mr_quickaction(false)
- end
-
- it "doesn't create a merge request when a branch with that name already exists" do
- add_note("/create_merge_request feature")
-
- wait_for_requests
-
- expect_mr_quickaction(false)
- end
-
- it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
- add_note("/create_merge_request")
-
- wait_for_requests
-
- expect_mr_quickaction(true)
-
- created_mr = project.merge_requests.last
- expect(created_mr.source_branch).to eq(issue.to_branch_name)
-
- visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
- end
-
- it 'creates a merge request using the given branch name' do
- branch_name = '1-feature'
- add_note("/create_merge_request #{branch_name}")
-
- expect_mr_quickaction(true)
-
- created_mr = project.merge_requests.last
- expect(created_mr.source_branch).to eq(branch_name)
-
- visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
- end
- end
end
end
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 7c31e67a7fa..489651fea15 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -21,7 +21,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do
end
shared_examples 'assigning labels from sidebar' do
- it 'can assign all ancestors labels' do
+ it 'can assign all ancestors labels', :quarantine do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
page.within('.block.labels') do
find('.edit-link').click
@@ -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_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index c837a6752f9..65de0afae0c 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -113,7 +113,7 @@ describe 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes', :quarantine do
indicator = find('.js-image-badge', match: :first)
badge = find('.user-avatar-link .badge', match: :first)
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..8d308729f62 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -56,197 +56,8 @@ describe 'Merge request > User uses quick actions', :js do
project.add_maintainer(user)
end
- describe 'toggling the WIP prefix in the title from note' do
- context 'when the current user can toggle the WIP prefix' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
- end
-
- it 'adds the WIP: prefix to the title' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- expect(page).to have_content 'Commands applied'
-
- expect(merge_request.reload.work_in_progress?).to eq true
- end
-
- it 'removes the WIP: prefix from the title' do
- merge_request.title = merge_request.wip_title
- merge_request.save
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- expect(page).to have_content 'Commands applied'
-
- expect(merge_request.reload.work_in_progress?).to eq false
- end
- end
-
- context 'when the current user cannot toggle the WIP prefix' do
- before do
- project.add_guest(guest)
- sign_in(guest)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not change the WIP prefix' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- expect(page).not_to have_content 'Commands applied'
-
- expect(merge_request.reload.work_in_progress?).to eq false
- 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
+ it_behaves_like 'merge quick action'
+ it_behaves_like 'target_branch quick action'
+ it_behaves_like 'wip quick action'
end
end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 9909bfb5904..1b3718968b9 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -63,7 +63,7 @@ describe 'User visits the profile preferences page' do
end
describe 'User changes their language', :js do
- it 'creates a flash message' do
+ it 'creates a flash message', :quarantine do
select2('en', from: '#user_preferred_language')
click_button 'Save'
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/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 84de6858d5f..b1c2bab08c0 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -93,11 +93,13 @@ describe 'Projects > Settings > User manages merge request settings' do
it 'when unchecked sets :printing_merge_request_link_enabled to false' do
uncheck('project_printing_merge_request_link_enabled')
within('.merge-request-settings-form') do
+ find('.qa-save-merge-request-changes')
click_on('Save changes')
end
- # Wait for save to complete and page to reload
+ find('.flash-notice')
checkbox = find_field('project_printing_merge_request_link_enabled')
+
expect(checkbox).not_to be_checked
project.reload
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_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index efb7b01f5ad..bcbba6f14da 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -43,7 +43,7 @@ describe "User creates wiki page" do
expect(page).to have_content("Create Page")
end
- it "shows non-escaped link in the pages list", :js do
+ it "shows non-escaped link in the pages list", :js, :quarantine do
fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
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/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql
new file mode 100644
index 00000000000..7b712068fcd
--- /dev/null
+++ b/spec/fixtures/api/graphql/introspection.graphql
@@ -0,0 +1,92 @@
+# pulled from GraphiQL query
+query IntrospectionQuery {
+ __schema {
+ queryType { name }
+ mutationType { name }
+ subscriptionType { name }
+ types {
+ ...FullType
+ }
+ directives {
+ name
+ description
+ locations
+ args {
+ ...InputValue
+ }
+ }
+ }
+}
+
+fragment FullType on __Type {
+ kind
+ name
+ description
+ fields(includeDeprecated: true) {
+ name
+ description
+ args {
+ ...InputValue
+ }
+ type {
+ ...TypeRef
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields {
+ ...InputValue
+ }
+ interfaces {
+ ...TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ description
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ...TypeRef
+ }
+}
+
+fragment InputValue on __InputValue {
+ name
+ description
+ type { ...TypeRef }
+ defaultValue
+}
+
+fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
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/artifact.json b/spec/fixtures/api/schemas/public_api/v4/artifact.json
new file mode 100644
index 00000000000..9df957b1498
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/artifact.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required": [
+ "file_type",
+ "size",
+ "filename",
+ "file_format"
+ ],
+ "properties": {
+ "file_type": { "type": "string"},
+ "size": { "type": "integer"},
+ "filename": { "type": "string"},
+ "file_format": { "type": "string"}
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact_file.json b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json
new file mode 100644
index 00000000000..4017e6bdabc
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required": [
+ "filename",
+ "size"
+ ],
+ "properties": {
+ "filename": { "type": "string"},
+ "size": { "type": "integer"}
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json
new file mode 100644
index 00000000000..3af2dc27d55
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json
@@ -0,0 +1,32 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "iid",
+ "ref",
+ "sha",
+ "created_at",
+ "user",
+ "deployable"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "ref": { "type": "string" },
+ "sha": { "type": "string" },
+ "created_at": { "type": "string" },
+ "user": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "user/basic.json" }
+ ]
+ },
+ "deployable": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "job.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json
new file mode 100644
index 00000000000..242e90fb7ac
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/environment.json
@@ -0,0 +1,23 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "slug",
+ "external_url",
+ "last_deployment"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "slug": { "type": "string" },
+ "external_url": { "$ref": "../../types/nullable_string.json" },
+ "last_deployment": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "deployment.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json
new file mode 100644
index 00000000000..454935422a0
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/job.json
@@ -0,0 +1,63 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "status",
+ "stage",
+ "name",
+ "ref",
+ "tag",
+ "coverage",
+ "created_at",
+ "started_at",
+ "finished_at",
+ "duration",
+ "user",
+ "commit",
+ "pipeline",
+ "web_url",
+ "artifacts",
+ "artifacts_expire_at",
+ "runner"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "status": { "type": "string" },
+ "stage": { "type": "string" },
+ "name": { "type": "string" },
+ "ref": { "type": "string" },
+ "tag": { "type": "boolean" },
+ "coverage": { "type": ["number", "null"] },
+ "created_at": { "type": "string" },
+ "started_at": { "type": ["null", "string"] },
+ "finished_at": { "type": ["null", "string"] },
+ "duration": { "type": ["null", "number"] },
+ "user": { "$ref": "user/basic.json" },
+ "commit": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "commit/basic.json" }
+ ]
+ },
+ "pipeline": { "$ref": "pipeline/basic.json" },
+ "web_url": { "type": "string" },
+ "artifacts": {
+ "type": "array",
+ "items": { "$ref": "artifact.json" }
+ },
+ "artifacts_file": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "artifact_file.json" }
+ ]
+ },
+ "artifacts_expire_at": { "type": ["null", "string"] },
+ "runner": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "runner.json" }
+ ]
+ }
+ },
+ "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/public_api/v4/runner.json b/spec/fixtures/api/schemas/public_api/v4/runner.json
new file mode 100644
index 00000000000..d97d74a93f2
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/runner.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "description",
+ "ip_address",
+ "active",
+ "is_shared",
+ "name",
+ "online",
+ "status"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "description": { "type": "string" },
+ "ip_address": { "type": "string" },
+ "active": { "type": "boolean" },
+ "is_shared": { "type": "boolean" },
+ "name": { "type": ["null", "string"] },
+ "online": { "type": "boolean" },
+ "status": { "type": "string" }
+ },
+ "additionalProperties": false
+}
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/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 054dc27cda6..0e6ca11c1f8 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -12,3 +12,7 @@ globals:
loadFixtures: false
preloadFixtures: false
setFixtures: false
+rules:
+ jest/no-identical-title: error
+ jest/no-focused-tests: error
+ jest/no-jasmine-globals: error
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 0d3dcc29f22..eea7bd87257 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -5,19 +5,41 @@ import {
APPLICATION_STATUS,
INGRESS_DOMAIN_SUFFIX,
} from '~/clusters/constants';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { loadHTMLFixture } from 'helpers/fixtures';
+import { setTestTimeout } from 'helpers/timeout';
+import $ from 'jquery';
describe('Clusters', () => {
+ setTestTimeout(500);
+
let cluster;
- preloadFixtures('clusters/show_cluster.html');
+ let mock;
+
+ const mockGetClusterStatusRequest = () => {
+ const { statusPath } = document.querySelector('.js-edit-cluster-form').dataset;
+
+ mock = new MockAdapter(axios);
+
+ mock.onGet(statusPath).reply(200);
+ };
+
+ beforeEach(() => {
+ loadHTMLFixture('clusters/show_cluster.html');
+ });
+
+ beforeEach(() => {
+ mockGetClusterStatusRequest();
+ });
beforeEach(() => {
- loadFixtures('clusters/show_cluster.html');
cluster = new Clusters();
});
afterEach(() => {
cluster.destroy();
+ mock.restore();
});
describe('toggle', () => {
@@ -29,16 +51,13 @@ describe('Clusters', () => {
'.js-cluster-enable-toggle-area .js-project-feature-toggle-input',
);
- toggleButton.click();
-
- getSetTimeoutPromise()
- .then(() => {
- expect(toggleButton.classList).not.toContain('is-checked');
+ $(toggleInput).one('trigger-change', () => {
+ expect(toggleButton.classList).not.toContain('is-checked');
+ expect(toggleInput.getAttribute('value')).toEqual('false');
+ done();
+ });
- expect(toggleInput.getAttribute('value')).toEqual('false');
- })
- .then(done)
- .catch(done.fail);
+ toggleButton.click();
});
});
@@ -197,7 +216,7 @@ describe('Clusters', () => {
describe('installApplication', () => {
it('tries to install helm', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
@@ -209,7 +228,7 @@ describe('Clusters', () => {
});
it('tries to install ingress', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
@@ -221,7 +240,7 @@ describe('Clusters', () => {
});
it('tries to install runner', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
@@ -233,7 +252,7 @@ describe('Clusters', () => {
});
it('tries to install jupyter', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
cluster.installApplication({
@@ -248,35 +267,32 @@ describe('Clusters', () => {
});
});
- it('sets error request status when the request fails', done => {
- spyOn(cluster.service, 'installApplication').and.returnValue(
- Promise.reject(new Error('STUBBED ERROR')),
- );
+ it('sets error request status when the request fails', () => {
+ jest
+ .spyOn(cluster.service, 'installApplication')
+ .mockRejectedValueOnce(new Error('STUBBED ERROR'));
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
- cluster.installApplication({ id: 'helm' });
+ const promise = cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalled();
- getSetTimeoutPromise()
- .then(() => {
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
- expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
- })
- .then(done)
- .catch(done.fail);
+ return promise.then(() => {
+ expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
+ });
});
});
describe('handleSuccess', () => {
beforeEach(() => {
- spyOn(cluster.store, 'updateStateFromServer');
- spyOn(cluster, 'toggleIngressDomainHelpText');
- spyOn(cluster, 'checkForNewInstalls');
- spyOn(cluster, 'updateContainer');
+ jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
+ jest.spyOn(cluster, 'toggleIngressDomainHelpText').mockReturnThis();
+ jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
+ jest.spyOn(cluster, 'updateContainer').mockReturnThis();
cluster.handleSuccess({ data: {} });
});
@@ -300,9 +316,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 +353,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 +362,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/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index a2dd4e93daf..b28d0075d06 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import eventHub from '~/clusters/event_hub';
import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
describe('Application Row', () => {
@@ -160,7 +160,7 @@ describe('Application Row', () => {
});
it('clicking install button emits event', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
@@ -176,7 +176,7 @@ describe('Application Row', () => {
});
it('clicking install button when installApplicationRequestParams are provided emits event', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
@@ -193,7 +193,7 @@ describe('Application Row', () => {
});
it('clicking disabled install button emits nothing', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLING,
@@ -255,7 +255,7 @@ describe('Application Row', () => {
});
it('clicking upgrade button emits event', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATE_ERRORED,
@@ -271,7 +271,7 @@ describe('Application Row', () => {
});
it('clicking disabled upgrade button emits nothing', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 0f8153ad493..7c54a27d950 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
describe('Applications', () => {
@@ -314,7 +314,7 @@ describe('Applications', () => {
});
it('emits event when clicking Save changes button', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(Applications, props);
const saveButton = vm.$el.querySelector('.js-knative-save-domain-button');
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index b4d1bb710e0..b4d1bb710e0 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index 161722ec571..161722ec571 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 503af3920a8..67e5dc399ac 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -31,7 +31,7 @@ describe('ErrorTrackingList', () => {
actions = {
getErrorList: () => {},
startPolling: () => {},
- restartPolling: jasmine.createSpy('restartPolling'),
+ restartPolling: jest.fn().mockName('restartPolling'),
};
const state = {
diff --git a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
index ea7c146fa4f..0e62bc94517 100644
--- a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
+++ b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
@@ -8,7 +8,7 @@ describe('RecentSearchesServiceError', () => {
});
it('instantiates an instance of RecentSearchesServiceError and not an Error', () => {
- expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError));
+ expect(recentSearchesServiceError).toEqual(expect.any(RecentSearchesServiceError));
expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError');
});
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/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js
index af12ca15369..8596642eb7a 100644
--- a/spec/frontend/ide/lib/common/disposable_spec.js
+++ b/spec/frontend/ide/lib/common/disposable_spec.js
@@ -8,7 +8,7 @@ describe('Multi-file editor library disposable class', () => {
instance = new Disposable();
disposableClass = {
- dispose: jasmine.createSpy('dispose'),
+ dispose: jest.fn().mockName('dispose'),
};
});
diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js
index 57f3ac3d365..d9b088e2c12 100644
--- a/spec/frontend/ide/lib/diff/diff_spec.js
+++ b/spec/frontend/ide/lib/diff/diff_spec.js
@@ -9,60 +9,57 @@ describe('Multi-file editor library diff calculator', () => {
});
describe('modified', () => {
- it('', () => {
- const diff = computeDiff('123', '1234')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeUndefined();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeUndefined();
- expect(diff.lineNumber).toBe(2);
- });
+ it.each`
+ originalContent | newContent | lineNumber
+ ${'123'} | ${'1234'} | ${1}
+ ${'123\n123\n123'} | ${'123\n1234\n123'} | ${2}
+ `(
+ 'marks line $lineNumber as added and modified but not removed',
+ ({ originalContent, newContent, lineNumber }) => {
+ const diff = computeDiff(originalContent, newContent)[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(lineNumber);
+ },
+ );
});
describe('added', () => {
- it('', () => {
- const diff = computeDiff('123', '123\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeUndefined();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeUndefined();
- expect(diff.lineNumber).toBe(3);
- });
+ it.each`
+ originalContent | newContent | lineNumber
+ ${'123'} | ${'123\n123'} | ${1}
+ ${'123\n123\n123'} | ${'123\n123\n1234\n123'} | ${3}
+ `(
+ 'marks line $lineNumber as added but not modified and not removed',
+ ({ originalContent, newContent, lineNumber }) => {
+ const diff = computeDiff(originalContent, newContent)[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(lineNumber);
+ },
+ );
});
describe('removed', () => {
- it('', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.added).toBeUndefined();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeTruthy();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n123')[0];
-
- expect(diff.added).toBeUndefined();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeTruthy();
- expect(diff.lineNumber).toBe(2);
- });
+ it.each`
+ originalContent | newContent | lineNumber | modified
+ ${'123'} | ${''} | ${1} | ${undefined}
+ ${'123\n123\n123'} | ${'123\n123'} | ${2} | ${true}
+ `(
+ 'marks line $lineNumber as removed',
+ ({ originalContent, newContent, lineNumber, modified }) => {
+ const diff = computeDiff(originalContent, newContent)[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBe(modified);
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(lineNumber);
+ },
+ );
});
it('includes line number of change', () => {
diff --git a/spec/frontend/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js
index d149a883166..b07a583b7c8 100644
--- a/spec/frontend/ide/lib/editor_options_spec.js
+++ b/spec/frontend/ide/lib/editor_options_spec.js
@@ -2,7 +2,7 @@ import editorOptions from '~/ide/lib/editor_options';
describe('Multi-file editor library editor options', () => {
it('returns an array', () => {
- expect(editorOptions).toEqual(jasmine.any(Array));
+ expect(editorOptions).toEqual(expect.any(Array));
});
it('contains readOnly option', () => {
diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js
index fe791aa2b74..aa1fa0373db 100644
--- a/spec/frontend/ide/lib/files_spec.js
+++ b/spec/frontend/ide/lib/files_spec.js
@@ -1,5 +1,5 @@
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import { decorateFiles, splitParent } from '~/ide/lib/files';
+import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files';
import { decorateData } from '~/ide/stores/utils';
const TEST_BRANCH_ID = 'lorem-ipsum';
@@ -20,7 +20,7 @@ const createEntries = paths => {
id: path,
name,
path,
- url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${path}`),
+ url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${escapeFileUrl(path)}`),
type,
previewMode: viewerInformationForPath(path),
parentPath: parent,
@@ -28,7 +28,7 @@ const createEntries = paths => {
? parentEntry.url
: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}`),
}),
- tree: children.map(childName => jasmine.objectContaining({ name: childName })),
+ tree: children.map(childName => expect.objectContaining({ name: childName })),
};
return acc;
@@ -36,10 +36,10 @@ const createEntries = paths => {
const entries = paths.reduce(createEntry, {});
- // Wrap entries in jasmine.objectContaining.
+ // Wrap entries in expect.objectContaining.
// We couldn't do this earlier because we still need to select properties from parent entries.
return Object.keys(entries).reduce((acc, key) => {
- acc[key] = jasmine.objectContaining(entries[key]);
+ acc[key] = expect.objectContaining(entries[key]);
return acc;
}, {});
@@ -47,13 +47,14 @@ const createEntries = paths => {
describe('IDE lib decorate files', () => {
it('creates entries and treeList', () => {
- const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'README.md'];
+ const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'app/#weird#file?.txt', 'README.md'];
const expectedEntries = createEntries([
- { path: 'app', type: 'tree', children: ['assets', 'bugs.js'] },
+ { path: 'app', type: 'tree', children: ['assets', '#weird#file?.txt', 'bugs.js'] },
{ path: 'app/assets', type: 'tree', children: ['apples'] },
{ path: 'app/assets/apples', type: 'tree', children: ['foo.js'] },
{ path: 'app/assets/apples/foo.js', type: 'blob', children: [] },
{ path: 'app/bugs.js', type: 'blob', children: [] },
+ { path: 'app/#weird#file?.txt', type: 'blob', children: [] },
{ path: 'README.md', type: 'blob', children: [] },
]);
@@ -64,7 +65,7 @@ describe('IDE lib decorate files', () => {
});
// Here we test the keys and then each key/value individually because `expect(entries).toEqual(expectedEntries)`
- // was taking a very long time for some reason. Probably due to large objects and nested `jasmine.objectContaining`.
+ // was taking a very long time for some reason. Probably due to large objects and nested `expect.objectContaining`.
const entryKeys = Object.keys(entries);
expect(entryKeys).toEqual(Object.keys(expectedEntries));
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/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 1e0bc708c31..7e9aec84016 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -25,14 +25,14 @@ describe('Abuse Reports', () => {
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
- expect($longMessage.data('originalMessage')).toEqual(jasmine.anything());
+ expect($longMessage.data('originalMessage')).toEqual(expect.anything());
assertMaxLength($longMessage);
});
it('should not truncate short messages', () => {
const $shortMessage = findMessage('SHORT MESSAGE');
- expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything());
+ expect($shortMessage.data('originalMessage')).not.toEqual(expect.anything());
});
it('should allow clicking a truncated message to expand and collapse the full message', () => {
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
index efc338b36eb..6ac1e83829f 100644
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
@@ -13,7 +13,7 @@ describe('EmojiMenu', () => {
let dummyEmojiList;
beforeEach(() => {
- dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback');
+ dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback');
dummyEmojiList = {
glEmojiTag() {
return dummyEmojiTag;
@@ -75,19 +75,19 @@ describe('EmojiMenu', () => {
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'one',
- jasmine.anything(),
+ expect.anything(),
'mouseenter focus',
dummyToggleButtonSelector,
'mouseenter focus',
- jasmine.anything(),
+ expect.anything(),
);
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
- jasmine.anything(),
+ expect.anything(),
'click',
dummyToggleButtonSelector,
- jasmine.anything(),
+ expect.anything(),
);
});
@@ -96,10 +96,10 @@ describe('EmojiMenu', () => {
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
- jasmine.anything(),
+ expect.anything(),
'click',
`.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
- jasmine.anything(),
+ expect.anything(),
);
});
});
diff --git a/spec/frontend/serverless/components/area_spec.js b/spec/frontend/serverless/components/area_spec.js
new file mode 100644
index 00000000000..62005e1981a
--- /dev/null
+++ b/spec/frontend/serverless/components/area_spec.js
@@ -0,0 +1,122 @@
+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,
+ },
+ sync: false,
+ });
+ });
+
+ 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(() => {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
+ 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/frontend/serverless/components/environment_row_spec.js
index bdf7a714910..161a637dd75 100644
--- a/spec/javascripts/serverless/components/environment_row_spec.js
+++ b/spec/frontend/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 }, sync: false }).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/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js
new file mode 100644
index 00000000000..31348ff1194
--- /dev/null
+++ b/spec/frontend/serverless/components/function_details_spec.js
@@ -0,0 +1,117 @@
+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',
+ },
+ sync: false,
+ });
+
+ 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',
+ },
+ sync: false,
+ });
+
+ 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',
+ },
+ sync: false,
+ });
+
+ 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',
+ },
+ sync: false,
+ });
+
+ 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/frontend/serverless/components/function_row_spec.js
index 6933a8f6c87..414fdc5cd82 100644
--- a/spec/javascripts/serverless/components/function_row_spec.js
+++ b/spec/frontend/serverless/components/function_row_spec.js
@@ -1,11 +1,10 @@
-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 }, sync: false }).vm;
describe('functionRowComponent', () => {
it('Parses the function details correctly', () => {
@@ -13,10 +12,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 +21,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/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
new file mode 100644
index 00000000000..7af33ceaadc
--- /dev/null
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -0,0 +1,110 @@
+import Vuex from 'vuex';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import functionsComponent from '~/serverless/components/functions.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
+import { TEST_HOST } from 'helpers/test_constants';
+import { mockServerlessFunctions } from '../mock_data';
+
+describe('functionsComponent', () => {
+ const statusPath = `${TEST_HOST}/statusPath`;
+
+ let component;
+ let store;
+ let localVue;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(statusPath).reply(200);
+
+ localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ store = createStore();
+ });
+
+ afterEach(() => {
+ component.vm.$destroy();
+ axiosMock.restore();
+ });
+
+ it('should render empty state when Knative is not installed', () => {
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: false,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null);
+ });
+
+ it('should render a loading component', () => {
+ store.dispatch('requestFunctionsLoading');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null);
+ });
+
+ it('should render empty state when there is no function data', () => {
+ store.dispatch('receiveFunctionsNoDataSuccess');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(
+ component.vm.$el
+ .querySelector('.empty-state, .js-empty-state')
+ .classList.contains('js-empty-state'),
+ ).toBe(true);
+
+ expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual(
+ 'No functions available',
+ );
+ });
+
+ it('should render the functions list', () => {
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: 'clustersPath',
+ helpPath: 'helpPath',
+ statusPath,
+ },
+ sync: false,
+ });
+
+ component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
+
+ return component.vm.$nextTick().then(() => {
+ expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
new file mode 100644
index 00000000000..d0df6125290
--- /dev/null
+++ b/spec/frontend/serverless/components/missing_prometheus_spec.js
@@ -0,0 +1,38 @@
+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,
+ },
+ sync: false,
+ }).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/frontend/serverless/components/pod_box_spec.js b/spec/frontend/serverless/components/pod_box_spec.js
new file mode 100644
index 00000000000..d82825d8f62
--- /dev/null
+++ b/spec/frontend/serverless/components/pod_box_spec.js
@@ -0,0 +1,23 @@
+import podBoxComponent from '~/serverless/components/pod_box.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = count =>
+ shallowMount(podBoxComponent, {
+ propsData: {
+ count,
+ },
+ sync: false,
+ }).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/frontend/serverless/components/url_spec.js
index 21a879a49bb..d05a9bba103 100644
--- a/spec/javascripts/serverless/components/url_spec.js
+++ b/spec/frontend/serverless/components/url_spec.js
@@ -1,15 +1,14 @@
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,
+ },
+ sync: false,
+ }).vm;
describe('urlComponent', () => {
it('should render correctly', () => {
@@ -17,9 +16,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/frontend/serverless/mock_data.js
index ecd393b174c..a2c18616324 100644
--- a/spec/javascripts/serverless/mock_data.js
+++ b/spec/frontend/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/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js
new file mode 100644
index 00000000000..aac57c75a4f
--- /dev/null
+++ b/spec/frontend/serverless/store/actions_spec.js
@@ -0,0 +1,90 @@
+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(() => new Promise(resolve => setTimeout(() => resolve(200), Infinity)));
+
+ 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/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
new file mode 100644
index 00000000000..fb549c8f153
--- /dev/null
+++ b/spec/frontend/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/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
new file mode 100644
index 00000000000..ca3053e5c38
--- /dev/null
+++ b/spec/frontend/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/frontend/serverless/utils.js b/spec/frontend/serverless/utils.js
new file mode 100644
index 00000000000..5ce2e37d493
--- /dev/null
+++ b/spec/frontend/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/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/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 2bc3933809f..6808ed86c9a 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -230,5 +230,18 @@ describe BlobHelper do
expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
end
+
+ it 'escapes special characters' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "testing/#hashes", "readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/readme.md%23test")
+ expect(helper.ide_edit_path(project, "testing/#hashes", "src#/readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/src%23/readme.md%23test")
+ end
+
+ it 'does not escape "/" character' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "testing/slashes", "readme.md/")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md/")
+ end
end
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/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/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index d9b298e84da..ef4589ada48 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -141,18 +141,16 @@ describe('DiffFile', () => {
it('should have too large warning and blob link', done => {
const BLOB_LINK = '/file/view/path';
vm.file.viewer.error = diffViewerErrors.too_large;
+ vm.file.viewer.error_message =
+ 'This source diff could not be displayed because it is too large';
vm.file.view_path = BLOB_LINK;
+ vm.file.renderIt = true;
vm.$nextTick(() => {
expect(vm.$el.innerText).toContain(
'This source diff could not be displayed because it is too large',
);
- expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined();
- expect(
- vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK),
- ).toBeGreaterThan(-1);
-
done();
});
});
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/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
new file mode 100644
index 00000000000..e3c455d1686
--- /dev/null
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -0,0 +1,29 @@
+import { getTimeDiff } from '~/monitoring/utils';
+import { timeWindows } from '~/monitoring/constants';
+
+describe('getTimeDiff', () => {
+ it('defaults to an 8 hour (28800s) difference', () => {
+ const params = getTimeDiff();
+
+ expect(params.end - params.start).toEqual(28800);
+ });
+
+ it('accepts time window as an argument', () => {
+ const params = getTimeDiff(timeWindows.thirtyMinutes);
+
+ expect(params.end - params.start).not.toEqual(28800);
+ });
+
+ it('returns a value for every defined time window', () => {
+ const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours');
+
+ nonDefaultWindows.forEach(window => {
+ const params = getTimeDiff(timeWindows[window]);
+ const diff = params.end - params.start;
+
+ // Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs)
+ expect(diff).not.toEqual(28800);
+ expect(typeof diff).toEqual('number');
+ });
+ });
+});
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/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
new file mode 100644
index 00000000000..a89952ee435
--- /dev/null
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -0,0 +1,167 @@
+import $ from 'jquery';
+import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars
+import TimezoneDropdown, {
+ formatUtcOffset,
+ formatTimezone,
+} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+
+describe('Timezone Dropdown', function() {
+ preloadFixtures('pipeline_schedules/edit.html');
+
+ let $inputEl = null;
+ let $dropdownEl = null;
+ let $wrapper = null;
+ const tzListSel = '.dropdown-content ul li a.is-active';
+
+ describe('Initialize', () => {
+ describe('with dropdown already loaded', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html');
+ $wrapper = $('.dropdown');
+ $inputEl = $('#schedule_cron_timezone');
+ $dropdownEl = $('.js-timezone-dropdown');
+
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ });
+ });
+
+ it('can take an $inputEl in the constructor', () => {
+ const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
+ const tzValue = 'Asia/Colombo';
+
+ expect($inputEl.val()).toBe('UTC');
+
+ $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click');
+
+ const val = $inputEl.val();
+
+ expect(val).toBe(tzValue);
+ expect(val).not.toBe('UTC');
+ });
+
+ it('will format data array of timezones into a list of offsets', () => {
+ const data = $dropdownEl.data('data');
+ const formatted = $wrapper.find(tzListSel).text();
+
+ data.forEach(item => {
+ expect(formatted).toContain(formatTimezone(item));
+ });
+ });
+
+ it('will default the timezone to UTC', () => {
+ const tz = $inputEl.val();
+
+ expect(tz).toBe('UTC');
+ });
+ });
+
+ describe('without dropdown loaded', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html');
+ $wrapper = $('.dropdown');
+ $inputEl = $('#schedule_cron_timezone');
+ $dropdownEl = $('.js-timezone-dropdown');
+ });
+
+ it('will populate the list of UTC offsets after the dropdown is loaded', () => {
+ expect($wrapper.find(tzListSel).length).toEqual(0);
+
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ });
+
+ expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
+ });
+
+ it('will call a provided handler when a new timezone is selected', () => {
+ const onSelectTimezone = jasmine.createSpy('onSelectTimezoneMock');
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ onSelectTimezone,
+ });
+
+ $wrapper
+ .find(tzListSel)
+ .first()
+ .trigger('click');
+
+ expect(onSelectTimezone).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('formatUtcOffset', () => {
+ it('will convert negative utc offsets in seconds to hours and minutes', () => {
+ expect(formatUtcOffset(-21600)).toEqual('- 6');
+ });
+
+ it('will convert positive utc offsets in seconds to hours and minutes', () => {
+ expect(formatUtcOffset(25200)).toEqual('+ 7');
+ expect(formatUtcOffset(49500)).toEqual('+ 13.75');
+ });
+
+ it('will return 0 when given a string', () => {
+ expect(formatUtcOffset('BLAH')).toEqual('0');
+ expect(formatUtcOffset('$%$%')).toEqual('0');
+ });
+
+ it('will return 0 when given an array', () => {
+ expect(formatUtcOffset(['an', 'array'])).toEqual('0');
+ });
+
+ it('will return 0 when given an object', () => {
+ expect(formatUtcOffset({ some: '', object: '' })).toEqual('0');
+ });
+
+ it('will return 0 when given null', () => {
+ expect(formatUtcOffset(null)).toEqual('0');
+ });
+
+ it('will return 0 when given undefined', () => {
+ expect(formatUtcOffset(undefined)).toEqual('0');
+ });
+
+ it('will return 0 when given empty input', () => {
+ expect(formatUtcOffset('')).toEqual('0');
+ });
+ });
+
+ describe('formatTimezone', () => {
+ it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => {
+ expect(
+ formatTimezone({
+ name: 'Chatham Is.',
+ offset: 49500,
+ identifier: 'Pacific/Chatham',
+ }),
+ ).toEqual('[UTC + 13.75] Chatham Is.');
+ });
+
+ it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => {
+ expect(
+ formatTimezone({
+ name: 'Saskatchewan',
+ offset: -21600,
+ identifier: 'America/Regina',
+ }),
+ ).toEqual('[UTC - 6] Saskatchewan');
+ });
+
+ it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => {
+ expect(
+ formatTimezone({
+ name: 'Accra',
+ offset: 0,
+ identifier: 'Africa/Accra',
+ }),
+ ).toEqual('[UTC 0] Accra');
+ });
+ });
+});
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/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js
deleted file mode 100644
index 85cfe71281f..00000000000
--- a/spec/javascripts/serverless/components/functions_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import Vue from 'vue';
-
-import functionsComponent from '~/serverless/components/functions.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
-
-import { mockServerlessFunctions } from '../mock_data';
-
-const createComponent = (
- functions,
- installed = true,
- loadingData = true,
- hasFunctionData = true,
-) => {
- const component = Vue.extend(functionsComponent);
-
- return mountComponent(component, {
- functions,
- installed,
- clustersPath: '/testClusterPath',
- helpPath: '/helpPath',
- loadingData,
- hasFunctionData,
- });
-};
-
-describe('functionsComponent', () => {
- it('should render empty state when Knative is not installed', () => {
- const vm = createComponent({}, false);
-
- 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',
- );
-
- vm.$destroy();
- });
-
- it('should render a loading component', () => {
- const vm = createComponent({});
-
- expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null);
- expect(vm.$el.querySelector('div.animation-container')).not.toBe(null);
- });
-
- it('should render empty state when there is no function data', () => {
- const vm = createComponent({}, true, false, false);
-
- expect(
- 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(
- '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);
-
- expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null);
- expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true);
- });
-});
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/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/entities/job_request/image_spec.rb b/spec/lib/api/entities/job_request/image_spec.rb
new file mode 100644
index 00000000000..092c181ae9c
--- /dev/null
+++ b/spec/lib/api/entities/job_request/image_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Entities::JobRequest::Image do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]}
+ let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)}
+ let(:entity) { described_class.new(image) }
+
+ subject { entity.as_json }
+
+ it 'returns the image name' do
+ expect(subject[:name]).to eq 'image_name'
+ end
+
+ it 'returns the entrypoint' do
+ expect(subject[:entrypoint]).to eq ['foo']
+ end
+
+ it 'returns the ports' do
+ expect(subject[:ports]).to eq ports
+ end
+
+ context 'when the ports param is nil' do
+ let(:ports) { nil }
+
+ it 'does not return the ports' do
+ expect(subject[:ports]).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/api/entities/job_request/port_spec.rb b/spec/lib/api/entities/job_request/port_spec.rb
new file mode 100644
index 00000000000..40ab4cd6231
--- /dev/null
+++ b/spec/lib/api/entities/job_request/port_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::API::Entities::JobRequest::Port do
+ let(:port) { double(number: 80, protocol: 'http', name: 'name')}
+ let(:entity) { described_class.new(port) }
+
+ subject { entity.as_json }
+
+ it 'returns the port number' do
+ expect(subject[:number]).to eq 80
+ end
+
+ it 'returns if the port protocol' do
+ expect(subject[:protocol]).to eq 'http'
+ end
+
+ it 'returns the port name' do
+ expect(subject[:name]).to eq 'name'
+ end
+end
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/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
index f8107dd40b9..4db829b1e7b 100644
--- a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
@@ -30,6 +30,6 @@ describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 201
described_class.new.perform(100, 101)
- expect(stages.all.pluck(:position)).to eq [2, 3]
+ expect(stages.all.order(:id).pluck(:position)).to eq [2, 3]
end
end
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/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index 7d3d8a949ef..1d0ffb5e9df 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -195,4 +195,44 @@ describe Gitlab::BackgroundMigration do
end
end
end
+
+ describe '.dead_jobs?' do
+ let(:queue) do
+ [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ end
+
+ context 'when there are dead jobs present' do
+ before do
+ allow(Sidekiq::DeadSet).to receive(:new).and_return(queue)
+ end
+
+ it 'returns true if specific job exists' do
+ expect(described_class.dead_jobs?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(described_class.dead_jobs?('Bar')).to eq(false)
+ end
+ end
+ end
+
+ describe '.retrying_jobs?' do
+ let(:queue) do
+ [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ end
+
+ context 'when there are dead jobs present' do
+ before do
+ allow(Sidekiq::RetrySet).to receive(:new).and_return(queue)
+ end
+
+ it 'returns true if specific job exists' do
+ expect(described_class.retrying_jobs?('Foo')).to eq(true)
+ end
+
+ it 'returns false if specific job does not exist' do
+ expect(described_class.retrying_jobs?('Bar')).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb
index 773a52cdfbc..6e20e0ef5c3 100644
--- a/spec/lib/gitlab/ci/build/image_spec.rb
+++ b/spec/lib/gitlab/ci/build/image_spec.rb
@@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do
it 'populates fabricated object with the proper name attribute' do
expect(subject.name).to eq(image_name)
end
+
+ it 'does not populate the ports' do
+ expect(subject.ports).to be_empty
+ end
end
context 'when image is defined as hash' do
let(:entrypoint) { '/bin/sh' }
- let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) }
+
+ let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) }
it 'fabricates an object of the proper class' do
is_expected.to be_kind_of(described_class)
@@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do
expect(subject.name).to eq(image_name)
expect(subject.entrypoint).to eq(entrypoint)
end
+
+ it 'populates the ports' do
+ port = subject.ports.first
+ expect(port.number).to eq 80
+ expect(port.protocol).to eq 'http'
+ expect(port.name).to eq 'default_port'
+ end
end
context 'when image name is empty' do
@@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first).to be_kind_of(described_class)
expect(subject.first.name).to eq(service_image_name)
end
+
+ it 'does not populate the ports' do
+ expect(subject.first.ports).to be_empty
+ end
end
context 'when service is defined as hash' do
@@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do
let(:service_command) { 'sleep 30' }
let(:job) do
create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint,
- alias: service_alias, command: service_command }] })
+ alias: service_alias, command: service_command, ports: [80] }] })
end
it 'fabricates an non-empty array of objects' do
@@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first.entrypoint).to eq(service_entrypoint)
expect(subject.first.alias).to eq(service_alias)
expect(subject.first.command).to eq(service_command)
+
+ port = subject.first.ports.first
+ expect(port.number).to eq 80
+ expect(port.protocol).to eq 'http'
+ expect(port.name).to eq 'default_port'
end
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/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb
new file mode 100644
index 00000000000..1413780dfa6
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/port_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Port do
+ subject { described_class.new(port) }
+
+ context 'when port is defined as an integer' do
+ let(:port) { 80 }
+
+ it 'populates the object' do
+ expect(subject.number).to eq 80
+ expect(subject.protocol).to eq described_class::DEFAULT_PORT_PROTOCOL
+ expect(subject.name).to eq described_class::DEFAULT_PORT_NAME
+ end
+ end
+
+ context 'when port is defined as hash' do
+ let(:port) { { number: 80, protocol: 'https', name: 'port_name' } }
+
+ it 'populates the object' do
+ expect(subject.number).to eq 80
+ expect(subject.protocol).to eq 'https'
+ expect(subject.name).to eq 'port_name'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 3c0007f4d57..0bc9e8bd3cd 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -100,6 +100,26 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when wrong action type is used' do
+ let(:config) do
+ { name: 'production',
+ action: ['stop'] }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about wrong action type' do
+ expect(entry.errors)
+ .to include 'environment action should be a string'
+ end
+ end
+ end
+
context 'when invalid action is used' do
let(:config) do
{ name: 'production',
@@ -151,6 +171,26 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when wrong url type is used' do
+ let(:config) do
+ { name: 'production',
+ url: ['https://meow.meow'] }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about wrong url type' do
+ expect(entry.errors)
+ .to include 'environment url should be a string'
+ end
+ end
+ end
+
context 'when variables are used for environment' do
let(:config) do
{ name: 'review/$CI_COMMIT_REF_NAME',
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index 1a4d9ed5517..1ebdda398b9 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to be_nil
end
end
+
+ describe '#ports' do
+ it "returns image's ports" do
+ expect(entry.ports).to be_nil
+ end
+ end
end
context 'when configuration is a hash' do
@@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to eq %w(/bin/sh run)
end
end
+
+ context 'when configuration has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } }
+ let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:image_ports) { false }
+
+ context 'when with_image_ports metadata is not enabled' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("image config contains disallowed keys: ports")
+ end
+ end
+ end
+
+ context 'when with_image_ports metadata is enabled' do
+ let(:image_ports) { true }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#ports' do
+ it "returns image's ports" do
+ expect(entry.ports).to eq ports
+ end
+ end
+ end
+ end
end
context 'when entry value is not correct' do
@@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'image config should be a hash or a string'
+ expect(entry.errors.first)
+ .to match /config should be a hash or a string/
end
end
@@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'image config contains unknown keys: non_existing'
+ expect(entry.errors.first)
+ .to match /config contains unknown keys: non_existing/
end
end
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/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb
new file mode 100644
index 00000000000..5f8f294334e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Port do
+ let(:entry) { described_class.new(config) }
+
+ before do
+ entry.compose!
+ end
+
+ context 'when configuration is a string' do
+ let(:config) { 80 }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq(number: 80)
+ end
+ end
+
+ describe '#number' do
+ it "returns port number" do
+ expect(entry.number).to eq 80
+ end
+ end
+
+ describe '#protocol' do
+ it "is nil" do
+ expect(entry.protocol).to be_nil
+ end
+ end
+
+ describe '#name' do
+ it "is nil" do
+ expect(entry.name).to be_nil
+ end
+ end
+ end
+
+ context 'when configuration is a hash' do
+ context 'with the complete hash' do
+ let(:config) do
+ { number: 80,
+ protocol: 'http',
+ name: 'foobar' }
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#number' do
+ it "returns port number" do
+ expect(entry.number).to eq 80
+ end
+ end
+
+ describe '#protocol' do
+ it "returns port protocol" do
+ expect(entry.protocol).to eq 'http'
+ end
+ end
+
+ describe '#name' do
+ it "returns port name" do
+ expect(entry.name).to eq 'foobar'
+ end
+ end
+ end
+
+ context 'with only the port number' do
+ let(:config) { { number: 80 } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq(number: 80)
+ end
+ end
+
+ describe '#number' do
+ it "returns port number" do
+ expect(entry.number).to eq 80
+ end
+ end
+
+ describe '#protocol' do
+ it "is nil" do
+ expect(entry.protocol).to be_nil
+ end
+ end
+
+ describe '#name' do
+ it "is nil" do
+ expect(entry.name).to be_nil
+ end
+ end
+ end
+
+ context 'without the number' do
+ let(:config) { { protocol: 'http' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ let(:config) { '80' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when protocol' do
+ let(:config) { { number: 80, protocol: protocol, name: 'foobar' } }
+
+ context 'is http' do
+ let(:protocol) { 'http' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'is https' do
+ let(:protocol) { 'https' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'is neither http nor https' do
+ let(:protocol) { 'foo' }
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry.errors).to include("port protocol should be http or https")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/ports_spec.rb b/spec/lib/gitlab/ci/config/entry/ports_spec.rb
new file mode 100644
index 00000000000..2063bd1d86c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/ports_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Ports do
+ let(:entry) { described_class.new(config) }
+
+ before do
+ entry.compose!
+ end
+
+ context 'when configuration is valid' do
+ let(:config) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid array' do
+ expect(entry.value).to eq(config)
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ let(:config) { 'postgresql:9.5' }
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ context 'when any of the ports' do
+ before do
+ expect(entry).not_to be_valid
+ expect(entry.errors.count).to eq 1
+ end
+
+ context 'have the same name' do
+ let(:config) do
+ [{ number: 80, protocol: 'http', name: 'foobar' },
+ { number: 81, protocol: 'http', name: 'foobar' }]
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry.errors.first).to match /each port name must be different/
+ end
+ end
+ end
+
+ context 'have the same port' do
+ let(:config) do
+ [{ number: 80, protocol: 'http', name: 'foobar' },
+ { number: 80, protocol: 'http', name: 'foobar1' }]
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry.errors.first).to match /each port number can only be referenced once/
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index 9ebf947a751..d5bd139b5f1 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.command).to be_nil
end
end
+
+ describe '#ports' do
+ it "returns service's ports" do
+ expect(entry.ports).to be_nil
+ end
+ end
end
context 'when configuration is a hash' do
@@ -81,6 +87,40 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.entrypoint).to eq %w(/bin/sh run)
end
end
+
+ context 'when configuration has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) do
+ { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ end
+ let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:image_ports) { false }
+
+ context 'when with_image_ports metadata is not enabled' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("service config contains disallowed keys: ports")
+ end
+ end
+ end
+
+ context 'when with_image_ports metadata is enabled' do
+ let(:image_ports) { true }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#ports' do
+ it "returns image's ports" do
+ expect(entry.ports).to eq ports
+ end
+ end
+ end
+ end
end
context 'when entry value is not correct' do
@@ -88,8 +128,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'service config should be a hash or a string'
+ expect(entry.errors.first)
+ .to match /config should be a hash or a string/
end
end
@@ -105,8 +145,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'service config contains unknown keys: non_existing'
+ expect(entry.errors.first)
+ .to match /config contains unknown keys: non_existing/
end
end
@@ -116,4 +156,26 @@ describe Gitlab::Ci::Config::Entry::Service do
end
end
end
+
+ context 'when service has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) do
+ { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ end
+
+ it 'alias field is mandatory' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("service alias can't be blank")
+ end
+ end
+
+ context 'when service does not have ports' do
+ let(:config) do
+ { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
+ end
+
+ it 'alias field is optional' do
+ expect(entry).to be_valid
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb
index 7c4319aee63..d5a1316f665 100644
--- a/spec/lib/gitlab/ci/config/entry/services_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb
@@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do
end
end
end
+
+ context 'when configuration has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] }
+ let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:image_ports) { false }
+
+ context 'when with_image_ports metadata is not enabled' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("service config contains disallowed keys: ports")
+ end
+ end
+ end
+
+ context 'when with_image_ports metadata is enabled' do
+ let(:image_ports) { true }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid array' do
+ expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }])
+ end
+ end
+
+ describe 'services alias' do
+ context 'when they are not unique' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
+ { name: 'ruby', alias: 'postgres_old', ports: [81] }]
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("services config alias must be unique in services with ports")
+ end
+ end
+ end
+
+ context 'when they are unique' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
+ { name: 'ruby', alias: 'ruby', ports: [81] }]
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when one of the duplicated alias is in a service without ports' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
+ { name: 'ruby', alias: 'postgres_old' }]
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when there are not any ports' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old' },
+ { name: 'ruby', alias: 'postgres_old' }]
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+ end
end
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 18f255c1ab7..fd2a29e4ddb 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -123,6 +123,63 @@ describe Gitlab::Ci::Config do
)
end
end
+
+ context 'when ports have been set' do
+ context 'in the main image' do
+ let(:yml) do
+ <<-EOS
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it 'raises an error' do
+ expect(config.errors).to include("image config contains disallowed keys: ports")
+ end
+ end
+
+ context 'in the job image' do
+ let(:yml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it 'raises an error' do
+ expect(config.errors).to include("jobs:test:image config contains disallowed keys: ports")
+ end
+ end
+
+ context 'in the services' do
+ let(:yml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image: ruby:2.2
+ services:
+ - name: test
+ alias: test
+ ports:
+ - 80
+ EOS
+ end
+
+ it 'raises an error' do
+ expect(config.errors).to include("jobs:test:services:service config contains disallowed keys: ports")
+ end
+ end
+ end
end
context "when using 'include' directive" do
@@ -168,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",
@@ -259,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
@@ -284,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 fbbd58280a9..4e3681cd943 100644
--- a/spec/lib/gitlab/ci/templates/templates_spec.rb
+++ b/spec/lib/gitlab/ci/templates/templates_spec.rb
@@ -4,6 +4,9 @@ require 'spec_helper'
describe "CI YML Templates" do
ABSTRACT_TEMPLATES = %w[Serverless].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|
@@ -20,7 +23,10 @@ describe "CI YML Templates" do
describe 'concrete templates with CI/CD jobs' do
concrete_templates.each do |template|
it "#{template.name} template should be valid" do
- expect { Gitlab::Ci::YamlProcessor.new(template.content) }
+ # Trigger processing of included files
+ project = create(:project, :test_repo) if PROJECT_DEPENDENT_TEMPLATES.include?(template.name)
+
+ expect { Gitlab::Ci::YamlProcessor.new(template.content, project: project) }
.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 29638ef47c5..b7b30e60d44 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", :quarantine 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"] }
@@ -1233,7 +1241,7 @@ module Gitlab
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string")
end
it "returns errors if job services parameter is not an array" do
@@ -1247,7 +1255,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string")
end
it "returns error if job configuration is invalid" do
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/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 41810a8ec03..705df1f4fe7 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -197,6 +197,11 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
.to receive(:fetch_as_mirror)
.with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github')
+ service = double
+ expect(Projects::HousekeepingService)
+ .to receive(:new).with(project, :gc).and_return(service)
+ expect(service).to receive(:execute)
+
expect(importer.import_repository).to eq(true)
end
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/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
new file mode 100644
index 00000000000..cc0f2baf905
--- /dev/null
+++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Import::MergeRequestHelpers, type: :helper do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+
+ describe '.create_merge_request_without_hooks' do
+ let(:iid) { 42 }
+
+ let(:attributes) do
+ {
+ iid: iid,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'master-42',
+ target_branch: 'master',
+ state: :merged,
+ author_id: user.id,
+ assignee_id: user.id
+ }
+ end
+
+ subject { helper.create_merge_request_without_hooks(project, attributes, iid) }
+
+ context 'when merge request does not exist' do
+ it 'returns a new object' do
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(false)
+ end
+
+ it 'does load all existing objects' do
+ 5.times do |iid|
+ MergeRequest.create!(
+ attributes.merge(iid: iid, source_branch: iid.to_s))
+ end
+
+ # does ensure that we only load object twice
+ # 1. by #insert_and_return_id
+ # 2. by project.merge_requests.find
+ expect_any_instance_of(MergeRequest).to receive(:attributes)
+ .twice.times.and_call_original
+
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(false)
+ end
+ end
+
+ context 'when merge request does exist' do
+ before do
+ MergeRequest.create!(attributes)
+ end
+
+ it 'returns an existing object' do
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(true)
+ end
+ end
+
+ context 'when project is deleted' do
+ before do
+ project.delete
+ end
+
+ it 'returns an existing object' do
+ expect(subject.first).to be_nil
+ end
+ end
+ 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/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index 082e3b36dd0..c57b96fb00d 100644
--- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -25,6 +25,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
expected = {
project: project,
tag: 'v1.0.0',
+ name: 'First release',
description: 'Release v1.0.0',
created_at: created_at,
updated_at: created_at
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/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb
new file mode 100644
index 00000000000..fc9e421bea6
--- /dev/null
+++ b/spec/lib/gitlab/push_options_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PushOptions do
+ describe 'namespace and key validation' do
+ it 'ignores unrecognised namespaces' do
+ options = described_class.new(['invalid.key=value'])
+
+ expect(options.get(:invalid)).to eq(nil)
+ end
+
+ it 'ignores unrecognised keys' do
+ options = described_class.new(['merge_request.key=value'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'ignores blank keys' do
+ options = described_class.new(['merge_request'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'parses recognised namespace and key pairs' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request)).to include({
+ target: 'value'
+ })
+ end
+ end
+
+ describe '#get' do
+ it 'can emulate Hash#dig' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+ end
+
+ describe '#as_json' do
+ it 'returns all options' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.as_json).to include(
+ merge_request: {
+ target: 'value'
+ }
+ )
+ end
+ end
+
+ it 'can parse multiple push options' do
+ options = described_class.new([
+ 'merge_request.create',
+ 'merge_request.target=value'
+ ])
+
+ expect(options.get(:merge_request)).to include({
+ create: true,
+ target: 'value'
+ })
+ expect(options.get(:merge_request, :create)).to eq(true)
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'stores options internally as a HashWithIndifferentAccess' do
+ options = described_class.new([
+ 'merge_request.create'
+ ])
+
+ expect(options.get('merge_request', 'create')).to eq(true)
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'selects the last option when options contain duplicate namespace and key pairs' do
+ options = described_class.new([
+ 'merge_request.target=value1',
+ 'merge_request.target=value2'
+ ])
+
+ expect(options.get(:merge_request, :target)).to eq('value2')
+ end
+
+ it 'defaults values to true' do
+ options = described_class.new(['merge_request.create'])
+
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'expands aliases' do
+ options = described_class.new(['mr.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'forgives broken push options' do
+ options = described_class.new(['merge_request . target = value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ 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/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index 3333f8307ae..cb14204b99a 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -61,13 +61,37 @@ describe Sentry::Client do
end
end
+ shared_examples 'maps exceptions' do
+ exceptions = {
+ HTTParty::Error => 'Error when connecting to Sentry',
+ Net::OpenTimeout => 'Connection to Sentry timed out',
+ SocketError => 'Received SocketError when trying to connect to Sentry',
+ OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data',
+ Errno::ECONNREFUSED => 'Connection refused',
+ StandardError => 'Sentry request failed due to StandardError'
+ }
+
+ exceptions.each do |exception, message|
+ context "#{exception}" do
+ before do
+ stub_request(:get, sentry_request_url).to_raise(exception)
+ end
+
+ it do
+ expect { subject }
+ .to raise_exception(Sentry::Client::Error, message)
+ end
+ end
+ end
+ end
+
describe '#list_issues' do
let(:issue_status) { 'unresolved' }
let(:limit) { 20 }
-
let(:sentry_api_response) { issues_sample_response }
+ let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
- let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sentry_api_response) }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
subject { client.list_issues(issue_status: issue_status, limit: limit) }
@@ -121,16 +145,14 @@ describe Sentry::Client do
# Sentry API returns 404 if there are extra slashes in the URL!
context 'extra slashes in URL' do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' }
- let(:client) { described_class.new(sentry_url, token) }
- let!(:valid_req_stub) do
- stub_sentry_request(
- 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \
+ let(:sentry_request_url) do
+ 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \
'issues/?limit=20&query=is:unresolved'
- )
end
it 'removes extra slashes in api url' do
+ expect(client.url).to eq(sentry_url)
expect(Gitlab::HTTP).to receive(:get).with(
URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'),
anything
@@ -138,7 +160,7 @@ describe Sentry::Client do
subject
- expect(valid_req_stub).to have_been_requested
+ expect(sentry_api_request).to have_been_requested
end
end
@@ -169,6 +191,8 @@ describe Sentry::Client do
expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
end
end
+
+ it_behaves_like 'maps exceptions'
end
describe '#list_projects' do
@@ -260,12 +284,18 @@ describe Sentry::Client do
expect(valid_req_stub).to have_been_requested
end
end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_list_projects_url }
+
+ it_behaves_like 'maps exceptions'
+ end
end
private
def stub_sentry_request(url, body: {}, status: 200, headers: {})
- WebMock.stub_request(:get, url)
+ stub_request(:get, url)
.to_return(
status: status,
headers: { 'Content-Type' => 'application/json' }.merge(headers),
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/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index e65f97df3c3..43954511858 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -82,6 +82,13 @@ describe InstanceConfiguration do
it 'returns the key artifacts_max_size' do
expect(gitlab_ci.keys).to include(:artifacts_max_size)
end
+
+ it 'returns the key artifacts_max_size with values' do
+ stub_application_setting(max_artifacts_size: 200)
+
+ expect(gitlab_ci[:artifacts_max_size][:default]).to eq(100.megabytes)
+ expect(gitlab_ci[:artifacts_max_size][:value]).to eq(200.megabytes)
+ end
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 892fdc4e4e9..6f34ef9c1bc 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -805,6 +805,14 @@ describe MergeRequest do
expect(merge_request.commits).not_to be_empty
expect(merge_request.related_notes.count).to eq(3)
end
+
+ it "excludes system notes for commits" do
+ system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id,
+ project: merge_request.project)
+
+ expect(merge_request.related_notes.count).to eq(2)
+ expect(merge_request.related_notes).not_to include(system_note)
+ end
end
describe '#for_fork?' do
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/release_spec.rb b/spec/models/release_spec.rb
index b4b32c95dee..0b19a4f8efc 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -18,6 +18,22 @@ RSpec.describe Release do
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_presence_of(:name) }
+
+ context 'when a release exists in the database without a name' do
+ it 'does not require name' do
+ existing_release_without_name = build(:release, project: project, author: user, name: nil)
+ existing_release_without_name.save(validate: false)
+
+ existing_release_without_name.description = "change"
+ existing_release_without_name.save
+ existing_release_without_name.reload
+
+ expect(existing_release_without_name).to be_valid
+ expect(existing_release_without_name.description).to eq("change")
+ expect(existing_release_without_name.name).to be_nil
+ end
+ end
end
describe '#assets_count' do
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/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 844d96017de..126d44d1860 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -100,5 +100,51 @@ describe Ci::PipelinePolicy, :models do
end
end
end
+
+ describe 'read_pipeline_variable' do
+ let(:project) { create(:project, :public) }
+
+ context 'when user has owner access' do
+ let(:user) { project.owner }
+
+ it 'is enabled' do
+ expect(policy).to be_allowed :read_pipeline_variable
+ end
+ end
+
+ context 'when user is developer and the creator of the pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: user) }
+
+ before do
+ project.add_developer(user)
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'is enabled' do
+ expect(policy).to be_allowed :read_pipeline_variable
+ end
+ end
+
+ context 'when user is developer and it is not the creator of the pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: project.owner) }
+
+ before do
+ project.add_developer(user)
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'is disabled' do
+ expect(policy).to be_disallowed :read_pipeline_variable
+ end
+ end
+
+ context 'when user is not owner nor developer' do
+ it 'is disabled' do
+ expect(policy).not_to be_allowed :read_pipeline_variable
+ end
+ end
+ end
end
end
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/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 493d3642255..8fc7fdc8632 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -32,6 +32,7 @@ describe API::Environments do
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
+ expect(json_response.first).not_to have_key("last_deployment")
end
end
@@ -188,4 +189,25 @@ describe API::Environments do
end
end
end
+
+ describe 'GET /projects/:id/environments/:environment_id' do
+ context 'as member of the project' do
+ it 'returns project environments' do
+ create(:deployment, :success, project: project, environment: environment)
+
+ get api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/environment')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
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..f95f460fd14
--- /dev/null
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'GitlabSchema configurations' do
+ include GraphqlHelpers
+
+ it 'shows an error if complexity is too high' do
+ project = create(:project, :repository)
+ query = graphql_query_for('project', { 'fullPath' => project.full_path }, "id\nname\ndescription")
+
+ 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
+
+ context 'when IntrospectionQuery' do
+ it 'is not too complex' do
+ query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql'))
+
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_errors).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 537194b8e11..1ce8f520962 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
@@ -838,8 +888,10 @@ describe API::Internal do
}
end
+ let(:branch_name) { 'feature' }
+
let(:changes) do
- "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch"
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
end
let(:push_options) do
@@ -855,9 +907,9 @@ describe API::Internal do
it 'enqueues a PostReceive worker job' do
expect(PostReceive).to receive(:perform_async)
- .with(gl_repository, identifier, changes, push_options)
+ .with(gl_repository, identifier, changes, { ci: { skip: true } })
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
end
it 'decreases the reference counter and returns the result' do
@@ -865,17 +917,17 @@ describe API::Internal do
.and_return(reference_counter)
expect(reference_counter).to receive(:decrease).and_return(true)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['reference_counter_decreased']).to be(true)
end
it 'returns link to create new merge request' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['merge_request_urls']).to match [{
- "branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "branch_name" => branch_name,
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}",
"new_merge_request" => true
}]
end
@@ -883,16 +935,87 @@ describe API::Internal do
it 'returns empty array if printing_merge_request_link_enabled is false' do
project.update!(printing_merge_request_link_enabled: false)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['merge_request_urls']).to eq([])
end
+ it 'does not invoke MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
+
+ post api('/internal/post_receive'), params: valid_params
+ end
+
+ context 'when there are merge_request push options' do
+ before do
+ valid_params[:push_options] = ['merge_request.create']
+ end
+
+ it 'invokes MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+
+ post api('/internal/post_receive'), params: valid_params
+ end
+
+ it 'creates a new merge request' do
+ expect do
+ post api('/internal/post_receive'), params: valid_params
+ end.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'links to the newly created merge request' do
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['merge_request_urls']).to match [{
+ 'branch_name' => branch_name,
+ 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1",
+ 'new_merge_request' => false
+ }]
+ end
+
+ it 'adds errors on the service instance to warnings' do
+ expect_any_instance_of(
+ MergeRequests::PushOptionsHandlerService
+ ).to receive(:errors).at_least(:once).and_return(['my error'])
+
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+
+ it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+
+ context 'when the feature is disabled' do
+ it 'does not invoke MergeRequests::PushOptionsHandlerService' do
+ Feature.disable(:mr_push_options)
+
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+
+ expect do
+ post api('/internal/post_receive'), params: valid_params
+ end.not_to change { MergeRequest.count }
+
+ Feature.enable(:mr_push_options)
+ end
+ end
+ end
+
context 'broadcast message exists' do
let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
it 'returns one broadcast message' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(broadcast_message.message)
@@ -901,7 +1024,7 @@ describe API::Internal do
context 'broadcast message does not exist' do
it 'returns empty string' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
@@ -912,7 +1035,7 @@ describe API::Internal do
it 'returns empty string' do
allow(BroadcastMessage).to receive(:current).and_return(nil)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
@@ -924,7 +1047,7 @@ describe API::Internal do
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz')
project_moved.add_message
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["redirected_message"]).to be_present
@@ -937,7 +1060,7 @@ describe API::Internal do
project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http')
project_created.add_message
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["project_created_message"]).to be_present
@@ -949,7 +1072,7 @@ describe API::Internal do
it 'does not try to notify that project moved' do
allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
end
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..0d46463312b 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)
@@ -438,6 +445,72 @@ describe API::Pipelines do
end
end
+ describe 'GET /projects/:id/pipelines/:pipeline_id/variables' do
+ subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", api_user) }
+
+ let(:api_user) { user }
+
+ context 'user is a mantainer' do
+ it 'returns pipeline variables empty' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_empty
+ end
+
+ context 'with variables' do
+ let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
+
+ it 'returns pipeline variables' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" })
+ end
+ end
+ end
+
+ context 'user is a developer' do
+ let(:pipeline_owner_user) { create(:user) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: pipeline_owner_user) }
+
+ before do
+ project.add_developer(api_user)
+ end
+
+ context 'pipeline created by the developer user' do
+ let(:api_user) { pipeline_owner_user }
+ let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
+
+ it 'returns pipeline variables' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" })
+ end
+ end
+
+ context 'pipeline created is not created by the developer user' do
+ let(:api_user) { create(:user) }
+
+ it 'should not return pipeline variables' do
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'user is not a project member' do
+ it 'should not return pipeline variables' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ end
+ end
+ end
+
describe 'DELETE /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
let(:owner) { project.owner }
@@ -474,7 +547,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 +562,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 +592,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/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 3ccedd8dd06..3585a827838 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -470,11 +470,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info)
- expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' })
+ expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
- 'alias' => nil, 'command' => nil },
+ 'alias' => nil, 'command' => nil, 'ports' => [] },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
- 'alias' => 'docker', 'command' => 'sleep 30' }])
+ 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }])
expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
@@ -853,6 +853,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ describe 'port support' do
+ let(:job) { create(:ci_build, pipeline: pipeline, options: options) }
+
+ context 'when job image has ports' do
+ let(:options) do
+ {
+ image: {
+ name: 'ruby',
+ ports: [80]
+ },
+ services: ['mysql']
+ }
+ end
+
+ it 'returns the image ports' do
+ request_job
+
+ expect(response).to have_http_status(:created)
+ expect(json_response).to include(
+ 'id' => job.id,
+ 'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]),
+ 'services' => all(a_hash_including('name' => 'mysql')))
+ end
+ end
+
+ context 'when job services settings has ports' do
+ let(:options) do
+ {
+ image: 'ruby',
+ services: [
+ {
+ name: 'tomcat',
+ ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }]
+ }
+ ]
+ }
+ end
+
+ it 'returns the service ports' do
+ request_job
+
+ expect(response).to have_http_status(:created)
+ expect(json_response).to include(
+ 'id' => job.id,
+ 'image' => a_hash_including('name' => 'ruby'),
+ 'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }])))
+ end
+ end
+ end
+
def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent }
@@ -1684,7 +1734,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
it 'download artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h).to include download_headers
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/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb
deleted file mode 100644
index bc9747d1413..00000000000
--- a/spec/services/after_branch_delete_service_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'spec_helper'
-
-describe AfterBranchDeleteService do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
-
- describe '#execute' do
- it 'stops environments attached to branch' do
- expect(service).to receive(:stop_environments)
-
- service.execute('feature')
- end
- end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 24707cd2d41..101b91e9cd8 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -306,6 +306,56 @@ describe Ci::CreatePipelineService do
it_behaves_like 'a failed pipeline'
end
+
+ context 'when config has ports' do
+ context 'in the main image' do
+ let(:ci_yaml) do
+ <<-EOS
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it_behaves_like 'a failed pipeline'
+ end
+
+ context 'in the job image' do
+ let(:ci_yaml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it_behaves_like 'a failed pipeline'
+ end
+
+ context 'in the service' do
+ let(:ci_yaml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image: ruby:2.2
+ services:
+ - name: test
+ ports:
+ - 80
+ EOS
+ end
+
+ it_behaves_like 'a failed pipeline'
+ end
+ end
end
context 'when commit contains a [ci skip] directive' do
@@ -368,8 +418,7 @@ describe Ci::CreatePipelineService do
context 'when push options contain ci.skip' do
let(:push_options) do
- ['ci.skip',
- 'another push option']
+ { 'ci' => { 'skip' => true } }
end
it 'creates a pipline in the skipped state' 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/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb
index a92d3376f7b..730fccc599e 100644
--- a/spec/services/error_tracking/list_projects_service_spec.rb
+++ b/spec/services/error_tracking/list_projects_service_spec.rb
@@ -51,14 +51,28 @@ describe ErrorTracking::ListProjectsService do
end
context 'sentry client raises exception' do
- before do
- expect(error_tracking_setting).to receive(:list_sentry_projects)
- .and_raise(Sentry::Client::Error, 'Sentry response status code: 500')
+ context 'Sentry::Client::Error' do
+ before do
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_raise(Sentry::Client::Error, 'Sentry response status code: 500')
+ end
+
+ it 'returns error response' do
+ expect(result[:message]).to eq('Sentry response status code: 500')
+ expect(result[:http_status]).to eq(:bad_request)
+ end
end
- it 'returns error response' do
- expect(result[:message]).to eq('Sentry response status code: 500')
- expect(result[:http_status]).to eq(:bad_request)
+ context 'Sentry::Client::MissingKeysError' do
+ before do
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ end
+
+ it 'returns error response' do
+ expect(result[:message]).to eq('Sentry API response is missing keys. key not found: "id"')
+ expect(result[:http_status]).to eq(:internal_server_error)
+ end
end
end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
new file mode 100644
index 00000000000..bb87267db7d
--- /dev/null
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -0,0 +1,339 @@
+require 'spec_helper'
+
+describe Git::BranchHooksService do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ let(:branch) { project.default_branch }
+ let(:ref) { "refs/heads/#{branch}" }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:oldrev) { commit.parent_id }
+ let(:newrev) { commit.id }
+
+ let(:service) do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ describe "Git Push Data" do
+ subject(:push_data) { service.execute }
+
+ it 'has expected push data attributes' do
+ is_expected.to match a_hash_including(
+ object_kind: 'push',
+ before: oldrev,
+ after: newrev,
+ ref: ref,
+ user_id: user.id,
+ user_name: user.name,
+ project_id: project.id
+ )
+ end
+
+ context "with repository data" do
+ subject { push_data[:repository] }
+
+ it 'has expected attributes' do
+ is_expected.to match a_hash_including(
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ )
+ end
+ end
+
+ context "with commits" do
+ subject { push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { push_data[:commits].first }
+
+ it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }
+
+ it 'includes expected commit data' do
+ is_expected.to match a_hash_including(
+ id: commit.id,
+ message: commit.safe_message,
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with a author" do
+ subject { push_data[:commits].first[:author] }
+
+ it 'includes expected author data' do
+ is_expected.to match a_hash_including(
+ name: commit.author_name,
+ email: commit.author_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Push Event' do
+ let(:event) { Event.find_by_action(Event::PUSHED) }
+
+ before do
+ service.execute
+ end
+
+ context "with an existing branch" do
+ it 'generates a push event with one commit' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to eq(oldrev)
+ expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to eq(1)
+ end
+ end
+
+ context "with a new branch" do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'generates a push event with more than one commit' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to be_nil
+ expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to be > 1
+ end
+ end
+
+ context 'removing a branch' do
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'generates a push event with no commits' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to eq(oldrev)
+ expect(event.push_event_payload.commit_to).to be_nil
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to eq(0)
+ end
+ end
+ end
+
+ describe 'Invalidating project cache' do
+ let(:commit_id) do
+ project.repository.update_file(
+ user, 'README.md', '', message: 'Update', branch_name: branch
+ )
+ end
+
+ let(:commit) { project.repository.commit(commit_id) }
+ let(:blank_sha) { Gitlab::Git::BLANK_SHA }
+
+ def clears_cache(extended: [])
+ expect(ProjectCacheWorker)
+ .to receive(:perform_async)
+ .with(project.id, extended, %i[commit_count repository_size])
+
+ service.execute
+ end
+
+ def clears_extended_cache
+ clears_cache(extended: %i[readme])
+ end
+
+ context 'on default branch' do
+ context 'create' do
+ # FIXME: When creating the default branch,the cache worker runs twice
+ before do
+ allow(ProjectCacheWorker).to receive(:perform_async)
+ end
+
+ let(:oldrev) { blank_sha }
+
+ it { clears_cache }
+ end
+
+ context 'update' do
+ it { clears_extended_cache }
+ end
+
+ context 'remove' do
+ let(:newrev) { blank_sha }
+
+ # TODO: this case should pass, but we only take account of added files
+ it { clears_cache }
+ end
+ end
+
+ context 'on ordinary branch' do
+ let(:branch) { 'fix' }
+
+ context 'create' do
+ let(:oldrev) { blank_sha }
+
+ it { clears_cache }
+ end
+
+ context 'update' do
+ it { clears_cache }
+ end
+
+ context 'remove' do
+ let(:newrev) { blank_sha }
+
+ it { clears_cache }
+ end
+ end
+ end
+
+ describe 'GPG signatures' do
+ context 'when the commit has a signature' do
+ context 'when the signature is already cached' do
+ before do
+ create(:gpg_signature, commit_sha: commit.id)
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'when the signature is not yet cached' do
+ it 'queues a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id)
+
+ service.execute
+ end
+
+ it 'can queue several commits to create the gpg signature' do
+ allow(Gitlab::Git::Commit)
+ .to receive(:shas_with_signatures)
+ .and_return([sample_commit.id, another_sample_commit.id])
+
+ expect(CreateGpgSignatureWorker)
+ .to receive(:perform_async)
+ .with([sample_commit.id, another_sample_commit.id], project.id)
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when the commit does not have a signature' do
+ before do
+ allow(Gitlab::Git::Commit)
+ .to receive(:shas_with_signatures)
+ .with(project.repository, [sample_commit.id])
+ .and_return([])
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker)
+ .not_to receive(:perform_async)
+ .with(sample_commit.id, project.id)
+
+ service.execute
+ end
+ end
+ end
+
+ describe 'Processing commit messages' do
+ # Create 4 commits, 2 of which have references. Limiting to 2 commits, we
+ # expect to see one commit message processor enqueued.
+ let(:commit_ids) do
+ Array.new(4) do |i|
+ message = "Issue #{'#' if i.even?}#{i}"
+ project.repository.update_file(
+ user, 'README.md', '', message: message, branch_name: branch
+ )
+ end
+ end
+
+ let(:oldrev) { commit_ids.first }
+ let(:newrev) { commit_ids.last }
+
+ before do
+ stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2)
+ end
+
+ context 'creating the default branch' do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'updating the default branch' do
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'removing the default branch' do
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'creating a normal branch' do
+ let(:branch) { 'fix' }
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'updating a normal branch' do
+ let(:branch) { 'fix' }
+
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'removing a normal branch' do
+ let(:branch) { 'fix' }
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index d0e2169b4a6..322e40a8112 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -8,7 +8,8 @@ describe Git::BranchPushService, services: true do
let(:blankrev) { Gitlab::Git::BLANK_SHA }
let(:oldrev) { sample_commit.parent_id }
let(:newrev) { sample_commit.id }
- let(:ref) { 'refs/heads/master' }
+ let(:branch) { 'master' }
+ let(:ref) { "refs/heads/#{branch}" }
before do
project.add_maintainer(user)
@@ -132,64 +133,6 @@ describe Git::BranchPushService, services: true do
end
end
- describe "Git Push Data" do
- let(:commit) { project.commit(newrev) }
-
- subject { push_data_from_service(project, user, oldrev, newrev, ref) }
-
- it { is_expected.to include(object_kind: 'push') }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
- end
- end
-
describe "Pipelines" do
subject { execute_service(project, user, oldrev, newrev, ref) }
@@ -203,59 +146,13 @@ describe Git::BranchPushService, services: true do
end
end
- describe "Push Event" do
- context "with an existing branch" do
- let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
- let(:event) { Event.find_by_action(Event::PUSHED) }
-
- it 'generates a push event with one commit' do
- expect(event).to be_an_instance_of(PushEvent)
- expect(event.project).to eq(project)
- expect(event.action).to eq(Event::PUSHED)
- expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
- expect(event.push_event_payload.commit_from).to eq(oldrev)
- expect(event.push_event_payload.commit_to).to eq(newrev)
- expect(event.push_event_payload.ref).to eq('master')
- expect(event.push_event_payload.commit_count).to eq(1)
- end
- end
-
- context "with a new branch" do
- let!(:new_branch_data) { push_data_from_service(project, user, Gitlab::Git::BLANK_SHA, newrev, ref) }
- let(:event) { Event.find_by_action(Event::PUSHED) }
-
- it 'generates a push event with more than one commit' do
- expect(event).to be_an_instance_of(PushEvent)
- expect(event.project).to eq(project)
- expect(event.action).to eq(Event::PUSHED)
- expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
- expect(event.push_event_payload.commit_from).to be_nil
- expect(event.push_event_payload.commit_to).to eq(newrev)
- expect(event.push_event_payload.ref).to eq('master')
- expect(event.push_event_payload.commit_count).to be > 1
- end
- end
-
- context "Updates merge requests" do
- it "when pushing a new branch for the first time" do
- expect(UpdateMergeRequestsWorker).to receive(:perform_async)
- .with(project.id, user.id, blankrev, 'newrev', ref)
- execute_service(project, user, blankrev, 'newrev', ref )
- end
- end
-
- describe 'system hooks' do
- let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
- let!(:system_hooks_service) { SystemHooksService.new }
+ describe "Updates merge requests" do
+ it "when pushing a new branch for the first time" do
+ expect(UpdateMergeRequestsWorker)
+ .to receive(:perform_async)
+ .with(project.id, user.id, blankrev, 'newrev', ref)
- it "sends a system hook after pushing a branch" do
- allow(SystemHooksService).to receive(:new).and_return(system_hooks_service)
- allow(system_hooks_service).to receive(:execute_hooks)
-
- execute_service(project, user, oldrev, newrev, ref)
-
- expect(system_hooks_service).to have_received(:execute_hooks).with(push_data, :push_hooks)
- end
+ execute_service(project, user, blankrev, 'newrev', ref )
end
end
@@ -700,125 +597,64 @@ describe Git::BranchPushService, services: true do
end
end
- describe '#update_caches' do
- let(:service) do
- described_class.new(project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: ref)
- end
-
- context 'on the default branch' do
- before do
- allow(service).to receive(:default_branch?).and_return(true)
- end
-
- it 'flushes the caches of any special files that have been changed' do
- commit = double(:commit)
- diff = double(:diff, new_path: 'README.md')
-
- expect(commit).to receive(:raw_deltas)
- .and_return([diff])
-
- service.push_commits = [commit]
+ describe "CI environments" do
+ context 'create branch' do
+ let(:oldrev) { blankrev }
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, %i(readme), %i(commit_count repository_size))
+ it 'does nothing' do
+ expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- service.update_caches
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- context 'on a non-default branch' do
- before do
- allow(service).to receive(:default_branch?).and_return(false)
- end
-
- it 'does not flush any conditional caches' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], %i(commit_count repository_size))
- .and_call_original
+ context 'update branch' do
+ it 'does nothing' do
+ expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- service.update_caches
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- end
-
- describe '#process_commit_messages' do
- let(:service) do
- described_class.new(project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: ref)
- end
- it 'only schedules a limited number of commits' do
- service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true))
-
- expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
-
- service.process_commit_messages
- end
-
- it "skips commits which don't include cross-references" do
- service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)]
-
- expect(ProcessCommitWorker).not_to receive(:perform_async)
-
- service.process_commit_messages
- end
- end
-
- describe '#update_signatures' do
- let(:service) do
- described_class.new(
- project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: 'refs/heads/master'
- )
- end
+ context 'delete branch' do
+ let(:newrev) { blankrev }
- context 'when the commit has a signature' do
- context 'when the signature is already cached' do
- before do
- create(:gpg_signature, commit_sha: sample_commit.id)
+ it 'stops environments' do
+ expect_next_instance_of(::Ci::StopEnvironmentsService) do |stop_service|
+ expect(stop_service.project).to eq(project)
+ expect(stop_service.current_user).to eq(user)
+ expect(stop_service).to receive(:execute).with(branch)
end
- it 'does not queue a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
-
- execute_service(project, user, oldrev, newrev, ref)
- end
+ execute_service(project, user, oldrev, newrev, ref)
end
+ end
+ end
- context 'when the signature is not yet cached' do
- it 'queues a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id], project.id)
+ describe 'Hooks' do
+ context 'run on a branch' do
+ it 'delegates to Git::BranchHooksService' do
+ expect_next_instance_of(::Git::BranchHooksService) do |hooks_service|
+ expect(hooks_service.project).to eq(project)
+ expect(hooks_service.current_user).to eq(user)
+ expect(hooks_service.params).to include(
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ )
- execute_service(project, user, oldrev, newrev, ref)
+ expect(hooks_service).to receive(:execute)
end
- it 'can queue several commits to create the gpg signature' do
- allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).and_return([sample_commit.id, another_sample_commit.id])
-
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id, another_sample_commit.id], project.id)
-
- execute_service(project, user, oldrev, newrev, ref)
- end
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- context 'when the commit does not have a signature' do
- before do
- allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([])
- end
+ context 'run on a tag' do
+ let(:ref) { 'refs/tags/v1.1.0' }
- it 'does not queue a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id)
+ it 'does nothing' do
+ expect(::Git::BranchHooksService).not_to receive(:new)
execute_service(project, user, oldrev, newrev, ref)
end
@@ -830,8 +666,4 @@ describe Git::BranchPushService, services: true do
service.execute
service
end
-
- def push_data_from_service(project, user, oldrev, newrev, ref)
- execute_service(project, user, oldrev, newrev, ref).push_data
- end
end
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
new file mode 100644
index 00000000000..8f91ce3b4c5
--- /dev/null
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Git::TagHooksService, :service do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { "refs/tags/#{tag_name}" }
+ let(:tag_name) { 'v1.1.0' }
+
+ let(:tag) { project.repository.find_tag(tag_name) }
+ let(:commit) { tag.dereferenced_target }
+
+ let(:service) do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ describe 'System hooks' do
+ it 'Executes system hooks' do
+ push_data = service.execute
+
+ expect_next_instance_of(SystemHooksService) do |system_hooks_service|
+ expect(system_hooks_service)
+ .to receive(:execute_hooks)
+ .with(push_data, :tag_push_hooks)
+ end
+
+ service.execute
+ end
+ end
+
+ describe "Webhooks" do
+ it "executes hooks on the project" do
+ expect(project).to receive(:execute_hooks)
+
+ service.execute
+ end
+ end
+
+ describe "Pipelines" do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+ project.add_developer(user)
+ end
+
+ it "creates a new pipeline" do
+ expect { service.execute }.to change { Ci::Pipeline.count }
+
+ expect(Ci::Pipeline.last).to be_push
+ end
+ end
+
+ describe 'Push data' do
+ shared_examples_for 'tag push data expectations' do
+ subject(:push_data) { service.execute }
+ it 'has expected push data attributes' do
+ is_expected.to match a_hash_including(
+ object_kind: 'tag_push',
+ ref: ref,
+ before: oldrev,
+ after: newrev,
+ message: tag.message,
+ user_id: user.id,
+ user_name: user.name,
+ project_id: project.id
+ )
+ end
+
+ context "with repository data" do
+ subject { push_data[:repository] }
+
+ it 'has expected repository attributes' do
+ is_expected.to match a_hash_including(
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ )
+ end
+ end
+
+ context "with commits" do
+ subject { push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { push_data[:commits].first }
+
+ it { is_expected.to include(timestamp: commit.date.xmlschema) }
+
+ it 'has expected commit attributes' do
+ is_expected.to match a_hash_including(
+ id: commit.id,
+ message: commit.safe_message,
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with an author" do
+ subject { push_data[:commits].first[:author] }
+
+ it 'has expected author attributes' do
+ is_expected.to match a_hash_including(
+ name: commit.author_name,
+ email: commit.author_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'annotated tag' do
+ include_examples 'tag push data expectations'
+ end
+
+ context 'lightweight tag' do
+ let(:tag_name) { 'light-tag' }
+ let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+
+ before do
+ # Create the lightweight tag
+ rugged_repo(project.repository).tags.create(tag_name, newrev)
+
+ # Clear tag list cache
+ project.repository.expire_tags_cache
+ end
+
+ include_examples 'tag push data expectations'
+ end
+ end
+end
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index e151db5827f..5e89a912060 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -31,164 +31,27 @@ describe Git::TagPushService do
end
end
- describe "Pipelines" do
- subject { service.execute }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- project.add_developer(user)
- end
-
- it "creates a new pipeline" do
- expect { subject }.to change { Ci::Pipeline.count }
- expect(Ci::Pipeline.last).to be_push
- end
- end
-
- describe "Git Tag Push Data" do
- subject { @push_data }
- let(:tag) { project.repository.find_tag(tag_name) }
- let(:commit) { tag.dereferenced_target }
-
- context 'annotated tag' do
- let(:tag_name) { Gitlab::Git.ref_name(ref) }
-
- before do
- service.execute
- @push_data = service.push_data
- end
-
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { @push_data[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
+ describe 'Hooks' do
+ context 'run on a tag' do
+ it 'delegates to Git::TagHooksService' do
+ expect_next_instance_of(::Git::TagHooksService) do |hooks_service|
+ expect(hooks_service.project).to eq(service.project)
+ expect(hooks_service.current_user).to eq(service.current_user)
+ expect(hooks_service.params).to eq(service.params)
+
+ expect(hooks_service).to receive(:execute)
end
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { is_expected.to include(timestamp: commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
- end
- end
-
- context 'lightweight tag' do
- let(:tag_name) { 'light-tag' }
- let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
- let(:ref) { "refs/tags/light-tag" }
-
- before do
- # Create the lightweight tag
- rugged_repo(project.repository).tags.create(tag_name, newrev)
-
- # Clear tag list cache
- project.repository.expire_tags_cache
-
service.execute
- @push_data = service.push_data
- end
-
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { @push_data[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { is_expected.to include(timestamp: commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
end
end
- end
- describe "Webhooks" do
- context "execute webhooks" do
- let(:service) { described_class.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') }
+ context 'run on a branch' do
+ let(:ref) { 'refs/heads/master' }
+
+ it 'does nothing' do
+ expect(::Git::BranchHooksService).not_to receive(:new)
- it "when pushing tags" do
- expect(project).to receive(:execute_hooks)
service.execute
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/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
new file mode 100644
index 00000000000..686b4b49f24
--- /dev/null
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -0,0 +1,404 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::PushOptionsHandlerService do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:service) { described_class.new(project, user, changes, push_options) }
+ let(:source_branch) { 'fix' }
+ let(:target_branch) { 'feature' }
+ let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
+ let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" }
+
+ before do
+ project.add_developer(user)
+ end
+
+ shared_examples_for 'a service that can create a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request' do
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'sets the correct target branch' do
+ branch = push_options[:target] || project.default_branch
+
+ service.execute
+
+ expect(last_mr.target_branch).to eq(branch)
+ end
+
+ it 'assigns the MR to the user' do
+ service.execute
+
+ expect(last_mr.assignee).to eq(user)
+ end
+
+ context 'when project has been forked' do
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:service) { described_class.new(forked_project, user, changes, push_options) }
+
+ before do
+ allow(forked_project).to receive(:empty_repo?).and_return(false)
+ end
+
+ it 'sets the correct source project' do
+ service.execute
+
+ expect(last_mr.source_project).to eq(forked_project)
+ end
+
+ it 'sets the correct target project' do
+ service.execute
+
+ expect(last_mr.target_project).to eq(project)
+ end
+ end
+ end
+
+ shared_examples_for 'a service that can set the target of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets the target_branch' do
+ service.execute
+
+ expect(last_mr.target_branch).to eq(target_branch)
+ end
+ end
+
+ shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets merge_when_pipeline_succeeds' do
+ service.execute
+
+ expect(last_mr.merge_when_pipeline_succeeds).to eq(true)
+ end
+
+ it 'sets merge_user to the user' do
+ service.execute
+
+ expect(last_mr.merge_user).to eq(user)
+ end
+ end
+
+ shared_examples_for 'a service that does not create a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.count }
+ end
+ end
+
+ shared_examples_for 'a service that does not update a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.maximum(:updated_at) }
+ end
+ end
+
+ shared_examples_for 'a service that does nothing' do
+ include_examples 'a service that does not create a merge request'
+ include_examples 'a service that does not update a merge request'
+ end
+
+ describe '`create` push option' do
+ let(:push_options) { { create: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`merge_when_pipeline_succeeds` push option' do
+ let(:push_options) { { merge_when_pipeline_succeeds: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`target` push option' do
+ let(:push_options) { { target: target_branch } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe 'multiple pushed branches' do
+ let(:push_options) { { create: true } }
+ let(:changes) do
+ [
+ new_branch_changes,
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/feature_conflict"
+ ]
+ end
+
+ it 'creates a merge request per branch' do
+ expect { service.execute }.to change { MergeRequest.count }.by(2)
+ end
+
+ context 'when there are too many pushed branches' do
+ let(:limit) { MergeRequests::PushOptionsHandlerService::LIMIT }
+ let(:changes) do
+ TestEnv::BRANCH_SHA.to_a[0..limit].map do |x|
+ "#{Gitlab::Git::BLANK_SHA} #{x.first} refs/heads/#{x.last}"
+ end
+ end
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(["Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})"])
+ end
+ end
+ end
+
+ describe 'no push options' do
+ let(:push_options) { {} }
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ describe 'no user' do
+ let(:user) { nil }
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(['User is required'])
+ end
+ end
+
+ describe 'unauthorized user' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id))
+
+ service.execute
+
+ expect(service.errors).to eq(['User access was denied'])
+ end
+ end
+
+ describe 'handling unexpected exceptions' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+ let(:exception) { StandardError.new('My standard error') }
+
+ def run_service_with_exception
+ allow_any_instance_of(
+ MergeRequests::BuildService
+ ).to receive(:execute).and_raise(exception)
+
+ service.execute
+ end
+
+ it 'records an error' do
+ run_service_with_exception
+
+ expect(service.errors).to eq(['An unknown error occurred'])
+ end
+
+ it 'writes to Gitlab::AppLogger' do
+ expect(Gitlab::AppLogger).to receive(:error).with(exception)
+
+ run_service_with_exception
+ end
+ end
+
+ describe 'when target is not a valid branch name' do
+ let(:push_options) { { create: true, target: 'my-branch' } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(['Branch my-branch does not exist'])
+ end
+ end
+
+ describe 'when MRs are not enabled' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ expect(project).to receive(:merge_requests_enabled?).and_return(false)
+
+ service.execute
+
+ expect(service.errors).to eq(["Merge requests are not enabled for project #{project.full_path}"])
+ end
+ end
+
+ describe 'when MR has ActiveRecord errors' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'adds the error to its errors property' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ service.execute
+
+ expect(service.errors).to eq(['my error'])
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 25cbac6d7ee..5ed06df7072 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -146,7 +146,10 @@ describe MergeRequests::RefreshService do
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
- subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
+ subject { service.new(project, @user).execute(@oldrev, @newrev, ref) }
+
+ let(:ref) { 'refs/heads/master' }
+ let(:project) { @project }
context "when .gitlab-ci.yml has merge_requests keywords" do
let(:config) do
@@ -162,14 +165,17 @@ describe MergeRequests::RefreshService do
it 'create detached merge request pipeline with commits' do
expect { subject }
.to change { @merge_request.merge_request_pipelines.count }.by(1)
- .and change { @fork_merge_request.merge_request_pipelines.count }.by(1)
.and change { @another_merge_request.merge_request_pipelines.count }.by(0)
expect(@merge_request.has_commits?).to be_truthy
- expect(@fork_merge_request.has_commits?).to be_truthy
expect(@another_merge_request.has_commits?).to be_falsy
end
+ it 'does not create detached merge request pipeline for forked project' do
+ expect { subject }
+ .not_to change { @fork_merge_request.merge_request_pipelines.count }
+ end
+
it 'create detached merge request pipeline for non-fork merge request' do
subject
@@ -177,11 +183,25 @@ describe MergeRequests::RefreshService do
.to be_detached_merge_request_pipeline
end
- it 'create legacy detached merge request pipeline for fork merge request' do
- subject
+ context 'when service is hooked by target branch' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'does not create detached merge request pipeline' do
+ expect { subject }
+ .not_to change { @merge_request.merge_request_pipelines.count }
+ end
+ end
+
+ context 'when service runs on forked project' do
+ let(:project) { @fork_project }
- expect(@fork_merge_request.merge_request_pipelines.first)
- .to be_legacy_detached_merge_request_pipeline
+ it 'creates legacy detached merge request pipeline for fork merge request' do
+ expect { subject }
+ .to change { @fork_merge_request.merge_request_pipelines.count }.by(1)
+
+ expect(@fork_merge_request.merge_request_pipelines.first)
+ .to be_legacy_detached_merge_request_pipeline
+ end
end
context 'when ci_use_merge_request_ref feature flag is false' do
@@ -230,17 +250,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 +474,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/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index a6cc2251e48..f6ee15f750c 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -21,16 +21,20 @@ describe NoteSummary do
describe '#note' do
it 'returns note hash' do
- expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note')
+ Timecop.freeze do
+ expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note',
+ created_at: Time.now)
+ end
end
context 'when noteable is a commit' do
- let(:noteable) { build(:commit) }
+ let(:noteable) { build(:commit, system_note_timestamp: Time.at(43)) }
it 'returns note hash specific to commit' do
expect(create_note_summary.note).to eq(
noteable: nil, project: project, author: user, note: 'note',
- noteable_type: 'Commit', commit_id: noteable.id
+ noteable_type: 'Commit', commit_id: noteable.id,
+ created_at: Time.at(43)
)
end
end
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/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
new file mode 100644
index 00000000000..7e351c9ce54
--- /dev/null
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Projects::UpdateStatisticsService do
+ let(:service) { described_class.new(project, nil, statistics: statistics)}
+ let(:statistics) { %w(repository_size) }
+
+ describe '#execute' do
+ context 'with a non-existing project' do
+ let(:project) { nil }
+
+ it 'does nothing' do
+ expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
+
+ service.execute
+ end
+ end
+
+ context 'with an existing project without a repository' do
+ let(:project) { create(:project) }
+
+ it 'does nothing' do
+ expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
+
+ service.execute
+ end
+ end
+
+ context 'with an existing project with a repository' do
+ let(:project) { create(:project, :repository) }
+
+ it 'refreshes the project statistics' do
+ expect_any_instance_of(ProjectStatistics).to receive(:refresh!)
+ .with(only: statistics.map(&:to_sym))
+ .and_call_original
+
+ service.execute
+ end
+ end
+ end
+end
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/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 8b0f9c8ade2..c7e5cca324f 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -10,6 +10,7 @@ describe QuickActions::InterpretService do
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:commit) { create(:commit, project: project) }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
+ let(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') }
let(:bug) { create(:label, project: project, title: 'Bug') }
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
let(:service) { described_class.new(project, developer) }
@@ -94,6 +95,26 @@ describe QuickActions::InterpretService do
end
end
+ shared_examples 'multiword label name starting without ~' do
+ it 'fetches label ids and populates add_label_ids if content contains /label' do
+ helmchart # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [helmchart.id])
+ end
+ end
+
+ shared_examples 'label name is included in the middle of another label name' do
+ it 'ignores the sublabel when the content contains the includer label name' do
+ helmchart # populate the label
+ create(:label, project: project, title: 'Chart')
+
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [helmchart.id])
+ end
+ end
+
shared_examples 'unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
issuable.update!(label_ids: [inprogress.id]) # populate the label
@@ -624,6 +645,26 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'multiword label name starting without ~' do
+ let(:content) { %(/label "#{helmchart.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'multiword label name starting without ~' do
+ let(:content) { %(/label "#{helmchart.title}") }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'label name is included in the middle of another label name' do
+ let(:content) { %(/label ~"#{helmchart.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'label name is included in the middle of another label name' do
+ let(:content) { %(/label ~"#{helmchart.title}") }
+ let(:issuable) { merge_request }
+ end
+
it_behaves_like 'unlabel command' do
let(:content) { %(/unlabel ~"#{inprogress.title}") }
let(:issuable) { issue }
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 612e9f152e7..0efe37f1167 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -19,6 +19,8 @@ describe Releases::CreateService do
shared_examples 'a successful release creation' do
it 'creates a new release' do
result = service.execute
+
+ expect(project.releases.count).to eq(1)
expect(result[:status]).to eq(:success)
expect(result[:tag]).not_to be_nil
expect(result[:release]).not_to be_nil
@@ -69,4 +71,12 @@ describe Releases::CreateService do
end
end
end
+
+ describe '#find_or_build_release' do
+ it 'does not save the built release' do
+ service.find_or_build_release
+
+ expect(project.releases.count).to eq(0)
+ 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/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index b917de14b2e..13d7d795703 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SystemNoteService do
+ include ProjectForksHelper
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
@@ -11,6 +14,14 @@ describe SystemNoteService do
let(:noteable) { create(:issue, project: project) }
let(:issue) { noteable }
+ shared_examples_for 'a note with overridable created_at' do
+ let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) }
+
+ it 'the note has the correct time' do
+ expect(subject.created_at).to eq Time.at(42)
+ end
+ end
+
shared_examples_for 'a system note' do
let(:expected_noteable) { noteable }
let(:commit_count) { nil }
@@ -137,6 +148,8 @@ describe SystemNoteService do
end
context 'when assignee added' do
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq "assigned to @#{assignee.username}"
end
@@ -145,6 +158,8 @@ describe SystemNoteService do
context 'when assignee removed' do
let(:assignee) { nil }
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq 'removed assignee'
end
@@ -168,6 +183,8 @@ describe SystemNoteService do
described_class.change_issue_assignees(issue, project, author, old_assignees).note
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
end
@@ -213,6 +230,8 @@ describe SystemNoteService do
expect(subject.note).to eq "changed milestone to #{reference}"
end
+
+ it_behaves_like 'a note with overridable created_at'
end
context 'when milestone removed' do
@@ -221,6 +240,8 @@ describe SystemNoteService do
it 'sets the note text' do
expect(subject.note).to eq 'removed milestone'
end
+
+ it_behaves_like 'a note with overridable created_at'
end
end
@@ -237,6 +258,8 @@ describe SystemNoteService do
it 'sets the note text to use the milestone name' do
expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
end
+
+ it_behaves_like 'a note with overridable created_at'
end
context 'when milestone removed' do
@@ -245,6 +268,8 @@ describe SystemNoteService do
it 'sets the note text' do
expect(subject.note).to eq 'removed milestone'
end
+
+ it_behaves_like 'a note with overridable created_at'
end
end
end
@@ -254,6 +279,8 @@ describe SystemNoteService do
let(:due_date) { Date.today }
+ it_behaves_like 'a note with overridable created_at'
+
it_behaves_like 'a system note' do
let(:action) { 'due_date' }
end
@@ -280,6 +307,8 @@ describe SystemNoteService do
let(:status) { 'reopened' }
let(:source) { nil }
+ it_behaves_like 'a note with overridable created_at'
+
it_behaves_like 'a system note' do
let(:action) { 'opened' }
end
@@ -289,6 +318,8 @@ describe SystemNoteService do
let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') }
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq "#{status} via commit 123456"
end
@@ -338,6 +369,8 @@ describe SystemNoteService do
let(:action) { 'title' }
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note)
.to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
@@ -353,6 +386,8 @@ describe SystemNoteService do
let(:action) { 'description' }
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq('changed the description')
end
@@ -478,6 +513,8 @@ describe SystemNoteService do
let(:action) { 'cross_reference' }
end
+ it_behaves_like 'a note with overridable created_at'
+
describe 'note_body' do
context 'cross-project' do
let(:project2) { create(:project, :repository) }
@@ -619,7 +656,7 @@ describe SystemNoteService do
context 'commit with cross-reference from fork' do
let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
- let(:forked_project) { Projects::ForkService.new(project, author2).execute }
+ let(:forked_project) { fork_project(project, author2, repository: true) }
let(:commit2) { forked_project.commit }
before do
@@ -896,6 +933,28 @@ describe SystemNoteService do
end
end
+ describe '.change_time_estimate' do
+ subject { described_class.change_time_estimate(noteable, project, author) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'time_tracking' }
+ end
+
+ context 'with a time estimate' do
+ it 'sets the note text' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ end
+ end
+
+ context 'without a time estimate' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "removed time estimate"
+ end
+ end
+ end
+
describe '.discussion_continued_in_issue' do
let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
let(:merge_request) { discussion.noteable }
@@ -922,28 +981,6 @@ describe SystemNoteService do
end
end
- describe '.change_time_estimate' do
- subject { described_class.change_time_estimate(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'time_tracking' }
- end
-
- context 'with a time estimate' do
- it 'sets the note text' do
- noteable.update_attribute(:time_estimate, 277200)
-
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
- end
- end
-
- context 'without a time estimate' do
- it 'sets the note text' do
- expect(subject.note).to eq "removed time estimate"
- end
- end
- end
-
describe '.change_time_spent' do
# We need a custom noteable in order to the shared examples to be green.
let(:noteable) 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/spec_helper.rb b/spec/spec_helper.rb
index 60db3e1bc46..953cf1519bb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -100,8 +100,8 @@ RSpec.configure do |config|
config.include PolicyHelpers, type: :policy
if ENV['CI']
- # This includes the first try, i.e. tests will be run 4 times before failing.
- config.default_retry_count = 4
+ # This includes the first try, i.e. tests will be run 2 times before failing.
+ config.default_retry_count = 2
config.reporter.register_listener(
RspecFlaky::Listener.new,
:example_passed,
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/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 6569feec39b..03057a102c5 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -149,4 +149,10 @@ module FilteredSearchHelpers
loop until find('.filtered-search').value.strip == text
end
end
+
+ def close_dropdown_menu_if_visible
+ find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
+ toggle.click if toggle.visible?
+ 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 8bcd26ec0cd..ee5cfcd850d 100644
--- a/spec/support/shared_context/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_context/policies/project_policy_shared_context.rb
@@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do
read_project_for_iids read_issue_iid read_label
read_milestone read_project_snippet read_project_member read_note
create_project create_issue create_note upload_file create_merge_request_in
- award_emoji read_release
+ award_emoji
]
end
@@ -24,7 +24,8 @@ RSpec.shared_context 'ProjectPolicy context' do
download_code fork_project create_project_snippet update_issue
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_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/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
index 5bfc3bb222f..34dba5dbc31 100644
--- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -1,4 +1,58 @@
# frozen_string_literal: true
shared_examples 'create_merge_request quick action' do
+ context 'create a merge request starting from an issue' do
+ def expect_mr_quickaction(success)
+ expect(page).to have_content 'Commands applied'
+
+ if success
+ expect(page).to have_content 'created merge request'
+ else
+ expect(page).not_to have_content 'created merge request'
+ end
+ end
+
+ it "doesn't create a merge request when the branch name is invalid" do
+ add_note("/create_merge_request invalid branch name")
+
+ wait_for_requests
+
+ expect_mr_quickaction(false)
+ end
+
+ it "doesn't create a merge request when a branch with that name already exists" do
+ add_note("/create_merge_request feature")
+
+ wait_for_requests
+
+ expect_mr_quickaction(false)
+ end
+
+ it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
+ add_note("/create_merge_request")
+
+ wait_for_requests
+
+ expect_mr_quickaction(true)
+
+ created_mr = project.merge_requests.last
+ expect(created_mr.source_branch).to eq(issue.to_branch_name)
+
+ visit project_merge_request_path(project, created_mr)
+ expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
+ end
+
+ it 'creates a merge request using the given branch name' do
+ branch_name = '1-feature'
+ add_note("/create_merge_request #{branch_name}")
+
+ expect_mr_quickaction(true)
+
+ created_mr = project.merge_requests.last
+ expect(created_mr.source_branch).to eq(branch_name)
+
+ visit project_merge_request_path(project, created_mr)
+ expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
index db3ecccc339..ae78cd86cd5 100644
--- a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
@@ -1,25 +1,35 @@
# frozen_string_literal: true
-shared_examples 'due quick action not available' do
- it 'does not set the due date' do
- add_note('/due 2016-08-28')
+shared_examples 'due quick action' do
+ context 'due quick action available and date can be added' do
+ it 'sets the due date accordingly' do
+ add_note('/due 2016-08-28')
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content '/due 2016-08-28'
- end
-end
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Commands applied'
+
+ visit project_issue_path(project, issue)
-shared_examples 'due quick action available and date can be added' do
- it 'sets the due date accordingly' do
- add_note('/due 2016-08-28')
+ page.within '.due_date' do
+ expect(page).to have_content 'Aug 28, 2016'
+ end
+ end
+ end
- expect(page).not_to have_content '/due 2016-08-28'
- expect(page).to have_content 'Commands applied'
+ context 'due quick 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 set the due date' do
+ add_note('/due 2016-08-28')
- page.within '.due_date' do
- expect(page).to have_content 'Aug 28, 2016'
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content '/due 2016-08-28'
end
end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
index 24576fe0021..633c7135fbc 100644
--- a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
@@ -1,4 +1,38 @@
# frozen_string_literal: true
shared_examples 'duplicate quick action' do
+ context 'mark issue as duplicate' do
+ 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 a duplicate' do
+ add_note("/duplicate ##{original_issue.to_reference}")
+
+ expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
+
+ expect(issue.reload).to be_closed
+ 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 a duplicate' do
+ add_note("/duplicate ##{original_issue.to_reference}")
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
+
+ expect(issue.reload).to be_open
+ end
+ 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/quick_actions/merge_request/wip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
index 6abb12b41b2..60d6e53e74f 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
@@ -1,4 +1,47 @@
# frozen_string_literal: true
shared_examples 'wip quick action' do
+ context 'when the current user can toggle the WIP prefix' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ end
+
+ it 'adds the WIP: prefix to the title' do
+ add_note('/wip')
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload.work_in_progress?).to eq true
+ end
+
+ it 'removes the WIP: prefix from the title' do
+ merge_request.update!(title: merge_request.wip_title)
+ add_note('/wip')
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+
+ context 'when the current user cannot toggle the WIP prefix' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not change the WIP prefix' do
+ add_note('/wip')
+
+ expect(page).not_to have_content '/wip'
+ expect(page).not_to have_content 'Commands applied'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ 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/support/shared_examples/time_tracking_shared_examples.rb b/spec/support/shared_examples/time_tracking_shared_examples.rb
deleted file mode 100644
index 909d4e2ee8d..00000000000
--- a/spec/support/shared_examples/time_tracking_shared_examples.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-shared_examples 'issuable time tracker' do
- it 'renders the sidebar component empty state' do
- page.within '.time-tracking-no-tracking-pane' do
- expect(page).to have_content 'No estimate or time spent'
- end
- end
-
- it 'updates the sidebar component when estimate is added' do
- submit_time('/estimate 3w 1d 1h')
-
- wait_for_requests
- page.within '.time-tracking-estimate-only-pane' do
- expect(page).to have_content '3w 1d 1h'
- end
- end
-
- it 'updates the sidebar component when spent is added' do
- submit_time('/spend 3w 1d 1h')
-
- wait_for_requests
- page.within '.time-tracking-spend-only-pane' do
- expect(page).to have_content '3w 1d 1h'
- end
- end
-
- it 'shows the comparison when estimate and spent are added' do
- submit_time('/estimate 3w 1d 1h')
- submit_time('/spend 3w 1d 1h')
-
- wait_for_requests
- page.within '.time-tracking-comparison-pane' do
- expect(page).to have_content '3w 1d 1h'
- end
- end
-
- it 'updates the sidebar component when estimate is removed' do
- submit_time('/estimate 3w 1d 1h')
- submit_time('/remove_estimate')
-
- page.within '.time-tracking-component-wrap' do
- expect(page).to have_content 'No estimate or time spent'
- end
- end
-
- it 'updates the sidebar component when spent is removed' do
- submit_time('/spend 3w 1d 1h')
- submit_time('/remove_time_spent')
-
- page.within '.time-tracking-component-wrap' do
- expect(page).to have_content 'No estimate or time spent'
- end
- end
-
- it 'shows the help state when icon is clicked' do
- page.within '.time-tracking-component-wrap' do
- find('.help-button').click
- expect(page).to have_content 'Track time with quick actions'
- expect(page).to have_content 'Learn more'
- end
- end
-
- it 'hides the help state when close icon is clicked' do
- page.within '.time-tracking-component-wrap' do
- find('.help-button').click
- find('.close-help-button').click
-
- expect(page).not_to have_content 'Track time with quick actions'
- expect(page).not_to have_content 'Learn more'
- end
- end
-
- it 'displays the correct help url' do
- page.within '.time-tracking-component-wrap' do
- find('.help-button').click
-
- expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
- end
- end
-end
-
-def submit_time(quick_action)
- fill_in 'note[note]', with: quick_action
- find('.js-comment-submit-button').click
- wait_for_requests
-end
diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb
index b6fb2a66b0e..22fbfb48928 100644
--- a/spec/support/shared_examples/wiki_file_attachments_examples.rb
+++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb
@@ -42,7 +42,7 @@ shared_examples 'wiki file attachments' do
end
end
- context 'uploading is complete' do
+ context 'uploading is complete', :quarantine do
it 'shows "Attach a file" button on uploading complete' do
attach_with_dropzone
wait_for_requests
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/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 66958a4c116..a3fe8fa4501 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -63,8 +63,12 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést" }
it "calls Git::BranchPushService" do
- expect_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true)
- expect_any_instance_of(Git::TagPushService).not_to receive(:execute)
+ expect_next_instance_of(Git::BranchPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
+ expect(Git::TagPushService).not_to receive(:new)
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -73,8 +77,12 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/tags/tag" }
it "calls Git::TagPushService" do
- expect_any_instance_of(Git::BranchPushService).not_to receive(:execute)
- expect_any_instance_of(Git::TagPushService).to receive(:execute).and_return(true)
+ expect(Git::BranchPushService).not_to receive(:execute)
+
+ expect_next_instance_of(Git::TagPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -83,8 +91,9 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/merge-requests/123" }
it "does not call any of the services" do
- expect_any_instance_of(Git::BranchPushService).not_to receive(:execute)
- expect_any_instance_of(Git::TagPushService).not_to receive(:execute)
+ expect(Git::BranchPushService).not_to receive(:new)
+ expect(Git::TagPushService).not_to receive(:new)
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -127,7 +136,9 @@ describe PostReceive do
allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
# silence hooks so we can isolate
allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true)
+ expect_next_instance_of(Git::BranchPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
end
it 'calls SystemHooksService' do
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index a7353227043..3c40269adc7 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -7,9 +7,9 @@ describe ProjectCacheWorker do
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
- let(:statistics) { project.statistics }
- let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" }
+ let(:lease_key) { ["project_cache_worker", project.id, *statistics.sort].join(":") }
let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT }
+ let(:statistics) { [] }
describe '#perform' do
before do
@@ -35,14 +35,6 @@ describe ProjectCacheWorker do
end
context 'with an existing project' do
- it 'updates the project statistics' do
- expect(worker).to receive(:update_statistics)
- .with(kind_of(Project), %i(repository_size))
- .and_call_original
-
- worker.perform(project.id, [], %w(repository_size))
- end
-
it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
.with(%i(readme))
@@ -51,6 +43,18 @@ describe ProjectCacheWorker do
worker.perform(project.id, %w(readme))
end
+ context 'with statistics' do
+ let(:statistics) { %w(repository_size) }
+
+ it 'updates the project statistics' do
+ expect(worker).to receive(:update_statistics)
+ .with(kind_of(Project), statistics)
+ .and_call_original
+
+ worker.perform(project.id, [], statistics)
+ end
+ end
+
context 'with plain readme' do
it 'refreshes the method caches' do
allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false)
@@ -66,25 +70,34 @@ describe ProjectCacheWorker do
end
describe '#update_statistics' do
+ let(:statistics) { %w(repository_size) }
+
context 'when a lease could not be obtained' do
- it 'does not update the repository size' do
+ it 'does not update the project statistics' do
stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(statistics).not_to receive(:refresh!)
+ expect(Projects::UpdateStatisticsService).not_to receive(:new)
- worker.update_statistics(project)
+ expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in)
+
+ worker.update_statistics(project, statistics)
end
end
context 'when a lease could be obtained' do
- it 'updates the project statistics' do
+ it 'updates the project statistics twice' do
stub_exclusive_lease(lease_key, timeout: lease_timeout)
- expect(statistics).to receive(:refresh!)
- .with(only: %i(repository_size))
+ expect(Projects::UpdateStatisticsService).to receive(:new)
+ .with(project, nil, statistics: statistics)
+ .and_call_original
+ .twice
+
+ expect(UpdateProjectStatisticsWorker).to receive(:perform_in)
+ .with(lease_timeout, project.id, statistics)
.and_call_original
- worker.update_statistics(project, %i(repository_size))
+ worker.update_statistics(project, statistics)
end
end
end
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
new file mode 100644
index 00000000000..a268fd2e4ba
--- /dev/null
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe UpdateProjectStatisticsWorker do
+ let(:worker) { described_class.new }
+ let(:project) { create(:project, :repository) }
+ let(:statistics) { %w(repository_size) }
+
+ describe '#perform' do
+ it 'updates the project statistics' do
+ expect(Projects::UpdateStatisticsService).to receive(:new)
+ .with(project, nil, statistics: statistics)
+ .and_call_original
+
+ worker.perform(project.id, statistics)
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 581d5818ccc..41758181610 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -645,28 +645,28 @@
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":
- version "1.58.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f"
- integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg==
+"@gitlab/svgs@^1.59.0":
+ version "1.59.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087"
+ integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g==
-"@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.2.0":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.2.0.tgz#3a44ac806a22b87fe45e6edfa410cb9355164f04"
+ integrity sha512-If2ngMIw0jWAdQ1q3PfB8sDhCXz1r3DsRm1X5Vy767kZ2TeFd7SGBp5KP5ceMGGpQ4TTYU/V8IqYnQbTXfPKRw==
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==
@@ -2804,7 +2796,7 @@ d3-dsv@1, d3-dsv@1.0.8:
iconv-lite "0.4"
rw "1"
-d3-ease@1, d3-ease@1.0.3:
+d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4=
@@ -2920,7 +2912,7 @@ d3-timer@1, d3-timer@1.0.7:
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
integrity sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA==
-d3-transition@1, d3-transition@1.1.1:
+d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
integrity sha512-xeg8oggyQ+y5eb4J13iDgKIjUcEfIOZs2BqV/eEmXm2twx80wTzJ4tB4vaZ5BKfz7XsI/DFmQL5me6O27/5ykQ==
@@ -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"