summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/cng.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml8
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml11
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml118
-rw-r--r--.stylelintrc10
-rw-r--r--CHANGELOG.md58
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile9
-rw-r--r--Gemfile.lock22
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/batch_comments/mixins/resolved_status.js13
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js5
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js3
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js5
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js3
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js5
-rw-r--r--app/assets/javascripts/blob/sketch/index.js5
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js3
-rw-r--r--app/assets/javascripts/blob/viewer/index.js14
-rw-r--r--app/assets/javascripts/boards/boards_util.js7
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js3
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue2
-rw-r--r--app/assets/javascripts/boards/index.js4
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js3
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js44
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue114
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue84
-rw-r--r--app/assets/javascripts/clusters/constants.js13
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js141
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js45
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js3
-rw-r--r--app/assets/javascripts/diffs/workers/tree_worker.js5
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js3
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js4
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js21
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service_error.js4
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js10
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue1
-rw-r--r--app/assets/javascripts/ide/ide_router.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js5
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue2
-rw-r--r--app/assets/javascripts/import_projects/store/index.js2
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js9
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue1
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js4
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue1
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js2
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js4
-rw-r--r--app/assets/javascripts/mr_popover/constants.js8
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue1
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue155
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue1
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue190
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue3
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js8
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/utils.js7
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js24
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js1
-rw-r--r--app/assets/javascripts/projects/project_new.js35
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js3
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js7
-rw-r--r--app/assets/javascripts/raven/index.js7
-rw-r--r--app/assets/javascripts/raven/raven_config.js11
-rw-r--r--app/assets/javascripts/u2f/error.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js1
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss8
-rw-r--r--app/assets/stylesheets/components/toast.scss3
-rw-r--r--app/assets/stylesheets/framework/header.scss41
-rw-r--r--app/assets/stylesheets/pages/boards.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss5
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/clusters/applications_controller.rb13
-rw-r--r--app/controllers/projects/environments_controller.rb43
-rw-r--r--app/controllers/projects/issues_controller.rb12
-rw-r--r--app/controllers/projects/settings/operations_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb6
-rw-r--r--app/graphql/resolvers/group_resolver.rb13
-rw-r--r--app/graphql/resolvers/issues_resolver.rb6
-rw-r--r--app/graphql/types/group_type.rb21
-rw-r--r--app/graphql/types/namespace_type.rb19
-rw-r--r--app/graphql/types/permission_types/group.rb11
-rw-r--r--app/graphql/types/project_type.rb3
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/helpers/broadcast_messages_helper.rb2
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/helpers/mirror_helper.rb4
-rw-r--r--app/models/application_record.rb17
-rw-r--r--app/models/application_setting.rb10
-rw-r--r--app/models/application_setting_implementation.rb16
-rw-r--r--app/models/ci/bridge.rb3
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/pipeline.rb12
-rw-r--r--app/models/clusters/applications/cert_manager.rb6
-rw-r--r--app/models/clusters/applications/helm.rb7
-rw-r--r--app/models/clusters/applications/ingress.rb7
-rw-r--r--app/models/clusters/applications/jupyter.rb6
-rw-r--r--app/models/clusters/applications/knative.rb6
-rw-r--r--app/models/clusters/applications/prometheus.rb18
-rw-r--r--app/models/clusters/applications/runner.rb9
-rw-r--r--app/models/clusters/concerns/application_core.rb10
-rw-r--r--app/models/clusters/concerns/application_status.rb13
-rw-r--r--app/models/clusters/platforms/kubernetes.rb4
-rw-r--r--app/models/concerns/avatarable.rb3
-rw-r--r--app/models/concerns/ci/contextable.rb8
-rw-r--r--app/models/concerns/ci/pipeline_delegator.rb21
-rw-r--r--app/models/concerns/has_ref.rb3
-rw-r--r--app/models/deployment.rb17
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/merge_request.rb10
-rw-r--r--app/models/notification_recipient.rb20
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_metrics_setting.rb9
-rw-r--r--app/models/project_services/chat_message/deployment_message.rb69
-rw-r--r--app/models/project_services/chat_notification_service.rb4
-rw-r--r--app/models/project_services/discord_service.rb5
-rw-r--r--app/models/project_services/hangouts_chat_service.rb5
-rw-r--r--app/models/project_services/kubernetes_service.rb4
-rw-r--r--app/models/project_services/microsoft_teams_service.rb5
-rw-r--r--app/models/project_wiki.rb22
-rw-r--r--app/models/repository.rb22
-rw-r--r--app/models/service.rb3
-rw-r--r--app/presenters/ci/pipeline_presenter.rb12
-rw-r--r--app/presenters/merge_request_presenter.rb16
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/concerns/user_status_tooltip.rb2
-rw-r--r--app/services/ci/stop_environments_service.rb16
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb6
-rw-r--r--app/services/clusters/applications/check_uninstall_progress_service.rb62
-rw-r--r--app/services/clusters/applications/create_service.rb4
-rw-r--r--app/services/clusters/applications/destroy_service.rb23
-rw-r--r--app/services/clusters/applications/uninstall_service.rb29
-rw-r--r--app/services/clusters/applications/update_service.rb2
-rw-r--r--app/services/git/base_hooks_service.rb14
-rw-r--r--app/services/merge_requests/base_service.rb5
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/projects/import_service.rb13
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb92
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_object_download_list_service.rb96
-rw-r--r--app/services/projects/operations/update_service.rb11
-rw-r--r--app/services/tags/destroy_service.rb11
-rw-r--r--app/services/test_hooks/project_service.rb2
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb13
-rw-r--r--app/views/admin/application_settings/_logging.html.haml6
-rw-r--r--app/views/admin/application_settings/_pages.html.haml29
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml1
-rw-r--r--app/views/ci/variables/_variable_row.html.haml4
-rw-r--r--app/views/clusters/clusters/show.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml3
-rw-r--r--app/views/events/event/_note.html.haml3
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/help/index.html.haml3
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml3
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml1
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/default_branch/_show.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml4
-rw-r--r--app/views/projects/issues/_merge_requests_status.html.haml25
-rw-r--r--app/views/projects/issues/new.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml8
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml6
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/search/_results.html.haml5
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml4
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/clusters/applications/uninstall_worker.rb17
-rw-r--r--app/workers/clusters/applications/wait_for_uninstall_app_worker.rb20
-rw-r--r--app/workers/deployments/finished_worker.rb13
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb16
-rw-r--r--app/workers/pipeline_schedule_worker.rb28
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--babel.config.js5
-rw-r--r--changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml5
-rw-r--r--changelogs/unreleased/11254-overflow-ce.yml5
-rw-r--r--changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml5
-rw-r--r--changelogs/unreleased/46048-canary-next.yml5
-rw-r--r--changelogs/unreleased/48479-auto-direction-for-issue-title.yml5
-rw-r--r--changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml5
-rw-r--r--changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml5
-rw-r--r--changelogs/unreleased/55948-help-text-formatting-wiki.yml5
-rw-r--r--changelogs/unreleased/57017-add-toast-success-message.yml5
-rw-r--r--changelogs/unreleased/57171-add-dashboard-settings.yml5
-rw-r--r--changelogs/unreleased/58294-discussion-notes-component.yml5
-rw-r--r--changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml5
-rw-r--r--changelogs/unreleased/60552-period-dropdown.yml5
-rw-r--r--changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml6
-rw-r--r--changelogs/unreleased/60874-fix-suggestion-misalignment.yml5
-rw-r--r--changelogs/unreleased/61036-fix-ingress-base-domain-text.yml5
-rw-r--r--changelogs/unreleased/bw-add-graphql-groups.yml5
-rw-r--r--changelogs/unreleased/da-sentry-client-side-settings.yml5
-rw-r--r--changelogs/unreleased/feat-sentry-environment.yml5
-rw-r--r--changelogs/unreleased/fix-api-ide-relative-url-root.yml5
-rw-r--r--changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml6
-rw-r--r--changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml5
-rw-r--r--changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml5
-rw-r--r--changelogs/unreleased/fj-60827-fix-web-strategy-error.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.36.0.yml5
-rw-r--r--changelogs/unreleased/issue-42692-deployment-chat-notifications.yml5
-rw-r--r--changelogs/unreleased/jc-client-gitaly-session-id.yml5
-rw-r--r--changelogs/unreleased/jc-update-list-last-commits.yml5
-rw-r--r--changelogs/unreleased/pl-upgrade-letter_opener_web.yml5
-rw-r--r--changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/remove-disabled-pages-domains-part-2.yml5
-rw-r--r--changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml5
-rw-r--r--changelogs/unreleased/sh-disable-batch-load-replace-methods.yml5
-rw-r--r--changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml5
-rw-r--r--changelogs/unreleased/wiki-search-results-fix.yml5
-rw-r--r--config/gitlab.yml.example8
-rw-r--r--config/initializers/1_settings.rb15
-rw-r--r--config/initializers/sentry.rb1
-rw-r--r--config/initializers/sidekiq.rb17
-rw-r--r--config/karma.config.js2
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/project.rb2
-rw-r--r--danger/roulette/Dangerfile44
-rw-r--r--db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb15
-rw-r--r--db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb21
-rw-r--r--db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb17
-rw-r--r--db/migrate/20190422082247_create_project_metrics_settings.rb14
-rw-r--r--db/migrate/20190426180107_add_deployment_events_to_services.rb17
-rw-r--r--db/schema.rb11
-rw-r--r--doc/README.md16
-rw-r--r--doc/administration/container_registry.md7
-rw-r--r--doc/administration/custom_hooks.md65
-rw-r--r--doc/administration/high_availability/nfs.md9
-rw-r--r--doc/analytics/README.md5
-rw-r--r--doc/analytics/contribution_analytics.md5
-rw-r--r--doc/api/graphql/index.md6
-rw-r--r--doc/api/merge_requests.md90
-rw-r--r--doc/api/milestones.md2
-rw-r--r--doc/api/users.md2
-rw-r--r--doc/ci/README.md194
-rw-r--r--doc/ci/environments.md177
-rw-r--r--doc/ci/environments/protected_environments.md25
-rw-r--r--doc/ci/img/add_file_template_11_10.pngbin0 -> 55910 bytes
-rw-r--r--doc/ci/img/deployments_view.pngbin23352 -> 58498 bytes
-rw-r--r--doc/ci/img/environments_available.pngbin8464 -> 20410 bytes
-rw-r--r--doc/ci/img/environments_mr_review_app.pngbin13394 -> 30140 bytes
-rw-r--r--doc/ci/junit_test_reports.md6
-rw-r--r--doc/ci/pipelines.md3
-rw-r--r--doc/ci/variables/README.md10
-rw-r--r--doc/ci/variables/predefined_variables.md4
-rw-r--r--doc/ci/yaml/README.md16
-rw-r--r--doc/development/code_review.md25
-rw-r--r--doc/development/documentation/styleguide.md13
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md5
-rw-r--r--doc/development/testing_guide/img/review_apps_cicd_architecture.pngbin73240 -> 136431 bytes
-rw-r--r--doc/development/testing_guide/review_apps.md162
-rw-r--r--doc/gitlab-basics/README.md46
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/topics/autodevops/index.md1
-rw-r--r--doc/topics/git/troubleshooting_git.md27
-rw-r--r--doc/user/admin_area/index.md46
-rw-r--r--doc/user/group/index.md23
-rw-r--r--doc/user/project/clusters/index.md24
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md4
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/labels.md2
-rw-r--r--doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.pngbin0 -> 20867 bytes
-rw-r--r--doc/user/project/merge_requests/index.md22
-rw-r--r--doc/user/project/pages/getting_started_part_four.md2
-rw-r--r--doc/user/project/quick_actions.md7
-rw-r--r--jest.config.js1
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/events.rb49
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/events_helpers.rb31
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/project_events.rb29
-rw-r--r--lib/api/wikis.rb3
-rw-r--r--lib/gitlab/action_view_output/context.rb41
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml26
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml30
-rw-r--r--lib/gitlab/ci/templates/dotNET-Core.yml107
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb6
-rw-r--r--lib/gitlab/danger/helper.rb8
-rw-r--r--lib/gitlab/data_builder/deployment.rb23
-rw-r--r--lib/gitlab/data_builder/push.rb12
-rw-r--r--lib/gitlab/file_detector.rb1
-rw-r--r--lib/gitlab/git/object_pool.rb4
-rw-r--r--lib/gitlab/git/repository.rb6
-rw-r--r--lib/gitlab/git/wiki.rb24
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb8
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb4
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/object_pool_service.rb9
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb23
-rw-r--r--lib/gitlab/gl_repository.rb12
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb2
-rw-r--r--lib/gitlab/gon_helper.rb7
-rw-r--r--lib/gitlab/graphql/generic_tracing.rb (renamed from lib/gitlab/graphql/tracing.rb)22
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/relation_factory.rb3
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb7
-rw-r--r--lib/gitlab/metrics/dashboard/base_service.rb73
-rw-r--r--lib/gitlab/metrics/dashboard/finder.rb51
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb46
-rw-r--r--lib/gitlab/metrics/dashboard/project_dashboard_service.rb47
-rw-r--r--lib/gitlab/metrics/dashboard/stages/base_stage.rb58
-rw-r--r--lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb23
-rw-r--r--lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb106
-rw-r--r--lib/gitlab/metrics/dashboard/stages/sorter.rb34
-rw-r--r--lib/gitlab/metrics/dashboard/system_dashboard_service.rb47
-rw-r--r--lib/gitlab/profiler.rb2
-rw-r--r--lib/gitlab/prometheus/query_variables.rb6
-rw-r--r--lib/gitlab/sidekiq_config.rb4
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--locale/gitlab.pot257
-rw-r--r--package.json7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb10
-rw-r--r--qa/qa/specs/helpers/quarantine.rb16
-rw-r--r--qa/qa/vendor/github/page/login.rb4
-rw-r--r--rubocop/cop/include_action_view_context.rb31
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/lint-doc.sh2
-rwxr-xr-xscripts/review_apps/review-apps.sh98
-rw-r--r--scripts/utils.sh136
-rw-r--r--security.txt6
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb62
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb122
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb224
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb22
-rw-r--r--spec/controllers/search_controller_spec.rb24
-rw-r--r--spec/factories/clusters/applications/helm.rb19
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/pages_domains.rb4
-rw-r--r--spec/factories/project_metrics_settings.rb8
-rw-r--r--spec/features/admin/admin_settings_spec.rb46
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb6
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb46
-rw-r--r--spec/features/protected_branches_spec.rb26
-rw-r--r--spec/features/protected_tags_spec.rb13
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb36
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml36
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json13
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json20
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json17
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json20
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js80
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js73
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js134
-rw-r--r--spec/frontend/clusters/services/mock_data.js1
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js43
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js32
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js185
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js (renamed from spec/javascripts/import_projects/components/imported_project_table_row_spec.js)25
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js (renamed from spec/javascripts/import_projects/components/provider_repo_table_row_spec.js)90
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js (renamed from spec/javascripts/import_projects/store/actions_spec.js)4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js139
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js172
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_warning_spec.js)10
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js (renamed from spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js)12
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/system_note_spec.js)7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js)48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js)5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js)5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js)7
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js)4
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb155
-rw-r--r--spec/graphql/types/group_type_spec.rb11
-rw-r--r--spec/graphql/types/namespace_type.rb7
-rw-r--r--spec/graphql/types/query_type_spec.rb2
-rw-r--r--spec/javascripts/api_spec.js14
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js4
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js4
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js111
-rw-r--r--spec/javascripts/fixtures/static/images/green_box.png (renamed from spec/javascripts/fixtures/images/green_box.png)bin1306 -> 1306 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/one_white_pixel.png (renamed from spec/javascripts/fixtures/one_white_pixel.png)bin68 -> 68 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/red_box.png (renamed from spec/javascripts/fixtures/images/red_box.png)bin1305 -> 1305 bytes
-rw-r--r--spec/javascripts/fixtures/static/projects.json (renamed from spec/javascripts/fixtures/projects.json)0
-rw-r--r--spec/javascripts/gl_dropdown_spec.js4
-rw-r--r--spec/javascripts/import_projects/components/import_projects_table_spec.js188
-rw-r--r--spec/javascripts/issue_spec.js1
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js2
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js23
-rw-r--r--spec/javascripts/pdf/index_spec.js4
-rw-r--r--spec/javascripts/pdf/page_spec.js4
-rw-r--r--spec/javascripts/raven/index_spec.js10
-rw-r--r--spec/javascripts/raven/raven_config_spec.js10
-rw-r--r--spec/javascripts/test_constants.js6
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js4
-rw-r--r--spec/lib/api/helpers_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb14
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb21
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb36
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb11
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb2
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb41
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb84
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb16
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb20
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb42
-rw-r--r--spec/lib/gitlab/graphql/generic_tracing_spec.rb67
-rw-r--r--spec/lib/gitlab/graphql/tracing_spec.rb33
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml7
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb22
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb105
-rw-r--r--spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb32
-rw-r--r--spec/lib/gitlab/profiler_spec.rb6
-rw-r--r--spec/lib/gitlab/prometheus/query_variables_spec.rb28
-rw-r--r--spec/models/application_record_spec.rb19
-rw-r--r--spec/models/application_setting_spec.rb14
-rw-r--r--spec/models/blob_spec.rb15
-rw-r--r--spec/models/ci/bridge_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb26
-rw-r--r--spec/models/ci/pipeline_spec.rb48
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb6
-rw-r--r--spec/models/clusters/applications/helm_spec.rb8
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb6
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb9
-rw-r--r--spec/models/clusters/applications/knative_spec.rb6
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb53
-rw-r--r--spec/models/clusters/applications/runner_spec.rb12
-rw-r--r--spec/models/deployment_spec.rb27
-rw-r--r--spec/models/member_spec.rb10
-rw-r--r--spec/models/merge_request_spec.rb46
-rw-r--r--spec/models/notification_recipient_spec.rb40
-rw-r--r--spec/models/pages_domain_spec.rb28
-rw-r--r--spec/models/project_metrics_setting_spec.rb55
-rw-r--r--spec/models/project_services/chat_message/deployment_message_spec.rb150
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb11
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb6
-rw-r--r--spec/models/project_wiki_spec.rb74
-rw-r--r--spec/models/repository_spec.rb85
-rw-r--r--spec/models/wiki_page_spec.rb66
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb46
-rw-r--r--spec/requests/api/events_spec.rb135
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb118
-rw-r--r--spec/requests/api/jobs_spec.rb4
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/project_events_spec.rb156
-rw-r--r--spec/routing/group_routing_spec.rb14
-rw-r--r--spec/rubocop/cop/include_action_view_context_spec.rb45
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb4
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb76
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb6
-rw-r--r--spec/services/clusters/applications/check_uninstall_progress_service_spec.rb145
-rw-r--r--spec/services/clusters/applications/destroy_service_spec.rb63
-rw-r--r--spec/services/clusters/applications/uninstall_service_spec.rb77
-rw-r--r--spec/services/merge_requests/close_service_spec.rb8
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb8
-rw-r--r--spec/services/projects/import_service_spec.rb45
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb3
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb1
-rw-r--r--spec/services/projects/lfs_pointers/lfs_import_service_spec.rb153
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb1
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb148
-rw-r--r--spec/services/projects/operations/update_service_spec.rb50
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb14
-rw-r--r--spec/services/update_deployment_service_spec.rb1
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb12
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/helpers/graphql_helpers.rb13
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb43
-rw-r--r--spec/support/helpers/repo_helpers.rb78
-rw-r--r--spec/support/protected_branch_helpers.rb30
-rw-r--r--spec/support/protected_tag_helpers.rb18
-rw-r--r--spec/support/shared_examples/application_setting_examples.rb39
-rw-r--r--spec/support/shared_examples/models/chat_service_spec.rb8
-rw-r--r--spec/support/shared_examples/models/cluster_application_core_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb65
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb14
-rw-r--r--spec/uploaders/object_storage_spec.rb8
-rw-r--r--spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb28
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb27
-rw-r--r--spec/workers/build_success_worker_spec.rb1
-rw-r--r--spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb32
-rw-r--r--spec/workers/deployments/finished_worker_spec.rb39
-rw-r--r--spec/workers/pages_domain_removal_cron_worker_spec.rb42
-rw-r--r--spec/workers/pages_domain_verification_cron_worker_spec.rb8
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb12
-rw-r--r--yarn.lock145
556 files changed, 9015 insertions, 3077 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4d30efccb5c..44beccd966a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
@@ -28,6 +28,8 @@ stages:
- prepare
- merge
- test
+ - review
+ - qa
- post-test
- pages
- post-cleanup
diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml
index e15f8ed91e0..c384bcdcdfc 100644
--- a/.gitlab/ci/cng.gitlab-ci.yml
+++ b/.gitlab/ci/cng.gitlab-ci.yml
@@ -9,7 +9,7 @@ cloud-native-image:
cache: {}
when: manual
script:
- - gem install gitlab --no-document
+ - install_gitlab_gem
- CNG_PROJECT_PATH="gitlab-org/build/CNG" BUILD_TRIGGER_TOKEN=$CI_JOB_TOKEN ./scripts/trigger-build cng
only:
- tags@gitlab-org/gitlab-ce
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index fd179f77e26..fbf8925e30a 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -16,7 +16,7 @@
gitlab:assets:compile:
<<: *assets-compile-cache
extends: .dedicated-no-docs-pull-cache-job
- image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.21-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
+ image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.21-chrome-73.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
dependencies:
- setup-test-env
services:
@@ -38,6 +38,10 @@ gitlab:assets:compile:
- bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image
- scripts/clean-old-cached-assets
+ # Play dependent manual jobs
+ - install_api_client_dependencies_with_apt
+ - play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
+ - play_job "schedule:review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
artifacts:
name: webpack-report
expire_in: 31d
@@ -103,7 +107,7 @@ gitlab:ui:visual:
- $CI_COMMIT_MESSAGE =~ /\[skip visual\]/i
artifacts:
paths:
- - tests/__image_snapshots__/
+ - gitlab-ui/tests/__image_snapshots__/
when: always
karma:
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 07b38c9aa85..85c6409186e 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -1,20 +1,17 @@
package-and-qa:
image: ruby:2.5-alpine
- stage: test
+ stage: qa
+ when: manual
before_script: []
dependencies: []
cache: {}
variables:
GIT_DEPTH: "1"
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
retry: 0
script:
- - apk add --update openssl curl jq
- - gem install gitlab --no-document
- - source ./scripts/review_apps/review-apps.sh
- - wait_for_job_to_be_done "gitlab:assets:compile"
+ - source scripts/utils.sh
+ - install_gitlab_gem
- ./scripts/trigger-build omnibus
- when: manual
only:
- /.+/@gitlab-org/gitlab-ce
- /.+/@gitlab-org/gitlab-ee
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index bdc6ce234b8..01e71a7faf1 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -86,7 +86,7 @@
.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.21-chrome-71.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 9cfb50eeefc..f5b131cf6b2 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -26,12 +26,10 @@
extends: .dedicated-runner
<<: *review-only
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
- stage: test
cache: {}
dependencies: []
- environment: &review-environment
- name: review/${CI_COMMIT_REF_NAME}
- url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
+ before_script:
+ - source scripts/utils.sh
.review-docker: &review-docker
<<: *review-base
@@ -42,18 +40,13 @@
- gitlab-org
- docker
variables: &review-docker-variables
- GIT_DEPTH: "1"
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375
LATEST_QA_IMAGE: "gitlab/${CI_PROJECT_NAME}-qa:nightly"
QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab/${CI_PROJECT_NAME}-qa:${CI_COMMIT_REF_SLUG}"
- before_script: []
build-qa-image:
<<: *review-docker
- variables:
- <<: *review-docker-variables
- GIT_DEPTH: "20"
stage: prepare
script:
- time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/
@@ -63,16 +56,14 @@ build-qa-image:
.review-build-cng-base: &review-build-cng-base
image: ruby:2.5-alpine
stage: test
- before_script: []
+ when: manual
+ before_script:
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - install_gitlab_gem
dependencies: []
cache: {}
- variables:
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
script:
- - apk add --update openssl curl jq
- - gem install gitlab --no-document
- - source ./scripts/review_apps/review-apps.sh
- - wait_for_job_to_be_done "gitlab:assets:compile"
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
review-build-cng:
@@ -85,26 +76,32 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base
<<: *review-base
+ stage: review
retry: 2
allow_failure: true
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master"
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
- environment:
- <<: *review-environment
+ environment: &review-environment
+ name: review/${CI_COMMIT_REF_NAME}
+ url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop
before_script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- - apk update && apk add jq
- - gem install gitlab --no-document
- - source ./scripts/review_apps/review-apps.sh
+ - echo "${CI_ENVIRONMENT_URL}" > review_app_url.txt
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - source scripts/review_apps/review-apps.sh
script:
- - wait_for_job_to_be_done "review-build-cng"
- perform_review_app_deployment
+ artifacts:
+ paths:
+ - review_app_url.txt
+ expire_in: 2 days
+ when: always
review-deploy:
<<: *review-deploy-base
@@ -113,15 +110,29 @@ schedule:review-deploy:
<<: *review-deploy-base
<<: *review-schedules-only
script:
- - wait_for_job_to_be_done "schedule:review-build-cng"
- perform_review_app_deployment
+review-stop:
+ <<: *review-base
+ stage: review
+ when: manual
+ allow_failure: true
+ variables:
+ GIT_DEPTH: "1"
+ environment:
+ <<: *review-environment
+ action: stop
+ script:
+ - source scripts/review_apps/review-apps.sh
+ - delete
+ - cleanup
+
.review-qa-base: &review-qa-base
<<: *review-docker
+ stage: qa
allow_failure: true
variables:
<<: *review-docker-variables
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
GITLAB_USERNAME: "root"
@@ -131,40 +142,45 @@ schedule:review-deploy:
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
QA_DEBUG: "true"
+ dependencies:
+ - review-deploy
artifacts:
paths:
- ./qa/gitlab-qa-run-*
expire_in: 7 days
when: always
before_script:
- - echo "${QA_IMAGE}"
+ - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
- - apk update && apk add curl jq
- - source ./scripts/review_apps/review-apps.sh
+ - echo "${QA_IMAGE}"
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
- gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
review-qa-smoke:
<<: *review-qa-base
retry: 2
script:
- - wait_for_job_to_be_done "review-deploy"
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
review-qa-all:
<<: *review-qa-base
+ when: manual
script:
- - wait_for_job_to_be_done "review-deploy"
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
- when: manual
.review-performance-base: &review-performance-base
<<: *review-qa-base
- script:
- - wait_for_job_to_be_done "review-deploy"
+ stage: qa
+ before_script:
+ - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
+ - echo "${CI_ENVIRONMENT_URL}"
- mkdir -p gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
+ - mkdir -p sitespeed-results
+ script:
+ - 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}"
+ after_script:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
@@ -175,42 +191,26 @@ review-qa-all:
review-performance:
<<: *review-performance-base
-review-stop:
- <<: *review-base
- extends: .single-script-job-dedicated-runner
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
- allow_failure: true
- variables:
- SCRIPT_NAME: "review_apps/review-apps.sh"
- when: manual
- environment:
- <<: *review-environment
- action: stop
- script:
- - source $(basename "${SCRIPT_NAME}")
- - delete
- - cleanup
+schedule:review-performance:
+ <<: *review-performance-base
+ <<: *review-schedules-only
+ dependencies:
+ - schedule:review-deploy
schedule:review-cleanup:
<<: *review-base
<<: *review-schedules-only
stage: build
allow_failure: true
- variables:
- GIT_DEPTH: "1"
environment:
name: review/auto-cleanup
+ action: stop
before_script:
- - gem install gitlab --no-document
+ - source scripts/utils.sh
+ - install_gitlab_gem
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
-schedule:review-performance:
- <<: *review-performance-base
- <<: *review-schedules-only
- script:
- - wait_for_job_to_be_done "schedule:review-deploy"
-
danger-review:
extends: .dedicated-pull-cache-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
diff --git a/.stylelintrc b/.stylelintrc
index 241d2c94a88..b0ace93e04f 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -2,7 +2,7 @@
"plugins":[
"./scripts/frontend/stylelint/stylelint-duplicate-selectors.js",
"./scripts/frontend/stylelint/stylelint-utility-classes.js",
- "stylelint-scss",
+ "stylelint-scss"
],
"rules":{
"at-rule-blacklist":[
@@ -64,7 +64,7 @@
"number-leading-zero":"always",
"number-no-trailing-zeros":true,
"property-no-unknown":true,
- "property-no-vendor-prefix":true,
+ "property-no-vendor-prefix": [true, { "ignoreProperties": ["user-select"] }],
"rule-empty-line-before":[
"always-multi-line",
{
@@ -94,7 +94,7 @@
{
"message":"Selector should be written in lowercase with hyphens (selector-class-pattern)",
"severity": "warning"
- },
+ }
],
"selector-list-comma-newline-after":"always",
"selector-max-compound-selectors":[3, { "severity": "warning" }],
@@ -104,8 +104,8 @@
"selector-pseudo-element-no-unknown":true,
"shorthand-property-no-redundant-values":true,
"string-quotes":"single",
- "value-no-vendor-prefix":[true, { ignoreValues: ["sticky"] }],
+ "value-no-vendor-prefix":[true, { "ignoreValues": ["sticky"] }],
"stylelint-gitlab/duplicate-selectors":[true,{ "severity": "warning" }],
- "stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }],
+ "stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }]
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd4c0e479cc..17b185358ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,46 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.10.4 (2019-05-01)
+
+### Fixed (12 changes)
+
+- Fix MR popover on ToDos page. !27382
+- Fix 500 in general pipeline settings when passing an invalid build timeout. !27416
+- Fix bug where system note MR has no popover. !27589
+- Fix bug when project export to remote url fails. !27614
+- `on_stop` is not automatically triggered with pipelines for merge requests. !27618
+- Update Workhorse to v8.5.2. !27631
+- Show proper wiki links in search results. !27634
+- Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent. !27663
+- Fix Kubernetes service template deployment jobs broken as of 11.10.0. !27687
+- Prevent text selection when dragging in issue boards. !27724
+- Fix pipelines for merge requests does not show pipeline page when source branch is removed. !27803
+- Fix Metrics Environments dropdown.
+
+### Performance (2 changes)
+
+- Prevent concurrent execution of PipelineScheduleWorker. !27781
+- Fix slow performance with compiling HAML templates. !27782
+
+
+## 11.10.3 (2019-04-30)
+
+### Security (1 change)
+
+- Allow to see project events only with api scope token.
+
+
+## 11.10.2 (2019-04-25)
+
+### Security (4 changes)
+
+- Loosen regex for exception sanitization. !3076
+- Resolve: moving an issue to private repo leaks namespace and project name.
+- Escape path in new merge request mail.
+- Stop sending emails to users who can't read commit.
+
+
## 11.10.1 (2019-04-23)
### Fixed (2 changes)
@@ -253,6 +293,17 @@ entry.
- Removes EE differences for environment_item.vue.
+## 11.9.10 (2019-04-26)
+
+### Security (5 changes)
+
+- Loosen regex for exception sanitization. !3077
+- Resolve: moving an issue to private repo leaks namespace and project name.
+- Escape path in new merge request mail.
+- Stop sending emails to users who can't read commit.
+- Upgrade Rails to 5.0.7.2.
+
+
## 11.9.9 (2019-04-23)
### Performance (1 change)
@@ -611,6 +662,13 @@ entry.
- Removes EE differences for jobs/getters.js.
+## 11.8.10 (2019-04-30)
+
+### Security (1 change)
+
+- Allow to see project events only with api scope token.
+
+
## 11.8.8 (2019-04-23)
### Fixed (5 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 2b17ffd5042..39fc130ef85 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.34.0
+1.36.0
diff --git a/Gemfile b/Gemfile
index 65ba7137892..f3c21c720e0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -79,6 +79,7 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10'
+gem 'apollo_upload_server', '~> 2.0.0.beta3'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
@@ -275,7 +276,7 @@ gem 'sentry-raven', '~> 2.7'
gem 'premailer-rails', '~> 1.9.7'
# LabKit: Tracing and Correlation
-gem 'gitlab-labkit', '~> 0.1.2'
+gem 'gitlab-labkit', '~> 0.2.0'
# I18n
gem 'ruby_parser', '~> 3.8', require: false
@@ -284,7 +285,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.3'
gem 'gettext', '~> 3.2.2', require: false, group: :development
-gem 'batch-loader', '~> 1.2.2'
+gem 'batch-loader', '~> 1.4.0'
# Perf bar
gem 'peek', '~> 1.0.1'
@@ -308,7 +309,7 @@ group :development do
gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false
- gem 'letter_opener_web', '~> 1.3.0'
+ gem 'letter_opener_web', '~> 1.3.4'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler
@@ -416,7 +417,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.19.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.26.0', require: 'gitaly'
gem 'grpc', '~> 1.19.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index da8f8db9528..4db00ba4e18 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -52,6 +52,9 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
+ apollo_upload_server (2.0.0.beta.3)
+ graphql (>= 1.8)
+ rails (>= 4.2)
arel (8.0.0)
asana (0.8.1)
faraday (~> 0.9)
@@ -73,7 +76,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
- batch-loader (1.2.2)
+ batch-loader (1.4.0)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -280,12 +283,12 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.19.0)
+ gitaly-proto (1.26.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
activerecord (>= 3.2.0, < 6.0)
- gitlab-labkit (0.1.2)
+ gitlab-labkit (0.2.0)
actionpack (~> 5)
activesupport (~> 5)
grpc (~> 1.15)
@@ -441,9 +444,9 @@ GEM
rest-client (~> 2.0)
launchy (2.4.3)
addressable (~> 2.3)
- letter_opener (1.4.1)
+ letter_opener (1.7.0)
launchy (~> 2.2)
- letter_opener_web (1.3.0)
+ letter_opener_web (1.3.4)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
@@ -988,6 +991,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
+ apollo_upload_server (~> 2.0.0.beta3)
asana (~> 0.8.1)
asciidoctor (~> 1.5.8)
asciidoctor-plantuml (= 0.0.8)
@@ -995,7 +999,7 @@ DEPENDENCIES
awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
- batch-loader (~> 1.2.2)
+ batch-loader (~> 1.4.0)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0)
@@ -1052,10 +1056,10 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.19.0)
+ gitaly-proto (~> 1.26.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
- gitlab-labkit (~> 0.1.2)
+ gitlab-labkit (~> 0.2.0)
gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0)
gitlab-styles (~> 2.5)
@@ -1089,7 +1093,7 @@ DEPENDENCIES
kaminari (~> 1.0)
knapsack (~> 1.17)
kubeclient (~> 4.2.2)
- letter_opener_web (~> 1.3.0)
+ letter_opener_web (~> 1.3.4)
license_finder (~> 5.4)
licensee (~> 8.9)
lograge (~> 0.5)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8754c253881..e583a8affd4 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { joinPaths } from './lib/utils/url_utility';
const Api = {
groupsPath: '/api/:version/groups.json',
@@ -339,11 +340,7 @@ const Api = {
},
buildUrl(url) {
- let urlRoot = '';
- if (gon.relative_url_root != null) {
- urlRoot = gon.relative_url_root;
- }
- return urlRoot + url.replace(':version', gon.api_version);
+ return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
};
diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
new file mode 100644
index 00000000000..20c31d9f8a4
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
@@ -0,0 +1,13 @@
+export default {
+ computed: {
+ resolveButtonTitle() {
+ let title = 'Mark as resolved';
+
+ if (this.resolvedBy) {
+ title = `Resolved by ${this.resolvedBy.name}`;
+ }
+
+ return title;
+ },
+ },
+};
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index 9a33a060c76..c3541e62568 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Clipboard from 'clipboard';
+import { sprintf, __ } from '~/locale';
function showTooltip(target, title) {
const $target = $(target);
@@ -16,7 +17,7 @@ function showTooltip(target, title) {
}
function genericSuccess(e) {
- showTooltip(e.trigger, 'Copied');
+ showTooltip(e.trigger, __('Copied'));
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
@@ -33,7 +34,7 @@ function genericError(e) {
} else {
key = 'Ctrl';
}
- showTooltip(e.trigger, `Press ${key}-C to copy`);
+ showTooltip(e.trigger, sprintf(__(`Press %{key}-C to copy`), { key }));
}
export default function initCopyToClipboard() {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
index 20c7fa8a9ab..9a2e2c03213 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import { Node } from 'tiptap';
+import { __ } from '~/locale';
// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
export default class TableOfContents extends Node {
@@ -22,7 +23,7 @@ export default class TableOfContents extends Node {
priority: 51,
},
],
- toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
+ toDOM: () => ['p', { class: 'table-of-contents' }, __('Table of Contents')],
};
}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 7adccbb062f..35874140bf9 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -22,7 +22,7 @@ function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10;
-MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
+MarkdownPreview.prototype.emptyMessage = __('Nothing to preview.');
MarkdownPreview.prototype.ajaxCache = {};
@@ -40,7 +40,7 @@ MarkdownPreview.prototype.showPreview = function($form) {
preview.text(this.emptyMessage);
this.hideReferencedUsers($form);
} else {
- preview.addClass('md-preview-loading').text('Loading...');
+ preview.addClass('md-preview-loading').text(__('Loading...'));
this.fetchMarkdownPreview(
mdText,
url,
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index c1ea67f9293..530ab0bd4d9 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import '../commons/bootstrap';
import { isInIssuePage } from '../lib/utils/common_utils';
+import { __ } from '~/locale';
// Quick Submit behavior
//
@@ -65,7 +66,9 @@ $(document).on(
}
const $this = $(this);
- const title = isMac() ? 'You can also press &#8984;-Enter' : 'You can also press Ctrl-Enter';
+ const title = isMac()
+ ? __('You can also press &#8984;-Enter')
+ : __('You can also press Ctrl-Enter');
$this.tooltip({
container: 'body',
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index b88e69a07bf..2e537d8c000 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,8 +1,9 @@
import Flash from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
+import { __ } from '~/locale';
function onError() {
- const flash = new Flash('Balsamiq file could not be loaded.');
+ const flash = new Flash(__('Balsamiq file could not be loaded.'));
return flash;
}
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index cd3251ad1ca..9010cd0c3c1 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -5,6 +5,7 @@ import Dropzone from 'dropzone';
import { visitUrl } from '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
+import { sprintf, __ } from '~/locale';
Dropzone.autoDiscover = false;
@@ -73,7 +74,7 @@ export default class BlobFileDropzone {
.html(errorMessage)
.text();
$('.dropzone-alerts')
- .html(`Error uploading file: "${stripped}"`)
+ .html(sprintf(__('Error uploading file: %{stripped}'), { stripped }))
.show();
this.removeFile(file);
},
@@ -84,7 +85,7 @@ export default class BlobFileDropzone {
e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
- alert('Please select a file');
+ alert(__('Please select a file'));
return false;
}
toggleLoading(submitButton, submitButtonLoadingIcon, true);
diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js
index 57c1baa9886..dbff03dc734 100644
--- a/app/assets/javascripts/blob/sketch/index.js
+++ b/app/assets/javascripts/blob/sketch/index.js
@@ -1,5 +1,6 @@
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
+import { __ } from '~/locale';
export default class SketchLoader {
constructor(container) {
@@ -56,10 +57,10 @@ export default class SketchLoader {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
- errorMsg.textContent = `
+ errorMsg.textContent = __(`
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
- `;
+ `);
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 4718b642617..659d57e6a6f 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,11 +1,12 @@
import FileTemplateSelector from '../file_template_selector';
+import { __ } from '~/locale';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'dockerfile',
- name: 'Dockerfile',
+ name: __('Dockerfile'),
pattern: /(Dockerfile)/,
type: 'dockerfiles',
dropdown: '.js-dockerfile-selector',
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index d0359fc5fe9..d246a1f6064 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
+import { __ } from '~/locale';
export default class BlobViewer {
constructor() {
@@ -26,7 +27,7 @@ export default class BlobViewer {
promise
.then(module => module.default(viewer))
.catch(error => {
- Flash('Error loading file viewer.');
+ Flash(__('Error loading file viewer.'));
throw error;
});
@@ -106,16 +107,19 @@ export default class BlobViewer {
if (!this.copySourceBtn) return;
if (this.simpleViewer.getAttribute('data-loaded')) {
- this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
+ this.copySourceBtn.setAttribute('title', __('Copy source to clipboard'));
this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) {
this.copySourceBtn.setAttribute(
'title',
- 'Wait for the source to load to copy it to the clipboard',
+ __('Wait for the source to load to copy it to the clipboard'),
);
this.copySourceBtn.classList.add('disabled');
} else {
- this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
+ this.copySourceBtn.setAttribute(
+ 'title',
+ __('Switch to the source to copy it to the clipboard'),
+ );
this.copySourceBtn.classList.add('disabled');
}
@@ -158,7 +162,7 @@ export default class BlobViewer {
this.toggleCopyButtonState();
})
- .catch(() => new Flash('Error loading viewer'));
+ .catch(() => new Flash(__('Error loading viewer')));
}
static loadViewer(viewerParam) {
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
new file mode 100644
index 00000000000..3178bda93b8
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -0,0 +1,7 @@
+export function getMilestone() {
+ return null;
+}
+
+export default {
+ getMilestone,
+};
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index c9effa0639b..b8882203cc7 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -83,7 +83,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
- class="board-card position-relative p-3 rounded"
+ class="board-card p-3 rounded"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index a5f9d65e4d5..a06db359c94 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __ } from '~/locale';
export default Vue.extend({
props: {
@@ -13,7 +14,7 @@ export default Vue.extend({
$(this.$el).tooltip('hide');
// eslint-disable-next-line no-alert
- if (window.confirm('Are you sure you want to delete this list?')) {
+ if (window.confirm(__('Are you sure you want to delete this list?'))) {
this.list.destroy();
}
},
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 70daba352f9..dc1bdc23b5e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
+import { getMilestone } from 'ee_else_ce/boards/boards_util';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
@@ -51,11 +52,14 @@ export default {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
+ const milestone = getMilestone(this.list);
+
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
assignees,
+ milestone,
project_id: this.selectedProject.id,
});
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 915d1676e62..c587b276fa3 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -45,7 +45,7 @@ export default Vue.extend({
return Object.keys(this.issue).length;
},
milestoneTitle() {
- return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
+ return this.issue.milestone ? this.issue.milestone.title : __('No Milestone');
},
canRemove() {
return !this.list.preset;
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 6aa689b4056..17de7b2cf1e 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -168,7 +168,7 @@ export default {
</script>
<template>
<div>
- <div class="d-flex board-card-header">
+ <div class="d-flex board-card-header" dir="auto">
<h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 009ae5dd331..4995a8d9367 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -123,7 +123,7 @@ export default () => {
this.loading = false;
})
.catch(() => {
- Flash('An error occurred while fetching the board lists. Please try again.');
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
});
},
methods: {
@@ -223,7 +223,7 @@ export default () => {
},
tooltipTitle() {
if (this.disabled) {
- return 'Please add a list to your board first';
+ return __('Please add a list to your board first');
}
return '';
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index a34222b6887..70861fbf2b3 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -7,6 +7,7 @@ import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
const boardsStore = {
disabled: false,
@@ -78,7 +79,7 @@ const boardsStore = {
this.addList({
id: 'blank',
list_type: 'blank',
- title: 'Welcome to your Issue Board!',
+ title: __('Welcome to your Issue Board!'),
position: 0,
});
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index df855261b3c..8461e01de7b 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,25 +1,20 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import {
- APPLICATION_STATUS,
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- UPGRADE_REQUESTED,
- UPGRADE_REQUEST_FAILURE,
- INGRESS,
- INGRESS_DOMAIN_SUFFIX,
-} from './constants';
+import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
+Vue.use(GlToast);
+
/**
* Cluster page has 2 separate parts:
* Toggle button and applications section
@@ -134,7 +129,6 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
- eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
@@ -144,7 +138,6 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication);
- eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
@@ -258,12 +251,13 @@ export default class Clusters {
installApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
+ this.store.installApplication(appId);
+
return this.service.installApplication(appId, data.params).catch(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
'requestReason',
@@ -274,27 +268,21 @@ export default class Clusters {
upgradeApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
- this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
- this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
- }
- upgradeFailed(appId) {
- this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
+ this.store.updateApplication(appId);
+ this.service.installApplication(appId, data.params).catch(() => {
+ this.store.notifyUpdateFailure(appId);
+ });
}
dismissUpgradeSuccess(appId) {
- this.store.updateAppProperty(appId, 'requestStatus', null);
+ this.store.acknowledgeSuccessfulUpdate(appId);
}
- toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
- const { externalIp, status } = ingressNewState;
- const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp;
- const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`;
-
- if (ingressPreviousState.status !== status) {
- this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden);
- this.ingressDomainSnippet.textContent = domainSnippetText;
+ toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
+ if (externalIp !== newExternalIp) {
+ this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
+ this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`;
}
}
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 937e4c3bfc3..a351916942e 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -8,12 +8,7 @@ import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
-import {
- APPLICATION_STATUS,
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- UPGRADE_REQUESTED,
-} from '../constants';
+import { APPLICATION_STATUS } from '../constants';
export default {
components: {
@@ -63,10 +58,6 @@ export default {
type: String,
required: false,
},
- requestStatus: {
- type: String,
- required: false,
- },
requestReason: {
type: String,
required: false,
@@ -76,6 +67,11 @@ export default {
required: false,
default: false,
},
+ installFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
version: {
type: String,
required: false,
@@ -88,6 +84,21 @@ export default {
type: Boolean,
required: false,
},
+ updateSuccessful: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateAcknowledged: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -102,21 +113,12 @@ export default {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalling() {
- return (
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING ||
- (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.installed)
- );
+ return this.status === APPLICATION_STATUS.INSTALLING;
},
canInstall() {
- if (this.isInstalling) {
- return false;
- }
-
return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
- this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus
);
},
@@ -137,7 +139,7 @@ export default {
return !this.installed || !this.uninstallable;
},
installButtonLoading() {
- return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
+ return !this.status || this.isInstalling;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
@@ -168,19 +170,13 @@ export default {
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
- hasError() {
- return (
- !this.isInstalling &&
- (this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
- );
- },
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
},
versionLabel() {
- if (this.upgradeFailed) {
+ if (this.updateFailed) {
return s__('ClusterIntegration|Upgrade failed');
} else if (this.isUpgrading) {
return s__('ClusterIntegration|Upgrading');
@@ -188,19 +184,6 @@ export default {
return s__('ClusterIntegration|Upgraded');
},
- upgradeRequested() {
- return this.requestStatus === UPGRADE_REQUESTED;
- },
- upgradeSuccessful() {
- return this.status === APPLICATION_STATUS.UPDATED;
- },
- upgradeFailed() {
- if (this.isUpgrading) {
- return false;
- }
-
- return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
- },
upgradeFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
@@ -211,11 +194,11 @@ export default {
},
upgradeButtonLabel() {
let label;
- if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
+ if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) {
label = s__('ClusterIntegration|Updating');
- } else if (this.upgradeFailed) {
+ } else if (this.updateFailed) {
label = s__('ClusterIntegration|Retry update');
}
@@ -223,24 +206,19 @@ export default {
},
isUpgrading() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
- return (
- this.status === APPLICATION_STATUS.UPDATING ||
- (this.upgradeRequested && !this.upgradeSuccessful)
- );
+ return this.status === APPLICATION_STATUS.UPDATING;
},
shouldShowUpgradeDetails() {
// This method only returns true when;
// Upgrade was successful OR Upgrade failed
// AND new upgrade is unavailable AND version information is present.
- return (
- (this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version
- );
+ return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
},
},
watch: {
- status() {
- if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
- eventHub.$emit('upgradeFailed', this.id);
+ updateSuccessful() {
+ if (this.updateSuccessful) {
+ this.$toast.show(this.upgradeSuccessDescription);
}
},
},
@@ -257,9 +235,6 @@ export default {
params: this.installApplicationRequestParams,
});
},
- dismissUpgradeSuccess() {
- eventHub.$emit('dismissUpgradeSuccess', this.id);
- },
},
};
</script>
@@ -297,7 +272,7 @@ export default {
</strong>
<slot name="description"></slot>
<div
- v-if="hasError || isUnknownStatus"
+ v-if="installFailed || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10"
>
<p class="js-cluster-application-general-error-message append-bottom-0">
@@ -318,10 +293,10 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
>
{{ versionLabel }}
- <span v-if="upgradeSuccessful">to</span>
+ <span v-if="updateSuccessful">to</span>
<gl-link
- v-if="upgradeSuccessful"
+ v-if="updateSuccessful"
:href="chartRepo"
target="_blank"
class="js-cluster-application-upgrade-version"
@@ -330,24 +305,13 @@ export default {
</div>
<div
- v-if="upgradeFailed && !isUpgrading"
+ v-if="updateFailed && !isUpgrading"
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
>
{{ upgradeFailureDescription }}
</div>
-
- <div
- v-if="upgradeRequested && upgradeSuccessful"
- class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
- >
- {{ upgradeSuccessDescription }}
- <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
- &times;
- </button>
- </div>
-
<loading-button
- v-if="upgradeAvailable || upgradeFailed || isUpgrading"
+ v-if="upgradeAvailable || updateFailed || isUpgrading"
class="btn btn-primary js-cluster-application-upgrade-button mt-2"
:loading="isUpgrading"
:disabled="isUpgrading"
@@ -361,9 +325,9 @@ export default {
role="gridcell"
>
<div v-if="showManageButton" class="btn-group table-action-buttons">
- <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
- manageButtonLabel
- }}</a>
+ <a :href="manageLink" :class="{ disabled: disabled }" class="btn">
+ {{ manageButtonLabel }}
+ </a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index ae4fe11c6ae..dfc2069f131 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -224,9 +224,9 @@ export default {
<p class="append-bottom-0">
{{
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
- Helm Tiller is required to install any of the following applications.`)
+ Helm Tiller is required to install any of the following applications.`)
}}
- <a :href="helpPath"> {{ __('More information') }} </a>
+ <a :href="helpPath">{{ __('More information') }}</a>
</p>
<div class="cluster-application-list prepend-top-10">
@@ -239,15 +239,16 @@ export default {
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
+ :install-failed="applications.helm.installFailed"
class="rounded-top"
title-link="https://docs.helm.sh/"
>
<div slot="description">
{{
s__(`ClusterIntegration|Helm streamlines installing
- and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster,
- and manages releases of your charts.`)
+ and managing Kubernetes applications.
+ Tiller runs inside of your Kubernetes Cluster,
+ and manages releases of your charts.`)
}}
</div>
</application-row>
@@ -255,7 +256,7 @@ export default {
<div class="svg-container" v-html="helmInstallIllustration"></div>
{{
s__(`ClusterIntegration|You must first install Helm Tiller before
- installing the applications below`)
+ installing the applications below`)
}}
</div>
<application-row
@@ -267,6 +268,7 @@ export default {
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
+ :install-failed="applications.ingress.installFailed"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
@@ -274,16 +276,14 @@ export default {
<p>
{{
s__(`ClusterIntegration|Ingress gives you a way to route
- requests to services based on the request host or path,
- centralizing a number of services into a single entrypoint.`)
+ requests to services based on the request host or path,
+ centralizing a number of services into a single entrypoint.`)
}}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
- <label for="ingress-endpoint">
- {{ s__('ClusterIntegration|Ingress Endpoint') }}
- </label>
+ <label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
<div v-if="ingressExternalEndpoint" class="input-group">
<input
id="ingress-endpoint"
@@ -309,8 +309,8 @@ export default {
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
- generated endpoint in order to access
- your application after it has been deployed.`)
+ generated endpoint in order to access
+ your application after it has been deployed.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -321,10 +321,9 @@ export default {
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{
s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
-
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
@@ -344,6 +343,7 @@ export default {
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:installed="applications.cert_manager.installed"
+ :install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
@@ -366,15 +366,14 @@ export default {
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Issuers represent a certificate authority.
- You must provide an email address for your Issuer. `)
+ You must provide an email address for your Issuer. `)
}}
<a
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank"
rel="noopener noreferrer"
+ >{{ __('More information') }}</a
>
- {{ __('More information') }}
- </a>
</p>
</div>
</div>
@@ -391,6 +390,7 @@ export default {
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
+ :install-failed="applications.prometheus.installFailed"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
@@ -408,15 +408,18 @@ export default {
:chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable"
:installed="applications.runner.installed"
+ :install-failed="applications.runner.installFailed"
+ :update-successful="applications.runner.updateSuccessful"
+ :update-failed="applications.runner.updateFailed"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
<div slot="description">
{{
s__(`ClusterIntegration|GitLab Runner connects to the
- repository and executes CI/CD jobs,
- pushing results back and deploying
- applications to production.`)
+ repository and executes CI/CD jobs,
+ pushing results back and deploying
+ applications to production.`)
}}
</div>
</application-row>
@@ -430,6 +433,7 @@ export default {
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
+ :install-failed="applications.jupyter.installFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
@@ -438,18 +442,16 @@ export default {
<p>
{{
s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
- manages, and proxies multiple instances of the single-user
- Jupyter notebook server. JupyterHub can be used to serve
- notebooks to a class of students, a corporate data science group,
- or a scientific research group.`)
+ manages, and proxies multiple instances of the single-user
+ Jupyter notebook server. JupyterHub can be used to serve
+ notebooks to a class of students, a corporate data science group,
+ or a scientific research group.`)
}}
</p>
<template v-if="ingressExternalEndpoint">
<div class="form-group">
- <label for="jupyter-hostname">
- {{ s__('ClusterIntegration|Jupyter Hostname') }}
- </label>
+ <label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
<input
@@ -470,7 +472,7 @@ export default {
<p v-if="ingressInstalled" class="form-text text-muted">
{{
s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`)
+ If you do so, point hostname to Ingress IP Address from above.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -490,8 +492,10 @@ export default {
:request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason"
:installed="applications.knative.installed"
+ :install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:disabled="!helmInstalled"
+ v-bind="applications.knative"
title-link="https://github.com/knative/docs"
>
<div slot="description">
@@ -499,7 +503,7 @@ export default {
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0">
{{
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
- to install Knative.`)
+ to install Knative.`)
}}
<a :href="helpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -510,9 +514,9 @@ export default {
<p>
{{
s__(`ClusterIntegration|Knative extends Kubernetes to provide
- a set of middleware components that are essential to build modern,
- source-centric, and container-based applications that can run
- anywhere: on premises, in the cloud, or even in a third-party data center.`)
+ a set of middleware components that are essential to build modern,
+ source-centric, and container-based applications that can run
+ anywhere: on premises, in the cloud, or even in a third-party data center.`)
}}
</p>
@@ -523,9 +527,7 @@ export default {
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
- <strong>
- {{ s__('ClusterIntegration|Knative Domain Name:') }}
- </strong>
+ <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<input
id="knative-domainname"
@@ -538,9 +540,7 @@ export default {
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
- <strong>
- {{ s__('ClusterIntegration|Knative Endpoint:') }}
- </strong>
+ <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
@@ -583,8 +583,8 @@ export default {
>
{{
s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
</p>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 17849497c87..48dbce9676e 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = {
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
+ NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable',
SCHEDULED: 'scheduled',
@@ -27,17 +28,13 @@ export const APPLICATION_STATUS = {
export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING,
- APPLICATION_STATUS.UPDATED,
- APPLICATION_STATUS.UPDATE_ERRORED,
- APPLICATION_STATUS.UNINSTALLING,
- APPLICATION_STATUS.UNINSTALL_ERRORED,
];
// These are only used client-side
-export const REQUEST_SUBMITTED = 'request-submitted';
-export const REQUEST_FAILURE = 'request-failure';
-export const UPGRADE_REQUESTED = 'upgrade-requested';
-export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
+
+export const UPDATE_EVENT = 'update';
+export const INSTALL_EVENT = 'install';
+
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
new file mode 100644
index 00000000000..aafb2350ae4
--- /dev/null
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -0,0 +1,141 @@
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+} = APPLICATION_STATUS;
+
+const applicationStateMachine = {
+ /* When the application initially loads, it will have `NO_STATUS`
+ * It will transition from `NO_STATUS` once the async backend call is completed
+ */
+ [NO_STATUS]: {
+ on: {
+ [SCHEDULED]: {
+ target: INSTALLING,
+ },
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ },
+ [INSTALLING]: {
+ target: INSTALLING,
+ },
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ [UPDATING]: {
+ target: UPDATING,
+ },
+ [UPDATED]: {
+ target: INSTALLED,
+ },
+ [UPDATE_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ updateFailed: true,
+ },
+ },
+ },
+ },
+ [NOT_INSTALLABLE]: {
+ on: {
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ },
+ },
+ },
+ [INSTALLABLE]: {
+ on: {
+ [INSTALL_EVENT]: {
+ target: INSTALLING,
+ effects: {
+ installFailed: false,
+ },
+ },
+ // This is possible in artificial environments for E2E testing
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ },
+ },
+ [INSTALLING]: {
+ on: {
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ },
+ },
+ [INSTALLED]: {
+ on: {
+ [UPDATE_EVENT]: {
+ target: UPDATING,
+ effects: {
+ updateFailed: false,
+ updateSuccessful: false,
+ },
+ },
+ },
+ },
+ [UPDATING]: {
+ on: {
+ [UPDATED]: {
+ target: INSTALLED,
+ effects: {
+ updateSuccessful: true,
+ updateAcknowledged: false,
+ },
+ },
+ [UPDATE_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ updateFailed: true,
+ },
+ },
+ },
+ },
+};
+
+/**
+ * Determines an application new state based on the application current state
+ * and an event. If the application current state cannot handle a given event,
+ * the current state is returned.
+ *
+ * @param {*} application
+ * @param {*} event
+ */
+const transitionApplicationState = (application, event) => {
+ const newState = applicationStateMachine[application.status].on[event];
+
+ return newState
+ ? {
+ ...application,
+ status: newState.target,
+ ...newState.effects,
+ }
+ : application;
+};
+
+export default transitionApplicationState;
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 38512ac28c2..c2e30960659 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -7,7 +7,11 @@ import {
CERT_MANAGER,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
+ APPLICATION_STATUS,
+ INSTALL_EVENT,
+ UPDATE_EVENT,
} from '../constants';
+import transitionApplicationState from '../services/application_state_machine';
const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
@@ -15,8 +19,8 @@ const applicationInitialState = {
status: null,
statusReason: null,
requestReason: null,
- requestStatus: null,
installed: false,
+ installFailed: false,
};
export default class ClusterStore {
@@ -49,6 +53,9 @@ export default class ClusterStore {
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null,
+ updateAcknowledged: true,
+ updateSuccessful: false,
+ updateFailed: false,
},
prometheus: {
...applicationInitialState,
@@ -93,6 +100,32 @@ export default class ClusterStore {
this.state.statusReason = reason;
}
+ installApplication(appId) {
+ this.handleApplicationEvent(appId, INSTALL_EVENT);
+ }
+
+ notifyInstallFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
+ }
+
+ updateApplication(appId) {
+ this.handleApplicationEvent(appId, UPDATE_EVENT);
+ }
+
+ notifyUpdateFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
+ }
+
+ handleApplicationEvent(appId, event) {
+ const currentAppState = this.state.applications[appId];
+
+ this.state.applications[appId] = transitionApplicationState(currentAppState, event);
+ }
+
+ acknowledgeSuccessfulUpdate(appId) {
+ this.state.applications[appId].updateAcknowledged = true;
+ }
+
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
@@ -109,12 +142,16 @@ export default class ClusterStore {
version,
update_available: upgradeAvailable,
} = serverAppEntry;
+ const currentApplicationState = this.state.applications[appId] || {};
+ const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = {
- ...(this.state.applications[appId] || {}),
- status,
+ ...currentApplicationState,
+ ...nextApplicationState,
statusReason,
- installed: isApplicationInstalled(status),
+ installed: isApplicationInstalled(nextApplicationState.status),
+ // Make sure uninstallable is always false until this feature is unflagged
+ uninstallable: false,
};
if (appId === INGRESS) {
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 4de425b48e7..3f0a9f2602c 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -12,6 +12,7 @@ import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
+import { __ } from '~/locale';
Vue.use(Translate);
@@ -61,7 +62,7 @@ export default () => {
methods: {
handleError() {
this.store.setErrorState(true);
- return new Flash('There was an error while fetching cycle analytics data.');
+ return new Flash(__('There was an error while fetching cycle analytics data.'));
},
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
diff --git a/app/assets/javascripts/diffs/workers/tree_worker.js b/app/assets/javascripts/diffs/workers/tree_worker.js
index 534d737c77e..415c463fd19 100644
--- a/app/assets/javascripts/diffs/workers/tree_worker.js
+++ b/app/assets/javascripts/diffs/workers/tree_worker.js
@@ -4,6 +4,11 @@ import { generateTreeList } from '../store/utils';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data } = e;
+
+ if (data === undefined) {
+ return;
+ }
+
const { treeEntries, tree } = generateTreeList(data);
// eslint-disable-next-line no-restricted-globals
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 0b24d9fc920..e020628a473 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default IssuableTokenKeys => {
const wipToken = {
key: 'wip',
@@ -5,7 +7,7 @@ export default IssuableTokenKeys => {
param: '',
symbol: '',
icon: 'admin',
- tag: 'Yes or No',
+ tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: true,
capitalizeTokenValue: true,
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index d9a4d06b549..dad188f6f98 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -3,6 +3,7 @@ import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class DropdownEmoji extends FilteredSearchDropdown {
constructor(options = {}) {
@@ -14,7 +15,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
+ new Flash(__('An error occurred fetching the dropdown data.'));
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 0264f934914..a2312de289d 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -3,6 +3,7 @@ import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class DropdownNonUser extends FilteredSearchDropdown {
constructor(options = {}) {
@@ -17,7 +18,7 @@ export default class DropdownNonUser extends FilteredSearchDropdown {
preprocessing,
onError() {
/* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
+ new Flash(__('An error occurred fetching the dropdown data.'));
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index efd03ec952f..78fbb3696cc 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -14,6 +14,7 @@ import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class FilteredSearchManager {
constructor({
@@ -64,7 +65,7 @@ export default class FilteredSearchManager {
.catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new Flash('An error occurred while parsing recent searches');
+ new Flash(__('An error occurred while parsing recent searches'));
// Gracefully fail to empty array
return [];
})
@@ -340,7 +341,7 @@ export default class FilteredSearchManager {
handleInputPlaceholder() {
const query = DropdownUtils.getSearchQuery();
- const placeholder = 'Search or filter results...';
+ const placeholder = __('Search or filter results...');
const currentPlaceholder = this.filteredSearchInput.placeholder;
if (query.length === 0 && currentPlaceholder !== placeholder) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 11ed85504ec..0a9579bf491 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class FilteredSearchTokenKeys {
constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) {
this.tokenKeys = tokenKeys;
@@ -79,7 +81,7 @@ export default class FilteredSearchTokenKeys {
param: '',
symbol: '',
icon: 'eye-slash',
- tag: 'Yes or No',
+ tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: false,
capitalizeTokenValue: true,
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index fd61030eb13..6c3d9e33420 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -1,4 +1,5 @@
import FilteredSearchTokenKeys from './filtered_search_token_keys';
+import { __ } from '~/locale';
export const tokenKeys = [
{
@@ -60,52 +61,52 @@ export const conditions = [
{
url: 'assignee_id=None',
tokenKey: 'assignee',
- value: 'None',
+ value: __('None'),
},
{
url: 'assignee_id=Any',
tokenKey: 'assignee',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'milestone_title=None',
tokenKey: 'milestone',
- value: 'None',
+ value: __('None'),
},
{
url: 'milestone_title=Any',
tokenKey: 'milestone',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
- value: 'Upcoming',
+ value: __('Upcoming'),
},
{
url: 'milestone_title=%23started',
tokenKey: 'milestone',
- value: 'Started',
+ value: __('Started'),
},
{
url: 'label_name[]=None',
tokenKey: 'label',
- value: 'None',
+ value: __('None'),
},
{
url: 'label_name[]=Any',
tokenKey: 'label',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
- value: 'None',
+ value: __('None'),
},
{
url: 'my_reaction_emoji=Any',
tokenKey: 'my-reaction',
- value: 'Any',
+ value: __('Any'),
},
];
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
index 5917b223d63..011b37e218d 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
@@ -1,7 +1,9 @@
+import { __ } from '~/locale';
+
class RecentSearchesServiceError {
constructor(message) {
this.name = 'RecentSearchesServiceError';
- this.message = message || 'Recent Searches Service is unavailable';
+ this.message = message || __('Recent Searches Service is unavailable');
}
}
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index a9d5ba8faa8..38327472cb3 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -5,6 +5,7 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import Flash from '~/flash';
import UsersCache from '~/lib/utils/users_cache';
+import { __ } from '~/locale';
export default class VisualTokenValue {
constructor(tokenValue, tokenType) {
@@ -77,7 +78,7 @@ export default class VisualTokenValue {
matchingLabel.text_color,
);
})
- .catch(() => new Flash('An error occurred while fetching label colors.'));
+ .catch(() => new Flash(__('An error occurred while fetching label colors.')));
}
static setTokenStyle(tokenValueContainer, backgroundColor, textColor) {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index f1e26cdfa21..f437954881c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -477,6 +477,16 @@ class GfmAutoComplete {
}
return null;
},
+ highlighter(li, query) {
+ // override default behaviour to escape dot character
+ // see https://github.com/ichord/At.js/pull/576
+ if (!query) {
+ return li;
+ }
+ const escapedQuery = query.replace(/[.+]/, '\\$&');
+ const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
+ return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
+ },
};
}
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index 26510fcdb2a..ce0c9256148 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { __ } from '~/locale';
export default class TransferDropdown {
constructor() {
@@ -13,7 +14,7 @@ export default class TransferDropdown {
}
buildDropdown() {
- const extraOptions = [{ id: '', text: 'No parent group' }, 'divider'];
+ const extraOptions = [{ id: '', text: __('No parent group') }, 'divider'];
this.groupDropdown.glDropdown({
selectable: true,
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index 00b2d236da3..6b0aa5b2b2b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -108,6 +108,7 @@ export default {
:placeholder="placeholder"
:value="text"
class="note-textarea ide-commit-message-textarea"
+ dir="auto"
name="commit-message"
@scroll="handleScroll"
@input="onInput"
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 229ef168926..8c84b98a108 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
-import { join as joinPath } from 'path';
+import { joinPaths } from '~/lib/utils/url_utility';
import flash from '~/flash';
import store from './stores';
+import { __ } from '~/locale';
Vue.use(VueRouter);
@@ -34,7 +35,7 @@ const EmptyRouterComponent = {
const router = new VueRouter({
mode: 'history',
- base: `${gon.relative_url_root}/-/ide/`,
+ base: joinPaths(gon.relative_url_root || '', '/-/ide/'),
routes: [
{
path: '/project/:namespace+/:project',
@@ -46,11 +47,11 @@ const router = new VueRouter({
},
{
path: ':targetmode(edit|tree|blob)/:branchid+/',
- redirect: to => joinPath(to.path, '/-/'),
+ redirect: to => joinPaths(to.path, '/-/'),
},
{
path: ':targetmode(edit|tree|blob)',
- redirect: to => joinPath(to.path, '/master/-/'),
+ redirect: to => joinPaths(to.path, '/master/-/'),
},
{
path: 'merge_requests/:mrid',
@@ -58,7 +59,7 @@ const router = new VueRouter({
},
{
path: '',
- redirect: to => joinPath(to.path, '/edit/master/-/'),
+ redirect: to => joinPaths(to.path, '/edit/master/-/'),
},
],
},
@@ -94,7 +95,7 @@ router.beforeEach((to, from, next) => {
})
.catch(e => {
flash(
- 'Error while loading the project data. Please try again.',
+ __('Error while loading the project data. Please try again.'),
'alert',
document,
null,
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 628babe6a01..f10891a8e5b 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,4 +1,5 @@
import { activityBarViews } from '../../../constants';
+import { __ } from '~/locale';
export const templateTypes = () => [
{
@@ -10,11 +11,11 @@ export const templateTypes = () => [
key: 'gitignores',
},
{
- name: 'LICENSE',
+ name: __('LICENSE'),
key: 'licenses',
},
{
- name: 'Dockerfile',
+ name: __('Dockerfile'),
key: 'dockerfiles',
},
];
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 777f8fa6691..00eb0afb3bf 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -74,7 +74,7 @@ export default {
<gl-loading-icon
v-if="isLoadingRepos"
class="js-loading-button-icon import-projects-loading-icon"
- :size="4"
+ size="md"
/>
<div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
<table class="table import-table">
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
index f666e2ebf33..ff1fd1e598e 100644
--- a/app/assets/javascripts/import_projects/store/index.js
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -7,6 +7,8 @@ import mutations from './mutations';
Vue.use(Vuex);
+export { state, actions, getters, mutations };
+
export default () =>
new Vuex.Store({
state: state(),
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 08b858305ab..a7746bb3a0b 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
+import { __ } from '~/locale';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@@ -65,10 +66,10 @@ export default class IntegrationSettingsForm {
* Toggle Submit button label based on Integration status and ability to test service
*/
toggleSubmitBtnLabel(serviceActive) {
- let btnLabel = 'Save changes';
+ let btnLabel = __('Save changes');
if (serviceActive && this.canTestService) {
- btnLabel = 'Test settings and save changes';
+ btnLabel = __('Test settings and save changes');
}
this.$submitBtnLabel.text(btnLabel);
@@ -105,7 +106,7 @@ export default class IntegrationSettingsForm {
if (data.test_failed) {
flashActions = {
- title: 'Save anyway',
+ title: __('Save anyway'),
clickHandler: e => {
e.preventDefault();
this.$form.submit();
@@ -121,7 +122,7 @@ export default class IntegrationSettingsForm {
this.toggleSubmitBtnState(false);
})
.catch(() => {
- flash('Something went wrong on our end.');
+ flash(__('Something went wrong on our end.'));
this.toggleSubmitBtnState(false);
});
}
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 385e9543973..f2462e50093 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -149,6 +149,7 @@ export default {
v-model="descriptionText"
:data-update-url="updateUrl"
class="hidden js-task-list-field"
+ dir="auto"
>
</textarea>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 299130e56ae..d27dd873125 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -53,6 +53,7 @@ export default {
v-model="formState.description"
class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea"
+ dir="auto"
data-supports-quick-actions="false"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index a3371cb9614..ce4baf17d09 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -20,6 +20,7 @@ export default {
ref="input"
v-model="formState.title"
class="form-control qa-title-input"
+ dir="auto"
type="text"
placeholder="Title"
aria-label="Title"
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 3b5c95ccded..d2f33dc31a7 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -72,6 +72,7 @@ export default {
'issue-realtime-trigger-pulse': pulseAnimation,
}"
class="title"
+ dir="auto"
v-html="titleHtml"
></h2>
<button
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index ae02559415c..498c2348ca2 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -3,10 +3,17 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
import csrf from '~/lib/utils/csrf';
-export default (resolvers = {}) =>
- new ApolloClient({
+export default (resolvers = {}, baseUrl = '') => {
+ let uri = `${gon.relative_url_root}/api/graphql`;
+
+ if (baseUrl) {
+ // Prepend baseUrl and ensure that `///` are replaced with `/`
+ uri = `${baseUrl}${uri}`.replace(/\/{3,}/g, '/');
+ }
+
+ return new ApolloClient({
link: createUploadLink({
- uri: `${gon.relative_url_root}/api/graphql`,
+ uri,
headers: {
[csrf.headerKey]: csrf.token,
},
@@ -14,3 +21,4 @@ export default (resolvers = {}) =>
cache: new InMemoryCache(),
resolvers,
});
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 4ba84589705..bdfd06fc250 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -120,3 +120,5 @@ export function webIDEUrl(route = undefined) {
}
return returnUrl;
}
+
+export { join as joinPaths } from 'path';
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 37b5409a51d..37b17f0fe23 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,3 +1,5 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+
// tell webpack to load assets from origin so that web workers don't break
// eslint-disable-next-line import/prefer-default-export
export function resetServiceWorkersPublicPath() {
@@ -5,7 +7,7 @@ export function resetServiceWorkersPublicPath() {
// the webpack publicPath setting at runtime.
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
- const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/');
__webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
// monaco-editor-webpack-plugin currently (incorrectly) references the
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1b722c0505a..a2ca4b07a66 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -135,6 +135,12 @@ function deferredInitialisation() {
});
loadAwardsHandler();
+
+ // Toggle Canary Badge
+ if (Cookies.get('gitlab_canary') && Cookies.get('gitlab_canary') === 'true') {
+ document.querySelector('.js-canary-badge').classList.remove('hidden');
+ document.querySelector('.js-canary-link').classList.add('hidden');
+ }
}
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index 547c078ec55..f7e80950803 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -290,7 +290,7 @@ export default class SSHMirror {
this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
})
.catch(() => {
- Flash(_('Unable to regenerate public ssh key.'));
+ Flash(__('Unable to regenerate public ssh key.'));
});
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index f2bd4150b6d..00547abd7bc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -221,6 +221,7 @@ export default {
<gl-dropdown-item
v-for="environment in store.environmentsData"
:key="environment.id"
+ :href="environment.metrics_path"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
>{{ environment.name }}</gl-dropdown-item
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index ed794779ff2..08dc57d545c 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import Dashboard from './components/dashboard.vue';
+import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index 5fcc2c8cfac..1efa5189996 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,7 +1,7 @@
import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
-import { s__ } from '../../locale';
+import { s__, __ } from '../../locale';
const MAX_REQUESTS = 3;
@@ -15,7 +15,7 @@ function backOffRequest(makeRequestCallback) {
if (requestCounter < MAX_REQUESTS) {
next();
} else {
- stop(new Error('Failed to connect to the prometheus server'));
+ stop(new Error(__('Failed to connect to the prometheus server')));
}
} else {
stop(resp);
diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js
index 433df844c80..c13c417cc18 100644
--- a/app/assets/javascripts/mr_popover/constants.js
+++ b/app/assets/javascripts/mr_popover/constants.js
@@ -1,10 +1,12 @@
+import { __ } from '~/locale';
+
export const mrStates = {
merged: 'merged',
closed: 'closed',
};
export const humanMRStates = {
- merged: 'Merged',
- closed: 'Closed',
- open: 'Open',
+ merged: __('Merged'),
+ closed: __('Closed'),
+ open: __('Open'),
};
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index caf22f71bf9..688c06878ac 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -351,6 +351,7 @@ Please check your network connection and try again.`;
ref="textarea"
slot="textarea"
v-model="note"
+ dir="auto"
:disabled="isSubmitting"
name="note[note]"
class="note-textarea js-vue-comment-form js-note-text
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
new file mode 100644
index 00000000000..5b6163a6214
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -0,0 +1,155 @@
+<script>
+import { mapGetters } from 'vuex';
+import { SYSTEM_NOTE } from '../constants';
+import { __ } from '~/locale';
+import NoteableNote from './noteable_note.vue';
+import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import ToggleRepliesWidget from './toggle_replies_widget.vue';
+import NoteEditedText from './note_edited_text.vue';
+
+export default {
+ name: 'DiscussionNotes',
+ components: {
+ ToggleRepliesWidget,
+ NoteEditedText,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ isExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ diffLine: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ shouldGroupReplies: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ ...mapGetters(['userCanReply']),
+ hasReplies() {
+ return !!this.replies.length;
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
+ firstNote() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ resolvedText() {
+ return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
+ },
+ commit() {
+ if (!this.discussion.for_commit) {
+ return null;
+ }
+
+ return {
+ id: this.discussion.commit_id,
+ url: this.discussion.discussion_path,
+ };
+ },
+ },
+ methods: {
+ componentName(note) {
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === SYSTEM_NOTE) {
+ return PlaceholderSystemNote;
+ }
+
+ return PlaceholderNote;
+ }
+
+ if (note.system) {
+ return SystemNote;
+ }
+
+ return NoteableNote;
+ },
+ componentData(note) {
+ return note.isPlaceholderNote ? note.notes[0] : note;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-notes">
+ <ul class="notes">
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(firstNote)"
+ :note="componentData(firstNote)"
+ :line="line"
+ :commit="commit"
+ :help-page-path="helpPagePath"
+ :show-reply-button="userCanReply"
+ @handle-delete-note="$emit('deleteNote')"
+ @start-replying="$emit('startReplying')"
+ >
+ <note-edited-text
+ v-if="discussion.resolved"
+ slot="discussion-resolved-text"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ @toggle="$emit('toggleDiscussion')"
+ />
+ <template v-if="isExpanded">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="line"
+ @handle-delete-note="$emit('deleteNote')"
+ />
+ </template>
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="diffLine"
+ @handle-delete-note="$emit('deleteNote')"
+ >
+ <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ </template>
+ </ul>
+ <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index aabb77f6a85..7e8f23d6a96 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status';
import ReplyButton from './note_actions/reply_button.vue';
export default {
@@ -14,6 +15,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [resolvedStatusMixin],
props: {
authorId: {
type: Number,
@@ -98,15 +100,6 @@ export default {
currentUserId() {
return this.getUserDataByProp('id');
},
- resolveButtonTitle() {
- let title = 'Mark as resolved';
-
- if (this.resolvedBy) {
- title = `Resolved by ${this.resolvedBy.name}`;
- }
-
- return title;
- },
},
methods: {
onEdit() {
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index fbf75ed0e41..8ddd5b8514a 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -122,6 +122,7 @@ export default {
v-model="note.note"
:data-update-url="note.path"
class="hidden js-task-list-field"
+ dir="auto"
></textarea>
<note-edited-text
v-if="note.last_edited_at"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 471323bfc83..fb098095cf3 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -268,6 +268,7 @@ export default {
:data-supports-quick-actions="!isEditing"
name="note[note]"
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ dir="auto"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleKeySubmit()"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 1e47bef7b61..2c549e7abdd 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -5,44 +5,35 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
-import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
-import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
-import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
-import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
import eventHub from '../event_hub';
+import DiscussionNotes from './discussion_notes.vue';
import DiscussionActions from './discussion_actions.vue';
export default {
name: 'NoteableDiscussion',
components: {
icon,
- noteableNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
noteEditedText,
noteForm,
- toggleRepliesWidget,
- placeholderNote,
- placeholderSystemNote,
- systemNote,
DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
TimelineEntryItem,
+ DiscussionNotes,
DiscussionActions,
},
directives: {
@@ -91,6 +82,7 @@ export default {
...mapGetters([
'convertedDisscussionIds',
'getNoteableData',
+ 'userCanReply',
'nextUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
@@ -102,21 +94,12 @@ export default {
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
},
- canReply() {
- return this.getNoteableData.current_user.can_create_note;
- },
newNotePath() {
return this.getNoteableData.create_note_path;
},
- hasReplies() {
- return this.discussion.notes.length > 1;
- },
firstNote() {
return this.discussion.notes.slice(0, 1)[0];
},
- replies() {
- return this.discussion.notes.slice(1);
- },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -222,18 +205,8 @@ export default {
return null;
},
- commit() {
- if (!this.discussion.for_commit) {
- return null;
- }
-
- return {
- id: this.discussion.commit_id,
- url: this.discussion.discussion_path,
- };
- },
resolveWithIssuePath() {
- return !this.discussionResolved && this.discussion.resolve_with_issue_path;
+ return !this.discussionResolved ? this.discussion.resolve_with_issue_path : '';
},
},
created() {
@@ -252,24 +225,6 @@ export default {
'removeConvertedDiscussion',
]),
truncateSha,
- componentName(note) {
- if (note.isPlaceholderNote) {
- if (note.placeholderType === SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
-
- return placeholderNote;
- }
-
- if (note.system) {
- return systemNote;
- }
-
- return noteableNote;
- },
- componentData(note) {
- return note.isPlaceholderNote ? note.notes[0] : note;
- },
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
@@ -399,97 +354,56 @@ Please check your network connection and try again.`;
v-bind="wrapperComponentProps"
class="card discussion-wrapper"
>
- <div class="discussion-notes">
- <ul class="notes">
- <template v-if="shouldGroupReplies">
- <component
- :is="componentName(firstNote)"
- :note="componentData(firstNote)"
- :line="line"
- :commit="commit"
- :help-page-path="helpPagePath"
- :show-reply-button="canReply"
- @handleDeleteNote="deleteNoteHandler"
- @startReplying="showReplyForm"
- >
- <note-edited-text
- v-if="discussion.resolved"
- slot="discussion-resolved-text"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- <slot slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="!isExpanded"
- :replies="replies"
- @toggle="toggleDiscussionHandler"
+ <discussion-notes
+ :discussion="discussion"
+ :diff-line="diffLine"
+ :help-page-path="helpPagePath"
+ :is-expanded="isExpanded"
+ :line="line"
+ :should-group-replies="shouldGroupReplies"
+ @startReplying="showReplyForm"
+ @toggleDiscussion="toggleDiscussionHandler"
+ @deleteNote="deleteNoteHandler"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #footer="{ showReplies }">
+ <draft-note
+ v-if="showDraft(discussion.reply_id)"
+ :key="`draft_${discussion.id}`"
+ :draft="draftForDiscussion(discussion.reply_id)"
+ />
+ <div
+ v-else-if="showReplies"
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder"
+ >
+ <discussion-actions
+ v-if="!isReplying && userCanReply"
+ :discussion="discussion"
+ :is-resolving="isResolving"
+ :resolve-button-title="resolveButtonTitle"
+ :resolve-with-issue-path="resolveWithIssuePath"
+ :should-show-jump-to-next-discussion="shouldShowJumpToNextDiscussion"
+ @showReplyForm="showReplyForm"
+ @resolve="resolveHandler"
+ @jumpToNextDiscussion="jumpToNextDiscussion"
/>
- <template v-if="isExpanded">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
- :line="line"
- @handleDeleteNote="deleteNoteHandler"
- />
- </template>
- </template>
- <template v-else>
- <component
- :is="componentName(note)"
- v-for="(note, index) in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
:line="diffLine"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- </template>
- </ul>
- <draft-note
- v-if="showDraft(discussion.reply_id)"
- :key="`draft_${discussion.id}`"
- :draft="draftForDiscussion(discussion.reply_id)"
- />
- <div
- v-else-if="isExpanded || !hasReplies"
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
- >
- <discussion-actions
- v-if="!isReplying && canReply"
- :discussion="discussion"
- :is-resolving="isResolving"
- :resolve-button-title="resolveButtonTitle"
- :resolve-with-issue-path="resolveWithIssuePath"
- :should-show-jump-to-next-discussion="shouldShowJumpToNextDiscussion"
- @showReplyForm="showReplyForm"
- @resolve="resolveHandler"
- @jumpToNextDiscussion="jumpToNextDiscussion"
- />
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- :line="diffLine"
- save-button-title="Comment"
- :autosave-key="autosaveKey"
- @handleFormUpdateAddToReview="addReplyToReview"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- <note-signed-out-widget v-if="!canReply" />
- </div>
- </div>
+ save-button-title="Comment"
+ :autosave-key="autosaveKey"
+ @handleFormUpdateAddToReview="addReplyToReview"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ <note-signed-out-widget v-if="!userCanReply" />
+ </div>
+ </template>
+ </discussion-notes>
</component>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index e2bd59f7631..0f1976db37d 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -67,6 +67,7 @@ export default {
'isLoading',
'commentsDisabled',
'getNoteableData',
+ 'userCanReply',
]),
noteableType() {
return this.noteableData.noteableType;
@@ -83,7 +84,7 @@ export default {
return this.discussions;
},
canReply() {
- return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled;
+ return this.userCanReply && !this.commentsDisabled;
},
},
watch: {
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 4f45f912479..b161773f5f1 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
import Autosave from '../../autosave';
import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
+import { s__ } from '~/locale';
export default {
methods: {
initAutoSave(noteable, extraKeys = []) {
let keys = [
- 'Note',
+ s__('Autosave|Note'),
capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType),
noteable.id,
];
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1a0dba69a7c..970e6551092 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -255,7 +255,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', noteData.flashContainer);
+ Flash(__('Commands applied'), 'notice', noteData.flashContainer);
}
if (commandsChanges) {
@@ -269,7 +269,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
})
.catch(() => {
Flash(
- 'Something went wrong while adding your award. Please try again.',
+ __('Something went wrong while adding your award. Please try again.'),
'alert',
noteData.flashContainer,
);
@@ -311,7 +311,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
data: state,
successCallback: resp =>
resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
- errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
+ errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')),
});
if (!Visibility.hidden()) {
@@ -347,7 +347,7 @@ export const fetchData = ({ commit, state, getters }) => {
.poll(requestData)
.then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters))
- .catch(() => Flash('Something went wrong while fetching latest comments.'));
+ .catch(() => Flash(__('Something went wrong while fetching latest comments.')));
};
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index fcc8889b0c7..2d150e64ef7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -20,6 +20,8 @@ export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+export const userCanReply = state => !!state.noteableData.current_user.can_create_note;
+
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 4b0feb0f94d..029fde348fb 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -1,12 +1,13 @@
import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import { sprintf, __ } from '~/locale';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
export const getQuickActionText = note => {
- let text = 'Applying command';
+ let text = __('Applying command');
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => {
@@ -16,10 +17,10 @@ export const getQuickActionText = note => {
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
- text = 'Applying multiple commands';
+ text = __('Applying multiple commands');
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
- text = `Applying command to ${commandDescription}`;
+ text = sprintf(__('Applying command to %{commandDescription}', { commandDescription }));
}
}
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 61204c37307..4a20753e7ae 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -65,18 +65,18 @@ export default class ActivityCalendar {
this.daySize = 15;
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec',
+ __('Jan'),
+ __('Feb'),
+ __('Mar'),
+ __('Apr'),
+ __('May'),
+ __('Jun'),
+ __('Jul'),
+ __('Aug'),
+ __('Sep'),
+ __('Oct'),
+ __('Nov'),
+ __('Dec'),
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6660f8120f8..b8976f77bac 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -34,6 +34,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
+ mediator: this.mediator,
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 2164e386fdb..ea82ff4e340 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { slugifyWithHyphens } from '../lib/utils/text_utility';
+import { s__ } from '~/locale';
let hasUserDefinedProjectPath = false;
@@ -114,71 +115,71 @@ const bindEvents = () => {
const value = $(this).val();
const templates = {
rails: {
- text: 'Ruby on Rails',
+ text: s__('ProjectTemplates|Ruby on Rails'),
icon: '.template-option .icon-rails',
},
express: {
- text: 'NodeJS Express',
+ text: s__('ProjectTemplates|NodeJS Express'),
icon: '.template-option .icon-express',
},
spring: {
- text: 'Spring',
+ text: s__('ProjectTemplates|Spring'),
icon: '.template-option .icon-spring',
},
iosswift: {
- text: 'iOS (Swift)',
+ text: s__('ProjectTemplates|iOS (Swift)'),
icon: '.template-option svg.icon-gitlab',
},
dotnetcore: {
- text: '.NET Core',
+ text: s__('ProjectTemplates|.NET Core'),
icon: '.template-option .icon-dotnet',
},
android: {
- text: 'Android',
+ text: s__('ProjectTemplates|Android'),
icon: '.template-option svg.icon-android',
},
gomicro: {
- text: 'Go Micro',
+ text: s__('ProjectTemplates|Go Micro'),
icon: '.template-option .icon-gomicro',
},
hugo: {
- text: 'Pages/Hugo',
+ text: s__('ProjectTemplates|Pages/Hugo'),
icon: '.template-option .icon-hugo',
},
jekyll: {
- text: 'Pages/Jekyll',
+ text: s__('ProjectTemplates|Pages/Jekyll'),
icon: '.template-option .icon-jekyll',
},
plainhtml: {
- text: 'Pages/Plain HTML',
+ text: s__('ProjectTemplates|Pages/Plain HTML'),
icon: '.template-option .icon-plainhtml',
},
gitbook: {
- text: 'Pages/GitBook',
+ text: s__('ProjectTemplates|Pages/GitBook'),
icon: '.template-option .icon-gitbook',
},
hexo: {
- text: 'Pages/Hexo',
+ text: s__('ProjectTemplates|Pages/Hexo'),
icon: '.template-option .icon-hexo',
},
nfhugo: {
- text: 'Netlify/Hugo',
+ text: s__('ProjectTemplates|Netlify/Hugo'),
icon: '.template-option .icon-netlify',
},
nfjekyll: {
- text: 'Netlify/Jekyll',
+ text: s__('ProjectTemplates|Netlify/Jekyll'),
icon: '.template-option .icon-netlify',
},
nfplainhtml: {
- text: 'Netlify/Plain HTML',
+ text: s__('ProjectTemplates|Netlify/Plain HTML'),
icon: '.template-option .icon-netlify',
},
nfgitbook: {
- text: 'Netlify/GitBook',
+ text: s__('ProjectTemplates|Netlify/GitBook'),
icon: '.template-option .icon-netlify',
},
nfhexo: {
- text: 'Netlify/Hexo',
+ text: s__('ProjectTemplates|Netlify/Hexo'),
icon: '.template-option .icon-netlify',
},
};
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index b803da798d5..def2f091947 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class ProtectedTagAccessDropdown {
constructor(options) {
this.options = options;
@@ -15,7 +17,7 @@ export default class ProtectedTagAccessDropdown {
if ($el.is('.is-active')) {
return item.text;
}
- return 'Select';
+ return __('Select');
},
clicked(options) {
options.e.preventDefault();
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index fddf2674cbb..03a5fe6b353 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import CreateItemDropdown from '../create_item_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagCreate {
constructor() {
@@ -27,7 +28,7 @@ export default class ProtectedTagCreate {
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
- defaultToggleLabel: 'Protected Tag',
+ defaultToggleLabel: __('Protected Tag'),
fieldName: 'protected_tag[name]',
onSelect: this.onSelectCallback,
getData: ProtectedTagCreate.getProtectedTags,
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index c52497e62f2..70bfd71abce 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,6 +1,7 @@
import flash from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagEdit {
constructor(options) {
@@ -47,7 +48,11 @@ export default class ProtectedTagEdit {
.catch(() => {
this.$allowedToCreateDropdownButton.enable();
- flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
+ flash(
+ __('Failed to update tag!'),
+ 'alert',
+ document.querySelector('.js-protected-tags-list'),
+ );
});
}
}
diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js
index edc2293915f..4dd0175e528 100644
--- a/app/assets/javascripts/raven/index.js
+++ b/app/assets/javascripts/raven/index.js
@@ -4,8 +4,11 @@ const index = function index() {
RavenConfig.init({
sentryDsn: gon.sentry_dsn,
currentUserId: gon.current_user_id,
- whitelistUrls: [gon.gitlab_url],
- isProduction: process.env.NODE_ENV,
+ whitelistUrls:
+ process.env.NODE_ENV === 'production'
+ ? [gon.gitlab_url]
+ : [gon.gitlab_url, 'webpack-internal://'],
+ environment: gon.sentry_environment,
release: gon.revision,
tags: {
revision: gon.revision,
diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js
index 338006ce2b9..7259e0df104 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -1,5 +1,6 @@
import Raven from 'raven-js';
import $ from 'jquery';
+import { __ } from '~/locale';
const IGNORE_ERRORS = [
// Random plugins/extensions
@@ -9,9 +10,9 @@ const IGNORE_ERRORS = [
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
- "Can't find variable: ZiteReader",
- 'jigsaw is not defined',
- 'ComboSearch is not defined',
+ __("Can't find variable: ZiteReader"),
+ __('jigsaw is not defined'),
+ __('ComboSearch is not defined'),
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
@@ -61,7 +62,7 @@ const RavenConfig = {
release: this.options.release,
tags: this.options.tags,
whitelistUrls: this.options.whitelistUrls,
- environment: this.options.isProduction ? 'production' : 'development',
+ environment: this.options.environment,
ignoreErrors: this.IGNORE_ERRORS,
ignoreUrls: this.IGNORE_URLS,
shouldSendCallback: this.shouldSendSample.bind(this),
@@ -80,7 +81,7 @@ const RavenConfig = {
handleRavenErrors(event, req, config, err) {
const error = err || req.statusText;
- const responseText = req.responseText || 'Unknown response text';
+ const responseText = req.responseText || __('Unknown response text');
Raven.captureMessage(error, {
extra: {
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 1a98564ff55..ca0fc0700ad 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class U2FError {
constructor(errorCode, u2fFlowType) {
this.errorCode = errorCode;
@@ -8,15 +10,17 @@ export default class U2FError {
message() {
if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
- return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ return __(
+ 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ );
} else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
if (this.u2fFlowType === 'authenticate') {
- return 'This device has not been registered with us.';
+ return __('This device has not been registered with us.');
}
if (this.u2fFlowType === 'register') {
- return 'This device has already been registered with us.';
+ return __('This device has already been registered with us.');
}
}
- return 'There was a problem communicating with your device.';
+ return __('There was a problem communicating with your device.');
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
index a38f25cce35..acd8037cfb2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -30,6 +30,7 @@ export default {
:id="inputId"
:value="value"
class="form-control js-gfm-input append-bottom-default commit-message-edit"
+ dir="auto"
required="required"
rows="7"
@input="$emit('input', $event.target.value)"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index aa4ecb0aac3..705ee05e29f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -119,7 +119,8 @@ export default {
},
showTargetBranchAdvancedError() {
return Boolean(
- this.mr.pipeline &&
+ this.mr.isOpen &&
+ this.mr.pipeline &&
this.mr.pipeline.target_sha &&
this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
);
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
index f01a51da0b3..ba63683f5c0 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -1,10 +1,12 @@
+import { __ } from '~/locale';
+
const viewers = {
image: {
id: 'image',
},
markdown: {
id: 'markdown',
- previewTitle: 'Preview Markdown',
+ previewTitle: __('Preview Markdown'),
},
};
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index 1f9670cf2fc..53e6efa6ea3 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -17,15 +17,13 @@ export default {
required: true,
},
},
- data() {
- return {
- milestoneDue: this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null,
- milestoneStart: this.milestone.start_date
- ? parsePikadayDate(this.milestone.start_date)
- : null,
- };
- },
computed: {
+ milestoneDue() {
+ return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null;
+ },
+ milestoneStart() {
+ return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null;
+ },
isMilestoneStarted() {
if (!this.milestoneStart) {
return false;
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 ffde55bf083..b807a35b421 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,4 +1,5 @@
<script>
+import '~/commons/bootstrap';
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from '../../components/issue/issue_milestone.vue';
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
index cafd3a515ea..c09bdfec250 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -17,10 +17,10 @@ export default {
<template>
<tr class="line_holder" :class="lineType">
- <td class="diff-line-num old_line" :class="lineType">
+ <td class="diff-line-num old_line border-top-0 border-bottom-0" :class="lineType">
{{ line.old_line }}
</td>
- <td class="diff-line-num new_line" :class="lineType">
+ <td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType">
{{ line.new_line }}
</td>
<td class="line_content" :class="lineType">
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index acc179b3834..3c86b7e4c61 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -22,6 +22,7 @@ import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
+import initMRPopovers from '~/mr_popover/';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -71,6 +72,9 @@ export default {
);
},
},
+ mounted() {
+ initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
+ },
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
index 3074ea859cc..6d2612556ff 100644
--- a/app/assets/javascripts/vue_shared/components/select2_select.vue
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import 'select2/select2';
+import 'select2';
export default {
name: 'Select2Select',
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index 549d27e96d9..2d1f7a1cfd0 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import '~/commons/bootstrap';
export default {
bind(el) {
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index 9775c329922..a104d035a9a 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -11,7 +11,7 @@
}
&-body {
- height: 120px;
+ min-height: 120px;
&-warning {
background-color: $orange-50;
@@ -22,10 +22,8 @@
}
}
- &-time-ago {
- &-icon {
- color: $gray-500;
- }
+ &-icon {
+ color: $gray-500;
}
&-footer {
diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss
new file mode 100644
index 00000000000..91d16c8e98d
--- /dev/null
+++ b/app/assets/stylesheets/components/toast.scss
@@ -0,0 +1,3 @@
+.toast-close {
+ font-size: $default-icon-size !important;
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1e025b3a67d..a6179e2a96e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -447,30 +447,29 @@
}
}
+.title-container,
.navbar-nav {
- li {
- .badge.badge-pill {
- position: inherit;
- font-weight: $gl-font-weight-normal;
- margin-left: -6px;
- font-size: 11px;
- color: $white-light;
- padding: 0 5px;
- line-height: 12px;
- border-radius: 7px;
- box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
-
- &.issues-count {
- background-color: $green-500;
- }
+ .badge.badge-pill {
+ position: inherit;
+ font-weight: $gl-font-weight-normal;
+ margin-left: -6px;
+ font-size: 11px;
+ color: $white-light;
+ padding: 0 5px;
+ line-height: 12px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
+
+ &.green-badge {
+ background-color: $green-500;
+ }
- &.merge-requests-count {
- background-color: $orange-600;
- }
+ &.merge-requests-count {
+ background-color: $orange-600;
+ }
- &.todos-count {
- background-color: $blue-500;
- }
+ &.todos-count {
+ background-color: $blue-500;
}
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 61fab445793..09ff518bbdf 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -7,6 +7,9 @@
opacity: 1 !important;
* {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
user-select: none;
// !important to make sure no style can override this when dragging
cursor: grabbing !important;
@@ -207,6 +210,7 @@
border: 1px solid $gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
line-height: $gl-padding;
+ list-style: none;
&:not(:last-child) {
margin-bottom: $gl-padding-8;
@@ -256,6 +260,10 @@
}
}
+.board-card-header {
+ text-align: initial;
+}
+
.board-card-assignee {
margin-top: -$gl-padding-4;
margin-bottom: -$gl-padding-4;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index f8620eec46d..04c66006027 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -60,6 +60,7 @@
overflow-wrap: break-word;
min-width: 0;
width: 100%;
+ text-align: initial;
}
.btn-edit {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 60a840aac1b..13288d8bad1 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -456,7 +456,9 @@
// Don't hide the overflow in system messages
.system-note-message,
-.issuable-detail {
+.issuable-detail,
+.md-preview-holder,
+.note-body {
.scoped-label-wrapper {
.badge {
overflow: initial;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 20bdc6596e9..37071a57bb3 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -75,6 +75,8 @@ input[type='checkbox']:hover {
}
.search-input-wrap {
+ width: 100%;
+
.search-icon,
.clear-icon {
position: absolute;
@@ -84,6 +86,9 @@ input[type='checkbox']:hover {
.search-icon {
transition: color $default-transition-duration;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
user-select: none;
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index b681949ab36..d445be0eb19 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -127,6 +127,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
[
*::ApplicationSettingsHelper.visible_attributes,
*::ApplicationSettingsHelper.external_authorization_service_attributes,
+ *lets_encrypt_visible_attributes,
:domain_blacklist_file,
disabled_oauth_sign_in_sources: [],
import_sources: [],
@@ -134,4 +135,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
restricted_visibility_levels: []
]
end
+
+ def lets_encrypt_visible_attributes
+ return [] unless Feature.enabled?(:pages_auto_ssl)
+
+ [
+ :lets_encrypt_notification_email,
+ :lets_encrypt_terms_of_service_accepted
+ ]
+ end
end
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index 73c744efeba..16c2365f85d 100644
--- a/app/controllers/clusters/applications_controller.rb
+++ b/app/controllers/clusters/applications_controller.rb
@@ -4,6 +4,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
before_action :cluster
before_action :authorize_create_cluster!, only: [:create]
before_action :authorize_update_cluster!, only: [:update]
+ before_action :authorize_admin_cluster!, only: [:destroy]
def create
request_handler do
@@ -21,6 +22,14 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
end
+ def destroy
+ request_handler do
+ Clusters::Applications::DestroyService
+ .new(@cluster, current_user, cluster_application_destroy_params)
+ .execute(request)
+ end
+ end
+
private
def request_handler
@@ -40,4 +49,8 @@ class Clusters::ApplicationsController < Clusters::BaseController
def cluster_application_params
params.permit(:application, :hostname, :email)
end
+
+ def cluster_application_destroy_params
+ params.permit(:application)
+ end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index e35f34be23c..d8812c023ca 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,8 +10,10 @@ 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
+ before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:metrics_time_window)
+ push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
+ push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
end
def index
@@ -134,13 +136,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics
- # Currently, this acts as a hint to load the metrics details into the cache
- # if they aren't there already
- @metrics = environment.metrics || {}
-
respond_to do |format|
format.html
format.json do
+ # Currently, this acts as a hint to load the metrics details into the cache
+ # if they aren't there already
+ @metrics = environment.metrics || {}
+
render json: @metrics, status: @metrics.any? ? :ok : :no_content
end
end
@@ -156,6 +158,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def metrics_dashboard
+ return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, project)
+
+ if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
+ result = dashboard_finder.find(project, current_user, environment, params[:dashboard])
+
+ result[:all_dashboards] = project.repository.metrics_dashboard_paths
+ else
+ result = dashboard_finder.find(project, current_user, environment)
+ end
+
+ respond_to do |format|
+ if result[:status] == :success
+ format.json do
+ render status: :ok, json: result.slice(:all_dashboards, :dashboard, :status)
+ end
+ else
+ format.json do
+ render(
+ status: result[:http_status],
+ json: result.slice(:all_dashboards, :message, :status)
+ )
+ end
+ end
+ end
+ end
+
def search
respond_to do |format|
format.json do
@@ -196,6 +225,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
params.require([:start, :end])
end
+ def dashboard_finder
+ Gitlab::Metrics::Dashboard::Finder
+ end
+
def search_environment_names
return [] unless params[:query]
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 2ef5c207d67..b4d89db20c5 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -132,18 +132,6 @@ class Projects::IssuesController < Projects::ApplicationController
render_conflict_response
end
- def referenced_merge_requests
- @merge_requests, @closed_by_merge_requests = ::Issues::ReferencedMergeRequestsService.new(project, current_user).execute(issue)
-
- respond_to do |format|
- format.json do
- render json: {
- html: view_to_html_string('projects/issues/_merge_requests')
- }
- end
- end
- end
-
def related_branches
@related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue)
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 1fafc33e917..5cfb0ac307d 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -56,6 +56,8 @@ module Projects
# overridden in EE
def permitted_project_params
{
+ metrics_setting_attributes: [:external_dashboard_url],
+
error_tracking_setting_attributes: [
:enabled,
:api_host,
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 88910c91763..fa5bdbc7d49 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -17,7 +17,7 @@ class Projects::WikisController < Projects::ApplicationController
def pages
@wiki_pages = Kaminari.paginate_array(
- @project_wiki.pages(sort: params[:sort], direction: params[:direction])
+ @project_wiki.list_pages(sort: params[:sort], direction: params[:direction])
).page(params[:page])
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
@@ -118,7 +118,7 @@ class Projects::WikisController < Projects::ApplicationController
@sidebar_page = @project_wiki.find_sidebar(params[:version_id])
unless @sidebar_page # Fallback to default sidebar
- @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.list_pages(limit: 15))
end
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.")
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 48f4d7a586d..e88c46144ef 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -36,10 +36,10 @@ class ProjectsController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def new
- namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
- return access_denied! if namespace && !can?(current_user, :create_projects, namespace)
+ @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
+ return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace)
- @project = Project.new(namespace_id: namespace&.id)
+ @project = Project.new(namespace_id: @namespace&.id)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 1afe000c5f8..a12568d5d31 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -11,7 +11,7 @@ class GitlabSchema < GraphQL::Schema
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections
- use Gitlab::Graphql::Tracing
+ use Gitlab::Graphql::GenericTracing
query_analyzer Gitlab::Graphql::QueryAnalyzers::LogQueryComplexity.analyzer
diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb
index 0f1a64b6c58..972f318c806 100644
--- a/app/graphql/resolvers/full_path_resolver.rb
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -7,14 +7,14 @@ module Resolvers
prepended do
argument :full_path, GraphQL::ID_TYPE,
required: true,
- description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
+ description: 'The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-ce"'
end
def model_by_full_path(model, full_path)
BatchLoader.for(full_path).batch(key: model) do |full_paths, loader, args|
# `with_route` avoids an N+1 calculating full_path
- args[:key].where_full_path_in(full_paths).with_route.each do |project|
- loader.call(project.full_path, project)
+ args[:key].where_full_path_in(full_paths).with_route.each do |model_instance|
+ loader.call(model_instance.full_path, model_instance)
end
end
end
diff --git a/app/graphql/resolvers/group_resolver.rb b/app/graphql/resolvers/group_resolver.rb
new file mode 100644
index 00000000000..4260e18829e
--- /dev/null
+++ b/app/graphql/resolvers/group_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class GroupResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::GroupType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Group, full_path)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index b98d8bd1fff..54d32a688bf 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -44,6 +44,12 @@ module Resolvers
alias_method :project, :object
def resolve(**args)
+ # The project could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` of the project to query for issues, so
+ # make sure it's loaded and not `nil` before continueing.
+ project.sync if project.respond_to?(:sync)
+ return Issue.none if project.nil?
+
# Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-ce/issues/54520
args[:project_id] = project.id
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
new file mode 100644
index 00000000000..a2d615ee732
--- /dev/null
+++ b/app/graphql/types/group_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ class GroupType < NamespaceType
+ graphql_name 'Group'
+
+ authorize :read_group
+
+ expose_permissions Types::PermissionTypes::Group
+
+ field :web_url, GraphQL::STRING_TYPE, null: true
+
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (group, args, ctx) do
+ group.avatar_url(only_path: false)
+ end
+
+ if ::Group.supports_nested_objects?
+ field :parent, GroupType, null: true
+ end
+ end
+end
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
new file mode 100644
index 00000000000..36d8ee8c878
--- /dev/null
+++ b/app/graphql/types/namespace_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ class NamespaceType < BaseObject
+ graphql_name 'Namespace'
+
+ field :id, GraphQL::ID_TYPE, null: false
+
+ field :name, GraphQL::STRING_TYPE, null: false
+ field :path, GraphQL::STRING_TYPE, null: false
+ field :full_name, GraphQL::STRING_TYPE, null: false
+ field :full_path, GraphQL::ID_TYPE, null: false
+
+ field :description, GraphQL::STRING_TYPE, null: true
+ field :visibility, GraphQL::STRING_TYPE, null: true
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ end
+end
diff --git a/app/graphql/types/permission_types/group.rb b/app/graphql/types/permission_types/group.rb
new file mode 100644
index 00000000000..29833993ce6
--- /dev/null
+++ b/app/graphql/types/permission_types/group.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Group < BasePermissionType
+ graphql_name 'GroupPermissions'
+
+ abilities :read_group
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index fbb4eddd13c..baea6658e05 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -66,6 +66,9 @@ module Types
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :namespace, Types::NamespaceType, null: false
+ field :group, Types::GroupType, null: true
+
field :merge_requests,
Types::MergeRequestType.connection_type,
null: true,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 0f655ab9d03..40d7de1a49a 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -9,6 +9,11 @@ module Types
resolver: Resolvers::ProjectResolver,
description: "Find a project"
+ field :group, Types::GroupType,
+ null: true,
+ resolver: Resolvers::GroupResolver,
+ description: "Find a group"
+
field :metadata, Types::MetadataType,
null: true,
resolver: Resolvers::MetadataResolver,
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 289cb44f1e8..495c29d3e24 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -4,7 +4,7 @@ module BroadcastMessagesHelper
def broadcast_message(message)
return unless message.present?
- content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
+ content_tag :div, dir: 'auto', class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message)
end
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index ad77f99fe44..dce4168ad7b 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -4,7 +4,7 @@ require 'nokogiri'
module MarkupHelper
include ActionView::Helpers::TagHelper
- include ActionView::Context
+ include ::Gitlab::ActionViewOutput::Context
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb
index 65c7cd82832..921c79ab771 100644
--- a/app/helpers/mirror_helper.rb
+++ b/app/helpers/mirror_helper.rb
@@ -7,4 +7,8 @@ module MirrorHelper
project_mirror_endpoint: project_mirror_path(@project, :json)
}
end
+
+ def mirror_lfs_sync_message
+ _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 9d71f250466..d1d01368972 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -17,6 +17,19 @@ class ApplicationRecord < ActiveRecord::Base
where(nil).pluck(self.primary_key)
end
+ def self.safe_ensure_unique(retries: 0)
+ transaction(requires_new: true) do
+ yield
+ end
+ rescue ActiveRecord::RecordNotUnique
+ if retries > 0
+ retries -= 1
+ retry
+ end
+
+ false
+ end
+
def self.safe_find_or_create_by!(*args)
safe_find_or_create_by(*args).tap do |record|
record.validate! unless record.persisted?
@@ -24,10 +37,8 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_find_or_create_by(*args)
- transaction(requires_new: true) do
+ safe_ensure_unique(retries: 1) do
find_or_create_by(*args)
end
- rescue ActiveRecord::RecordNotUnique
- retry
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 2f9b4c4eaa2..fb1e558e46c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -229,6 +229,16 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? }
+ validates :lets_encrypt_notification_email,
+ devise_email: true,
+ format: { without: /@example\.(com|org|net)\z/,
+ message: N_("Let's Encrypt does not accept emails on example.com") },
+ allow_blank: true
+
+ validates :lets_encrypt_notification_email,
+ presence: true,
+ if: :lets_encrypt_terms_of_service_accepted?
+
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index b413ffddb9d..ee12a1d09f3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -183,6 +183,22 @@ module ApplicationSettingImplementation
clientside_sentry_dsn.strip! if clientside_sentry_dsn.present?
end
+ def sentry_enabled
+ Gitlab.config.sentry.enabled || read_attribute(:sentry_enabled)
+ end
+
+ def sentry_dsn
+ Gitlab.config.sentry.dsn || read_attribute(:sentry_dsn)
+ end
+
+ def clientside_sentry_enabled
+ Gitlab.config.sentry.enabled || read_attribute(:clientside_sentry_enabled)
+ end
+
+ def clientside_sentry_dsn
+ Gitlab.config.sentry.clientside_dsn || read_attribute(:clientside_sentry_dsn)
+ end
+
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 0d8d7d95791..644716ba8e7 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -4,6 +4,7 @@ module Ci
class Bridge < CommitStatus
include Ci::Processable
include Ci::Contextable
+ include Ci::PipelineDelegator
include Importable
include AfterCommitQueue
include HasRef
@@ -13,8 +14,6 @@ module Ci
belongs_to :trigger_request
validates :ref, presence: true
- delegate :merge_request_event?, to: :pipeline
-
def self.retry(bridge, current_user)
raise NotImplementedError
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e5236051118..d2f5ff13408 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -6,6 +6,7 @@ module Ci
include Ci::Processable
include Ci::Metadatable
include Ci::Contextable
+ include Ci::PipelineDelegator
include TokenAuthenticatable
include AfterCommitQueue
include ObjectStorage::BackgroundMove
@@ -49,8 +50,6 @@ module Ci
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
- delegate :merge_request_event?, :merge_request_ref?,
- :legacy_detached_merge_request_pipeline?, to: :pipeline
##
# Since Gitlab 11.5, deployments records started being created right after
@@ -353,7 +352,7 @@ module Ci
end
def retryable?
- !archived? && (success? || failed? || canceled?)
+ !archived? && (success? || failed?)
end
def retries_count
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index bbd21eb0e78..2b7835d7fab 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -759,6 +759,18 @@ module Ci
user == current_user
end
+ def source_ref
+ if triggered_by_merge_request?
+ merge_request.source_branch
+ else
+ ref
+ end
+ end
+
+ def source_ref_slug
+ Gitlab::Utils.slugify(source_ref.to_s)
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index ac0e7eb03bc..d6a7d1d2bdd 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -24,6 +24,12 @@ module Clusters
'stable/cert-manager'
end
+ # We will implement this in future MRs.
+ # Need to reverse postinstall step
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: 'certmanager',
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 71aff00077d..a83d06c4b00 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -29,6 +29,13 @@ module Clusters
self.status = 'installable' if cluster&.platform_kubernetes_active?
end
+ # We will implement this in future MRs.
+ # Basically we need to check all other applications are not installed
+ # first.
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 376d54aab2c..a1023f44049 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -35,6 +35,13 @@ module Clusters
'stable/nginx-ingress'
end
+ # We will implement this in future MRs.
+ # Basically we need to check all dependent applications are not installed
+ # first.
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index f86ff3551a1..987c057ad6d 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -38,6 +38,12 @@ module Clusters
content_values.to_yaml
end
+ # Will be addressed in future MRs
+ # We need to investigate and document what will be permenantly deleted.
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 38cbc9ce8eb..9fbf5d8af04 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -51,6 +51,12 @@ module Clusters
{ "domain" => hostname }.to_yaml
end
+ # Handled in a new issue:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/59369
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 954c29da196..a6b7617b830 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -16,10 +16,12 @@ module Clusters
default_value_for :version, VERSION
+ after_destroy :disable_prometheus_integration
+
state_machine :status do
after_transition any => [:installed] do |application|
application.cluster.projects.each do |project|
- project.find_or_initialize_service('prometheus').update(active: true)
+ project.find_or_initialize_service('prometheus').update!(active: true)
end
end
end
@@ -47,6 +49,14 @@ module Clusters
)
end
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: name,
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files
+ )
+ end
+
def upgrade_command(values)
::Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
@@ -82,6 +92,12 @@ module Clusters
private
+ def disable_prometheus_integration
+ cluster.projects.each do |project|
+ project.prometheus_service&.update!(active: false)
+ end
+ end
+
def kube_client
cluster&.kubeclient&.core_client
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 06ab0855e40..af648db3708 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.4.0'.freeze
+ VERSION = '0.4.1'.freeze
self.table_name = 'clusters_applications_runners'
@@ -29,6 +29,13 @@ module Clusters
content_values.to_yaml
end
+ # Need to investigate if pipelines run by this runner will stop upon the
+ # executor pod stopping
+ # I.e.run a pipeline, and uninstall runner while pipeline is running
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index ee964fb7c93..4514498b84b 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -18,6 +18,16 @@ module Clusters
self.status = 'installable' if cluster&.application_helm_available?
end
+ def can_uninstall?
+ allowed_to_uninstall?
+ end
+
+ # All new applications should uninstall by default
+ # Override if there's dependencies that needs to be uninstalled first
+ def allowed_to_uninstall?
+ true
+ end
+
def self.application_name
self.to_s.demodulize.underscore
end
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 1273ed83abe..54a3dda6d75 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -25,9 +25,11 @@ module Clusters
state :updating, value: 4
state :updated, value: 5
state :update_errored, value: 6
+ state :uninstalling, value: 7
+ state :uninstall_errored, value: 8
event :make_scheduled do
- transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled
+ transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled
end
event :make_installing do
@@ -40,8 +42,9 @@ module Clusters
end
event :make_errored do
- transition any - [:updating] => :errored
+ transition any - [:updating, :uninstalling] => :errored
transition [:updating] => :update_errored
+ transition [:uninstalling] => :uninstall_errored
end
event :make_updating do
@@ -52,6 +55,10 @@ module Clusters
transition any => :update_errored
end
+ event :make_uninstalling do
+ transition [:scheduled] => :uninstalling
+ end
+
before_transition any => [:scheduled] do |app_status, _|
app_status.status_reason = nil
end
@@ -65,7 +72,7 @@ module Clusters
app_status.status_reason = nil
end
- before_transition any => [:update_errored] do |app_status, transition|
+ before_transition any => [:update_errored, :uninstall_errored] do |app_status, transition|
status_reason = transition.args.first
app_status.status_reason = status_reason if status_reason
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index a806367a49b..ca7d109d4f0 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -76,6 +76,10 @@ module Clusters
end
end
+ def namespace_for(project)
+ cluster.find_or_initialize_kubernetes_namespace_for_project(project).namespace
+ end
+
def predefined_variables(project:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'KUBE_URL', value: api_url)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 4687ec7d166..80278e07e65 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -91,7 +91,8 @@ module Avatarable
private
def retrieve_upload_from_batch(identifier)
- BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args|
+ BatchLoader.for(identifier: identifier, model: self)
+ .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args|
model_class = args[:key]
paths = upload_params.flat_map do |params|
params[:model].upload_paths(params[:identifier])
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 4986a42dbd2..e1d5ce7f7d4 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -70,8 +70,8 @@ module Ci
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
- variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
@@ -85,8 +85,8 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
- variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
variables.append(key: 'CI_BUILD_NAME', value: name)
variables.append(key: 'CI_BUILD_STAGE', value: stage)
variables.append(key: "CI_BUILD_TAG", value: ref) if tag?
diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb
new file mode 100644
index 00000000000..dbc5ed1bc9a
--- /dev/null
+++ b/app/models/concerns/ci/pipeline_delegator.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+##
+# This module is mainly used by child associations of `Ci::Pipeline` that needs to look up
+# single source of truth. For example, `Ci::Build` has `git_ref` method, which behaves
+# slightly different from `Ci::Pipeline`'s `git_ref`. This is very confusing as
+# the system could behave differently time to time.
+# We should have a single interface in `Ci::Pipeline` and access the method always.
+module Ci
+ module PipelineDelegator
+ extend ActiveSupport::Concern
+
+ included do
+ delegate :merge_request_event?,
+ :merge_request_ref?,
+ :source_ref,
+ :source_ref_slug,
+ :legacy_detached_merge_request_pipeline?, to: :pipeline
+ end
+ end
+end
diff --git a/app/models/concerns/has_ref.rb b/app/models/concerns/has_ref.rb
index 413cd36dcaa..fa0cf5ddfd2 100644
--- a/app/models/concerns/has_ref.rb
+++ b/app/models/concerns/has_ref.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+##
+# We will disable `ref` and `sha` attributes in `Ci::Build` in the future
+# and remove this module in favor of Ci::PipelineDelegator.
module HasRef
extend ActiveSupport::Concern
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index d847a0a11e4..92c7311014a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -47,6 +47,12 @@ class Deployment < ApplicationRecord
Deployments::SuccessWorker.perform_async(id)
end
end
+
+ after_transition any => [:success, :failed, :canceled] do |deployment|
+ deployment.run_after_commit do
+ Deployments::FinishedWorker.perform_async(id)
+ end
+ end
end
enum status: {
@@ -79,7 +85,16 @@ class Deployment < ApplicationRecord
end
def cluster
- project.deployment_platform(environment: environment.name)&.cluster
+ platform = project.deployment_platform(environment: environment.name)
+
+ if platform.present? && platform.respond_to?(:cluster)
+ platform.cluster
+ end
+ end
+
+ def execute_hooks
+ deployment_data = Gitlab::DataBuilder::Deployment.build(self)
+ project.execute_services(deployment_data, :deployment_hooks)
end
def last?
diff --git a/app/models/member.rb b/app/models/member.rb
index 8a06bff51b5..83b4f5b29c4 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -446,10 +446,10 @@ class Member < ApplicationRecord
end
def higher_access_level_than_group
- if highest_group_member && highest_group_member.access_level >= access_level
+ if highest_group_member && highest_group_member.access_level > access_level
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
- errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
+ errors.add(:access_level, s_("should be greater than or equal to %{access} inherited membership from group %{group_name}") % error_parameters)
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a5b62659b24..c2a1487fc6e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1054,6 +1054,16 @@ class MergeRequest < ApplicationRecord
@environments[current_user]
end
+ ##
+ # This method is for looking for active environments which created via pipelines for merge requests.
+ # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
+ # we cannot look up environments with source branch name.
+ def environments
+ return Environment.none unless actual_head_pipeline&.triggered_by_merge_request?
+
+ actual_head_pipeline.environments
+ end
+
def state_human_name
if merged?
"Merged"
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 377ac3febb6..6889e0d776b 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -119,15 +119,19 @@ class NotificationRecipient
return @read_ability if instance_variable_defined?(:@read_ability)
@read_ability =
- case @target
- when Issuable
- :"read_#{@target.to_ability_name}"
- when Ci::Pipeline
+ if @target.is_a?(Ci::Pipeline)
:read_build # We have build trace in pipeline emails
- when ActiveRecord::Base
- :"read_#{@target.class.model_name.name.underscore}"
- else
- nil
+ elsif default_ability_for_target
+ :"read_#{default_ability_for_target}"
+ end
+ end
+
+ def default_ability_for_target
+ @default_ability_for_target ||=
+ if @target.respond_to?(:to_ability_name)
+ @target.to_ability_name
+ elsif @target.class.respond_to?(:model_name)
+ @target.class.model_name.name.underscore
end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index d73b2889f30..9e806b2e232 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -38,6 +38,8 @@ class PagesDomain < ApplicationRecord
where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
end
+ scope :for_removal, -> { where("remove_at < ?", Time.now) }
+
def verified?
!!verified_at
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 626ff9e1389..a29100405f9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -188,6 +188,7 @@ class Project < ApplicationRecord
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :project_repository, inverse_of: :project
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
+ has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
@@ -297,6 +298,7 @@ class Project < ApplicationRecord
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
accepts_nested_attributes_for :error_tracking_setting, update_only: true
+ accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
diff --git a/app/models/project_metrics_setting.rb b/app/models/project_metrics_setting.rb
new file mode 100644
index 00000000000..a2a7dc571a4
--- /dev/null
+++ b/app/models/project_metrics_setting.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ProjectMetricsSetting < ApplicationRecord
+ belongs_to :project
+
+ validates :external_dashboard_url,
+ length: { maximum: 255 },
+ addressable_url: { enforce_sanitization: true, ascii_only: true }
+end
diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb
new file mode 100644
index 00000000000..656a3e6ab4b
--- /dev/null
+++ b/app/models/project_services/chat_message/deployment_message.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module ChatMessage
+ class DeploymentMessage < BaseMessage
+ attr_reader :commit_url
+ attr_reader :deployable_id
+ attr_reader :deployable_url
+ attr_reader :environment
+ attr_reader :short_sha
+ attr_reader :status
+
+ def initialize(data)
+ super
+
+ @commit_url = data[:commit_url]
+ @deployable_id = data[:deployable_id]
+ @deployable_url = data[:deployable_url]
+ @environment = data[:environment]
+ @short_sha = data[:short_sha]
+ @status = data[:status]
+ end
+
+ def attachments
+ [{
+ text: "#{project_link}\n#{deployment_link}, SHA #{commit_link}, by #{user_combined_name}",
+ color: color
+ }]
+ end
+
+ def activity
+ {}
+ end
+
+ private
+
+ def message
+ "Deploy to #{environment} #{humanized_status}"
+ end
+
+ def color
+ case status
+ when 'success'
+ 'good'
+ when 'canceled'
+ 'warning'
+ when 'failed'
+ 'danger'
+ else
+ '#334455'
+ end
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def deployment_link
+ link("Job ##{deployable_id}", deployable_url)
+ end
+
+ def commit_link
+ link(short_sha, commit_url)
+ end
+
+ def humanized_status
+ status == 'success' ? 'succeeded' : status
+ end
+ end
+end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index c10ee07ccf4..7c9ecc6b821 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -33,7 +33,7 @@ class ChatNotificationService < Service
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
+ pipeline wiki_page deployment]
end
def fields
@@ -122,6 +122,8 @@ class ChatNotificationService < Service
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
ChatMessage::WikiPageMessage.new(data)
+ when "deployment"
+ ChatMessage::DeploymentMessage.new(data)
end
end
diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb
index 405676792de..4385834ed0a 100644
--- a/app/models/project_services/discord_service.rb
+++ b/app/models/project_services/discord_service.rb
@@ -33,6 +33,11 @@ class DiscordService < ChatNotificationService
# No-op.
end
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
def default_fields
[
{ type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" },
diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb
index 272cd0f4e47..699cf1659d1 100644
--- a/app/models/project_services/hangouts_chat_service.rb
+++ b/app/models/project_services/hangouts_chat_service.rb
@@ -35,6 +35,11 @@ class HangoutsChatService < ChatNotificationService
'https://chat.googleapis.com/v1/spaces…'
end
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f650dbd3726..fc8afa9bead 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -94,6 +94,10 @@ class KubernetesService < DeploymentService
end
end
+ def namespace_for(project)
+ actual_namespace
+ end
+
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kube_client!
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index c34078f13c1..c22a6dc26f6 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -33,6 +33,11 @@ class MicrosoftTeamsService < ChatNotificationService
def default_channel_placeholder
end
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 23ddd708396..c91add6439f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -82,17 +82,25 @@ class ProjectWiki
end
def empty?
- pages(limit: 1).empty?
+ list_pages(limit: 1).empty?
end
+ # Lists wiki pages of the repository.
+ #
+ # limit - max number of pages returned by the method.
+ # sort - criterion by which the pages are sorted.
+ # direction - order of the sorted pages.
+ # load_content - option, which specifies whether the content inside the page
+ # will be loaded.
+ #
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
- 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
+ def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
+ wiki.list_pages(
+ limit: limit,
+ sort: sort,
+ direction_desc: direction == DIRECTION_DESC,
+ load_content: load_content
).map do |page|
WikiPage.new(self, page, true)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 51ab2247a03..f495a03ad8e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -39,7 +39,8 @@ class Repository
changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref has_visible_content?
- issue_template_names merge_request_template_names xcode_project?).freeze
+ issue_template_names merge_request_template_names
+ metrics_dashboard_paths xcode_project?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license).freeze
@@ -57,6 +58,7 @@ class Repository
avatar: :avatar,
issue_template: :issue_template_names,
merge_request_template: :merge_request_template_names,
+ metrics_dashboard: :metrics_dashboard_paths,
xcode_config: :xcode_project?
}.freeze
@@ -602,6 +604,11 @@ class Repository
end
cache_method :merge_request_template_names, fallback: []
+ def metrics_dashboard_paths
+ Gitlab::Metrics::Dashboard::Finder.find_all_paths_from_source(project)
+ end
+ cache_method :metrics_dashboard_paths
+
def readme
head_tree&.readme
end
@@ -1065,6 +1072,19 @@ class Repository
blob.data
end
+ def create_if_not_exists
+ return if exists?
+
+ raw.create_repository
+ after_create
+ end
+
+ def blobs_metadata(paths, ref = 'HEAD')
+ references = Array.wrap(paths).map { |path| [ref, path] }
+
+ Gitlab::Git::Blob.batch_metadata(raw, references).map { |raw_blob| Blob.decorate(raw_blob) }
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
diff --git a/app/models/service.rb b/app/models/service.rb
index de549becf71..9896aa12e90 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -50,6 +50,7 @@ class Service < ApplicationRecord
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
+ scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
@@ -335,6 +336,8 @@ class Service < ApplicationRecord
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
+ when "deployment"
+ "Event will be triggered when a deployment finishes"
end
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 1c1347c5a57..944895904fe 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -63,19 +63,11 @@ module Ci
end
def link_to_merge_request_source_branch
- return unless merge_request_presenter
-
- link_to(merge_request_presenter.source_branch,
- merge_request_presenter.source_branch_commits_path,
- class: 'ref-name')
+ merge_request_presenter&.source_branch_link
end
def link_to_merge_request_target_branch
- return unless merge_request_presenter
-
- link_to(merge_request_presenter.target_branch,
- merge_request_presenter.target_branch_commits_path,
- class: 'ref-name')
+ merge_request_presenter&.target_branch_link
end
private
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 3f7b5bebb74..ba0711ca867 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -216,6 +216,22 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
help_page_path('ci/merge_request_pipelines/index.md')
end
+ def source_branch_link
+ if source_branch_exists?
+ link_to(source_branch, source_branch_commits_path, class: 'ref-name')
+ else
+ content_tag(:span, source_branch, class: 'ref-name')
+ end
+ end
+
+ def target_branch_link
+ if target_branch_exists?
+ link_to(target_branch, target_branch_commits_path, class: 'ref-name')
+ else
+ content_tag(:span, target_branch, class: 'ref-name')
+ end
+ end
+
private
def cached_can_be_reverted?
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index a4a2c015c4e..2a916b13f52 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -10,4 +10,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
+ expose :can_uninstall?, as: :can_uninstall
end
diff --git a/app/serializers/concerns/user_status_tooltip.rb b/app/serializers/concerns/user_status_tooltip.rb
index 633b117d392..a81e377691e 100644
--- a/app/serializers/concerns/user_status_tooltip.rb
+++ b/app/serializers/concerns/user_status_tooltip.rb
@@ -3,7 +3,7 @@
module UserStatusTooltip
extend ActiveSupport::Concern
include ActionView::Helpers::TagHelper
- include ActionView::Context
+ include ::Gitlab::ActionViewOutput::Context
include EmojiHelper
include UsersHelper
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index 973ae5ce5aa..d9a800791f2 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -9,12 +9,11 @@ module Ci
return unless @ref.present?
- environments.each do |environment|
- next unless environment.stop_action_available?
- next unless can?(current_user, :stop_environment, environment)
+ environments.each { |environment| stop(environment) }
+ end
- environment.stop_with_action!(current_user)
- end
+ def execute_for_merge_request(merge_request)
+ merge_request.environments.each { |environment| stop(environment) }
end
private
@@ -24,5 +23,12 @@ module Ci
.new(project, current_user, ref: @ref, recently_updated: true)
.execute
end
+
+ def stop(environment)
+ return unless environment.stop_action_available?
+ return unless can?(current_user, :stop_environment, environment)
+
+ environment.stop_with_action!(current_user)
+ end
end
end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index c592d608b89..3c6803d24e6 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -37,7 +37,7 @@ module Clusters
end
def check_timeout
- if timeouted?
+ if timed_out?
begin
app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
end
@@ -51,8 +51,8 @@ module Clusters
install_command.pod_name
end
- def timeouted?
- Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
+ def timed_out?
+ Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb
new file mode 100644
index 00000000000..8786d295d6a
--- /dev/null
+++ b/app/services/clusters/applications/check_uninstall_progress_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class CheckUninstallProgressService < BaseHelmService
+ def execute
+ return unless app.uninstalling?
+
+ case installation_phase
+ when Gitlab::Kubernetes::Pod::SUCCEEDED
+ on_success
+ when Gitlab::Kubernetes::Pod::FAILED
+ on_failed
+ else
+ check_timeout
+ end
+ rescue Kubeclient::HttpError => e
+ log_error(e)
+
+ app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
+ end
+
+ private
+
+ def on_success
+ app.destroy!
+ rescue StandardError => e
+ app.make_errored!(_('Application uninstalled but failed to destroy: %{error_message}') % { error_message: e.message })
+ ensure
+ remove_installation_pod
+ end
+
+ def on_failed
+ app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
+ end
+
+ def check_timeout
+ if timed_out?
+ app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
+ else
+ WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
+ end
+ end
+
+ def pod_name
+ app.uninstall_command.pod_name
+ end
+
+ def timed_out?
+ Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
+ end
+
+ def remove_installation_pod
+ helm_api.delete_pod!(pod_name)
+ end
+
+ def installation_phase
+ helm_api.status(pod_name)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index ae36da7b3dd..f723c42c049 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -10,8 +10,8 @@ module Clusters
end
def builder
- cluster.method("application_#{application_name}").call ||
- cluster.method("build_application_#{application_name}").call
+ cluster.public_send(:"application_#{application_name}") || # rubocop:disable GitlabSecurity/PublicSend
+ cluster.public_send(:"build_application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb
new file mode 100644
index 00000000000..f3a4c4f754a
--- /dev/null
+++ b/app/services/clusters/applications/destroy_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class DestroyService < ::Clusters::Applications::BaseService
+ def execute(_request)
+ instantiate_application.tap do |application|
+ break unless application.can_uninstall?
+
+ application.make_scheduled!
+
+ Clusters::Applications::UninstallWorker.perform_async(application.name, application.id)
+ end
+ end
+
+ private
+
+ def builder
+ cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/uninstall_service.rb b/app/services/clusters/applications/uninstall_service.rb
new file mode 100644
index 00000000000..50c8d806c14
--- /dev/null
+++ b/app/services/clusters/applications/uninstall_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class UninstallService < BaseHelmService
+ def execute
+ return unless app.scheduled?
+
+ app.make_uninstalling!
+ uninstall
+ end
+
+ private
+
+ def uninstall
+ helm_api.uninstall(app.uninstall_command)
+
+ Clusters::Applications::WaitForUninstallAppWorker.perform_in(
+ Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
+ rescue Kubeclient::HttpError => e
+ log_error(e)
+ app.make_errored!("Kubernetes error: #{e.error_code}")
+ rescue StandardError => e
+ log_error(e)
+ app.make_errored!('Failed to uninstall.')
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb
index 5071c31839c..0fa937da865 100644
--- a/app/services/clusters/applications/update_service.rb
+++ b/app/services/clusters/applications/update_service.rb
@@ -10,7 +10,7 @@ module Clusters
end
def builder
- cluster.method("application_#{application_name}").call
+ cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index a8478e3a904..9d371e234ee 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -73,13 +73,13 @@ module Git
def push_data
@push_data ||= Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- limited_commits,
- event_message,
+ project: project,
+ user: current_user,
+ oldrev: params[:oldrev],
+ newrev: params[:newrev],
+ ref: params[:ref],
+ commits: limited_commits,
+ message: event_message,
commits_count: commits_count,
push_options: params[:push_options] || {}
)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index b8334a87f6d..a9dd26c02ad 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -24,6 +24,11 @@ module MergeRequests
end
end
+ def cleanup_environments(merge_request)
+ Ci::StopEnvironmentsService.new(merge_request.source_project, current_user)
+ .execute_for_merge_request(merge_request)
+ end
+
private
def handle_wip_event(merge_request)
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 04527bb9713..e77051bb1c9 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -17,6 +17,7 @@ module MergeRequests
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
+ cleanup_environments(merge_request)
end
merge_request
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index f26e3bee06f..c13f7dd5088 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -18,6 +18,7 @@ module MergeRequests
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
+ cleanup_environments(merge_request)
end
private
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 642465551c9..073c14040ce 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -94,16 +94,13 @@ module Projects
return unless project.lfs_enabled?
- lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
+ result = Projects::LfsPointers::LfsImportService.new(project).execute
- lfs_objects_to_download.each do |lfs_download_object|
- Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object)
- .execute
+ if result[:status] == :error
+ # To avoid aborting the importing process, we silently fail
+ # if any exception raises.
+ Gitlab::AppLogger.error("The Lfs import process failed. #{result[:message]}")
end
- rescue => e
- # Right now, to avoid aborting the importing process, we silently fail
- # if any exception raises.
- Rails.logger.error("The Lfs import process failed. #{e.message}")
end
def import_data
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index a9570176e81..05974948505 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -21,9 +21,9 @@ module Projects
# This method accepts two parameters:
# - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size }
#
- # Returns a hash with the structure { lfs_file_oids => download_link }
+ # Returns an array of LfsDownloadObject
def execute(oids)
- return {} unless project&.lfs_enabled? && remote_uri && oids.present?
+ return [] unless project&.lfs_enabled? && remote_uri && oids.present?
get_download_links(oids)
end
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
index 9215fa0a7bf..2afcce7099b 100644
--- a/app/services/projects/lfs_pointers/lfs_import_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -1,95 +1,23 @@
# frozen_string_literal: true
-# This service manages the whole worflow of discovering the Lfs files in a
-# repository, linking them to the project and downloading (and linking) the non
-# existent ones.
+# This service is responsible of managing the retrieval of the lfs objects,
+# and call the service LfsDownloadService, which performs the download
+# for each of the retrieved lfs objects
module Projects
module LfsPointers
class LfsImportService < BaseService
- include Gitlab::Utils::StrongMemoize
-
- HEAD_REV = 'HEAD'.freeze
- LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
- LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze
-
- LfsImportError = Class.new(StandardError)
-
def execute
- return {} unless project&.lfs_enabled?
+ return success unless project&.lfs_enabled?
- if external_lfs_endpoint?
- # If the endpoint host is different from the import_url it means
- # that the repo is using a third party service for storing the LFS files.
- # In this case, we have to disable lfs in the project
- disable_lfs!
+ lfs_objects_to_download = LfsObjectDownloadListService.new(project).execute
- return {}
+ lfs_objects_to_download.each do |lfs_download_object|
+ LfsDownloadService.new(project, lfs_download_object).execute
end
- get_download_links
- rescue LfsDownloadLinkListService::DownloadLinksError => e
- raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
- end
-
- private
-
- def external_lfs_endpoint?
- lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
- end
-
- def disable_lfs!
- project.update(lfs_enabled: false)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def get_download_links
- existent_lfs = LfsListService.new(project).execute
- linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys)
-
- # Retrieving those oids not linked and which we need to download
- not_linked_lfs = existent_lfs.except(*linked_oids)
-
- LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def lfsconfig_endpoint_uri
- strong_memoize(:lfsconfig_endpoint_uri) do
- # Retrieveing the blob data from the .lfsconfig file
- data = project.repository.lfsconfig_for(HEAD_REV)
- # Parsing the data to retrieve the url
- parsed_data = data&.match(LFS_ENDPOINT_PATTERN)
-
- if parsed_data
- URI.parse(parsed_data[1]).tap do |endpoint|
- endpoint.user ||= import_uri.user
- endpoint.password ||= import_uri.password
- end
- end
- end
- rescue URI::InvalidURIError
- raise LfsImportError, 'Invalid URL in .lfsconfig file'
- end
-
- def import_uri
- @import_uri ||= URI.parse(project.import_url)
- rescue URI::InvalidURIError
- raise LfsImportError, 'Invalid project import URL'
- end
-
- def current_endpoint_uri
- (lfsconfig_endpoint_uri || default_endpoint_uri)
- end
-
- # The import url must end with '.git' here we ensure it is
- def default_endpoint_uri
- @default_endpoint_uri ||= begin
- import_uri.dup.tap do |uri|
- path = uri.path.gsub(%r(/$), '')
- path += '.git' unless path.ends_with?('.git')
- uri.path = path + LFS_BATCH_API_ENDPOINT
- end
- end
+ success
+ rescue => e
+ error(e.message)
end
end
end
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index 8401f3d1d89..e3c956250f0 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -6,9 +6,9 @@ module Projects
class LfsLinkService < BaseService
# Accept an array of oids to link
#
- # Returns a hash with the same structure with oids linked
+ # Returns an array with the oid of the existent lfs objects
def execute(oids)
- return {} unless project&.lfs_enabled?
+ return [] unless project&.lfs_enabled?
# Search and link existing LFS Object
link_existing_lfs_objects(oids)
diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
new file mode 100644
index 00000000000..5ba0f50f2ff
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+# This service manages the whole worflow of discovering the Lfs files in a
+# repository, linking them to the project and downloading (and linking) the non
+# existent ones.
+module Projects
+ module LfsPointers
+ class LfsObjectDownloadListService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ HEAD_REV = 'HEAD'.freeze
+ LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
+ LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze
+
+ LfsObjectDownloadListError = Class.new(StandardError)
+
+ def execute
+ return [] unless project&.lfs_enabled?
+
+ if external_lfs_endpoint?
+ # If the endpoint host is different from the import_url it means
+ # that the repo is using a third party service for storing the LFS files.
+ # In this case, we have to disable lfs in the project
+ disable_lfs!
+
+ return []
+ end
+
+ # Getting all Lfs pointers already in the database and linking them to the project
+ linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys)
+ # Retrieving those oids not present in the database which we need to download
+ missing_oids = lfs_pointers_in_repository.except(*linked_oids) # rubocop: disable CodeReuse/ActiveRecord
+ # Downloading the required information and gathering it inside a LfsDownloadObject for each oid
+ LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids)
+ rescue LfsDownloadLinkListService::DownloadLinksError => e
+ raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
+ end
+
+ private
+
+ def external_lfs_endpoint?
+ lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
+ end
+
+ def disable_lfs!
+ unless project.update(lfs_enabled: false)
+ raise LfsDownloadLinkListService::DownloadLinksError, "Invalid project state"
+ end
+ end
+
+ # Retrieves all lfs pointers in the repository
+ def lfs_pointers_in_repository
+ @lfs_pointers_in_repository ||= LfsListService.new(project).execute
+ end
+
+ def lfsconfig_endpoint_uri
+ strong_memoize(:lfsconfig_endpoint_uri) do
+ # Retrieveing the blob data from the .lfsconfig file
+ data = project.repository.lfsconfig_for(HEAD_REV)
+ # Parsing the data to retrieve the url
+ parsed_data = data&.match(LFS_ENDPOINT_PATTERN)
+
+ if parsed_data
+ URI.parse(parsed_data[1]).tap do |endpoint|
+ endpoint.user ||= import_uri.user
+ endpoint.password ||= import_uri.password
+ end
+ end
+ end
+ rescue URI::InvalidURIError
+ raise LfsObjectDownloadListError, 'Invalid URL in .lfsconfig file'
+ end
+
+ def import_uri
+ @import_uri ||= URI.parse(project.import_url)
+ rescue URI::InvalidURIError
+ raise LfsObjectDownloadListError, 'Invalid project import URL'
+ end
+
+ def current_endpoint_uri
+ (lfsconfig_endpoint_uri || default_endpoint_uri)
+ end
+
+ # The import url must end with '.git' here we ensure it is
+ def default_endpoint_uri
+ @default_endpoint_uri ||= begin
+ import_uri.dup.tap do |uri|
+ path = uri.path.gsub(%r(/$), '')
+ path += '.git' unless path.ends_with?('.git')
+ uri.path = path + LFS_BATCH_API_ENDPOINT
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index aedf79c86d7..48eddb0e8d0 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -12,7 +12,16 @@ module Projects
private
def project_update_params
- error_tracking_params
+ error_tracking_params.merge(metrics_setting_params)
+ end
+
+ def metrics_setting_params
+ attribs = params[:metrics_setting_attributes]
+ return {} unless attribs
+
+ destroy = attribs[:external_dashboard_url].blank?
+
+ { metrics_setting_attributes: attribs.merge(_destroy: destroy) }
end
def error_tracking_params
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index cab507946b4..4f6ae07be7d 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -41,12 +41,11 @@ module Tags
def build_push_data(tag)
Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- tag.dereferenced_target.sha,
- Gitlab::Git::BLANK_SHA,
- "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
- [])
+ project: project,
+ user: current_user,
+ oldrev: tag.dereferenced_target.sha,
+ newrev: Gitlab::Git::BLANK_SHA,
+ ref: "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}")
end
end
end
diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb
index 6607f5b2418..a71278e8b8b 100644
--- a/app/services/test_hooks/project_service.rb
+++ b/app/services/test_hooks/project_service.rb
@@ -56,7 +56,7 @@ module TestHooks
end
def wiki_page_events_data
- page = project.wiki.pages.first
+ page = project.wiki.list_pages(limit: 1).first
if !project.wiki_enabled? || page.blank?
throw(:validation_error, s_('TestHooks|Ensure the wiki is enabled and has pages.'))
end
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index ebfb20132d0..4743e9b02ce 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -37,8 +37,8 @@ module Todos
private
def enqueue_private_features_worker
- project_ids.each do |project_id|
- TodosDestroyer::PrivateFeaturesWorker.perform_async(project_id, user.id)
+ projects.each do |project|
+ TodosDestroyer::PrivateFeaturesWorker.perform_async(project.id, user.id)
end
end
@@ -62,9 +62,8 @@ module Todos
end
# rubocop: enable CodeReuse/ActiveRecord
- override :project_ids
# rubocop: disable CodeReuse/ActiveRecord
- def project_ids
+ def projects
condition = case entity
when Project
{ id: entity.id }
@@ -72,13 +71,13 @@ module Todos
{ namespace_id: non_member_groups }
end
- Project.where(condition).select(:id)
+ Project.where(condition)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def non_authorized_projects
- project_ids.where('id NOT IN (?)', user.authorized_projects.select(:id))
+ projects.where('id NOT IN (?)', user.authorized_projects.select(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -110,7 +109,7 @@ module Todos
authorized_reporter_projects = user
.authorized_projects(Gitlab::Access::REPORTER).select(:id)
- Issue.where(project_id: project_ids, confidential: true)
+ Issue.where(project_id: projects, confidential: true)
.where('project_id NOT IN(?)', authorized_reporter_projects)
.where('author_id != ?', user.id)
.where('id NOT IN (?)', assigned_ids)
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
index 41b787515b5..1da5f6fccd6 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -1,6 +1,12 @@
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-logging-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
+ %p
+ %strong
+ NOTE:
+ These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml.
+ In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production.
+
%fieldset
.form-group
.form-check
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index ad5c8d4da22..64e01fa2d00 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -5,16 +5,33 @@
.form-group
= f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-bold'
= f.number_field :max_pages_size, class: 'form-control'
- .form-text.text-muted 0 for unlimited
+ .form-text.text-muted
+ = _("0 for unlimited")
.form-group
.form-check
= f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
= f.label :pages_domain_verification_enabled, class: 'form-check-label' do
- Require users to prove ownership of custom domains
+ = _("Require users to prove ownership of custom domains")
.form-text.text-muted
- Domain verification is an essential security measure for public GitLab
- sites. Users are required to demonstrate they control a domain before
- it is enabled
+ = _("Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled")
= link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
+ - if Feature.enabled?(:pages_auto_ssl)
+ %h5
+ = _("Configure Let's Encrypt")
+ %p
+ - lets_encrypt_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: "https://letsencrypt.org/" }
+ = _("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites.").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: '</a>'.html_safe }
+ .form-group
+ = f.label :lets_encrypt_notification_email, _("Email"), class: 'label-bold'
+ = f.text_field :lets_encrypt_notification_email, class: 'form-control'
+ .form-text.text-muted
+ = _("A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates.")
+ .form-group
+ .form-check
+ = f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
+ = f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
+ // Terms of Service should actually be a link, but the best way to get the url is using API
+ // So it will be done in later MR
+ = _("I have read and agree to the Let's Encrypt Terms of Service")
- = f.submit 'Save changes', class: "btn btn-success"
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index c465d9f51d6..46beca0465e 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -14,6 +14,7 @@
.col-sm-10
= f.text_area :message, class: "form-control js-autosize",
required: true,
+ dir: 'auto',
data: { preview_path: preview_admin_broadcast_messages_path }
.form-group.row.js-toggle-colors-container
.col-sm-10.offset-sm-2
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index aecfdea10d9..12a8d9930b7 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -33,7 +33,9 @@
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")
+ %p.masking-validation-error.gl-field-error.hide
+ = s_("CiVariables|Cannot use Masked Variable with current value")
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'masked-variables'), target: '_blank', rel: 'noopener noreferrer'
- unless only_key_value
.ci-variable-body-item.ci-variable-protected-item
.append-right-default
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index e38a16e7a1a..80d706ae3d3 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -4,7 +4,7 @@
- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 96d6553a2ac..b02fdb4b638 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -11,7 +11,8 @@
= link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
- %span.event-target-title.append-right-4= "&quot;".html_safe + event.target.title + "&quot".html_safe
+ %span.event-target-title.append-right-4{ dir: "auto" }
+ = "&quot;".html_safe + event.target.title + "&quot".html_safe
- else
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event)
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 90ed8e41d32..7e2103287f7 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -7,7 +7,8 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
= event_note_title_html(event)
- %span.event-target-title.append-right-4= "&quot;".html_safe + event.target.title + "&quot".html_safe
+ %span.event-target-title.append-right-4{ dir: "auto" }
+ = "&quot;".html_safe + event.target.title + "&quot".html_safe
= render "events/event_scope", event: event
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 2f635757902..0c8f86c2822 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index d0f5cd94002..d21496ee0aa 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title "CI / CD Settings"
- page_title "CI / CD"
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 916f98a62d1..75e4dc46c9b 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,6 +1,7 @@
%div
- if Gitlab::CurrentSettings.help_page_text.present?
- = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text)
+ .prepend-top-default.md
+ = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text)
%hr
%h1
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index a9b85889846..319d0307f78 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -17,6 +17,8 @@
- if logo_text.present?
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
+ %span.js-canary-badge.badge.badge-pill.green-badge.align-self-center
+ = _('Next')
- if current_user
= render "layouts/nav/dashboard"
@@ -38,7 +40,7 @@
= link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
- %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
+ %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index cd9128c452b..c53bfd8a85d 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -7,3 +7,6 @@
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
+ - if Gitlab.com?
+ %li.js-canary-link
+ = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 77d2e65d285..9ab648e2a64 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -3,7 +3,7 @@
#{link_to @merge_request.author_name, user_url(@merge_request.author)} created a merge request:
%p.details
- != merge_path_description(@merge_request, '&rarr;')
+ = merge_path_description(@merge_request, '→')
- if @merge_request.assignees.any?
%p
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 50adc19f524..3ca4abddbb8 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -80,6 +80,8 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
+ = render_if_exists "projects/home_mirror"
+
- if @project.badges.present?
.project-badges.mb-2
- @project.badges.each do |badge|
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index afc40ca4eab..c502b392384 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -8,6 +8,7 @@
= f.text_area attr,
class: classes,
placeholder: placeholder,
+ dir: 'auto',
data: { supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete }
- else
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index 888be4ee282..ed3c9890efd 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.no-animate#cleanup{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index ff6a9d49a61..59efcde5825 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 24d665761cc..fcf27351a21 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index c04530dc62c..c15b84d0aac 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("General Settings")
- page_title _("General")
- @content_class = "limit-container-width" unless fluid_layout
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 945d1b00b08..9293aa1b309 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -6,7 +6,7 @@
.issuable-info-container
.issuable-main-info
.issue-title.title
- %span.issue-title-text
+ %span.issue-title-text{ dir: "auto" }
- if issue.confidential?
%span.has-tooltip{ title: _('Confidential') }
= confidential_icon(issue)
@@ -39,6 +39,8 @@
- presented_labels_sorted_by_title(issue.labels, issue.project).each do |label|
= link_to_label(label, css_class: 'label-link')
+ = render_if_exists "projects/issues/issue_weight", issue: issue
+
.issuable-meta
%ul.controls
- if issue.closed?
diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml
deleted file mode 100644
index 90838a75214..00000000000
--- a/app/views/projects/issues/_merge_requests_status.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-- time_format = '%b %e, %Y %l:%M%P %Z%z'
-
-- if merge_request.merged?
- - mr_status_date = merge_request.merged_at
- - mr_status_title = _('Merged')
- - mr_status_icon = 'merge'
- - mr_status_class = 'merged'
-- elsif merge_request.closed?
- - mr_status_date = merge_request.closed_event&.created_at
- - mr_status_title = _('Closed')
- - mr_status_icon = 'issue-close'
- - mr_status_class = 'closed'
-- else
- - mr_status_date = merge_request.created_at
- - mr_status_title = mr_status_date ? _('Opened') : _('Open')
- - mr_status_icon = 'issue-open-m'
- - mr_status_class = 'open'
-
-- if mr_status_date
- - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>"
-- else
- - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span></div>"
-
-%span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } }
- = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}")
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index c6ff0d50ef4..d1601d7fd10 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -2,8 +2,7 @@
- breadcrumb_title _("New")
- page_title _("New Issue")
-%h3.page-title
- _("New Issue")
+%h3.page-title= _("New Issue")
%hr
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 0bf664d5b66..715c36fa9aa 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -15,7 +15,7 @@
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
= sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
.d-none.d-sm-block
- - if @issue.moved?
+ - if @issue.moved? && can?(current_user, :read_issue, @issue.moved_to)
- moved_link_start = "<a href=\"#{issue_path(@issue.moved_to)}\" class=\"text-white text-underline\">".html_safe
- moved_link_end = '</a>'.html_safe
= s_('IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})').html_safe % {moved_link_start: moved_link_start,
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 79c586eef73..05aeb5d972b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -37,21 +37,21 @@
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
- Discussion
+ = _("Discussion")
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= tab_link_for @merge_request, :commits do
- Commits
+ = _("Commits")
%span.badge.badge-pill= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= tab_link_for @merge_request, :pipelines do
- Pipelines
+ = _("Pipelines")
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do
- Changes
+ = _("Changes")
%span.badge.badge-pill= @merge_request.diff_size
.d-inline-flex.flex-wrap
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 35a6885318a..33e5a6e67c3 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -7,7 +7,7 @@
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
= _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
- %li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ %li= mirror_lfs_sync_message
%li
= _('This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.')
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index c031815200b..0cd00d3e708 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
%section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) }
@@ -11,7 +11,7 @@
= link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
.settings-content
- = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f|
+ = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'new-password', data: mirrors_form_data_attributes } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title= _('Mirror a repository')
@@ -20,7 +20,7 @@
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
+ = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password'
= render 'projects/mirrors/instructions'
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 95027634de2..d7e16dbd40c 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,6 +16,7 @@
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
+ = render_if_exists 'projects/new_ci_cd_banner_external_repo'
%p
- pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank'
= _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
@@ -42,6 +43,7 @@
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Import project
%span.d-block.d-sm-none Import
+ = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
.tab-content.gitlab-tab-content
.tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
@@ -68,6 +70,8 @@
%h4 No import options available
%p Contact an administrator to enable options for importing your project.
+ = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab
+
.save-project-loader.d-none
.center
%h2
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 539b184e5c2..63748d8d85f 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.qa-protected-branches-settings.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 9a50a51e4be..b0c87ac8c17 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 548b7c06867..5e3e1076c2c 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -2,7 +2,7 @@
- page_title _("CI / CD Settings")
- page_title _("CI / CD")
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 5b25a67bc87..8ae2807729b 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -21,10 +21,9 @@
- if @scope == 'projects'
.term
= render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- - elsif %w[blobs wiki_blobs].include?(@scope)
- = render partial: 'search/results/blob', collection: @search_objects, locals: { projects: blob_projects(@search_objects) }
- else
- = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
+ = render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
- if @scope != 'projects'
= paginate_collection(@search_objects)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index b351ecd4edf..5847751b268 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,5 +1,5 @@
- project = find_project_for_result_blob(projects, wiki_blob)
- wiki_blob = parse_search_result(wiki_blob)
-- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
+- wiki_blob_link = project_wiki_path(project, Pathname.new(wiki_blob.filename).sub_ext(''))
= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index aa428f9fe73..d90a6d43761 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,8 +1,8 @@
%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
- %span.collapse-text _("Collapse sidebar")
+ %span.collapse-text= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do
= sprite_icon('close', size: 16)
- %span.collapse-text _("Close sidebar")
+ %span.collapse-text= _("Close sidebar")
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 56c4b021eab..75e9ab547ce 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -6,7 +6,7 @@
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title')
+ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title'), dir: 'auto'
- if issuable.respond_to?(:work_in_progress?)
.form-text.text-muted
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f9b2e698fc9..e4e85de93da 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -6,6 +6,7 @@
- cronjob:gitlab_usage_ping
- cronjob:import_export_project_cleanup
- cronjob:pages_domain_verification_cron
+- cronjob:pages_domain_removal_cron
- cronjob:pipeline_schedule
- cronjob:prune_old_events
- cronjob:remove_expired_group_links
@@ -32,6 +33,8 @@
- gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_configure
- gcp_cluster:cluster_project_configure
+- gcp_cluster:clusters_applications_wait_for_uninstall_app
+- gcp_cluster:clusters_applications_uninstall
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -83,6 +86,7 @@
- pipeline_processing:ci_build_schedule
- deployment:deployments_success
+- deployment:deployments_finished
- repository_check:repository_check_clear
- repository_check:repository_check_batch
diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb
new file mode 100644
index 00000000000..85e8ecc4ad5
--- /dev/null
+++ b/app/workers/clusters/applications/uninstall_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class UninstallWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::UninstallService.new(app).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
new file mode 100644
index 00000000000..163c99d3c3c
--- /dev/null
+++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class WaitForUninstallAppWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::CheckUninstallProgressService.new(app).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/deployments/finished_worker.rb b/app/workers/deployments/finished_worker.rb
new file mode 100644
index 00000000000..c9d448d5d18
--- /dev/null
+++ b/app/workers/deployments/finished_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Deployments
+ class FinishedWorker
+ include ApplicationWorker
+
+ queue_namespace :deployment
+
+ def perform(deployment_id)
+ Deployment.find_by_id(deployment_id).try(:execute_hooks)
+ end
+ end
+end
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
new file mode 100644
index 00000000000..3aca123e5ac
--- /dev/null
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class PagesDomainRemovalCronWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ return unless Feature.enabled?(:remove_disabled_domains)
+
+ PagesDomain.for_removal.find_each do |domain|
+ domain.destroy!
+ rescue => e
+ Raven.capture_exception(e)
+ end
+ end
+end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 02a69ea3b54..8a9ee7808e4 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,20 +3,26 @@
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
+ include ::Gitlab::ExclusiveLeaseHelpers
+
+ EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock'
+ LOCK_TIMEOUT = 50.minutes
# rubocop: disable CodeReuse/ActiveRecord
def perform
- Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
- .preload(:owner, :project).find_each do |schedule|
-
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
- rescue => e
- error(schedule, e)
- ensure
- schedule.schedule_next_run!
+ in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
+ Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
+ .preload(:owner, :project).find_each do |schedule|
+
+ schedule.schedule_next_run!
+
+ Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
+ rescue => e
+ error(schedule, e)
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 337efa7919b..9a9c0c9d803 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -21,8 +21,10 @@ class PostReceive
if repo_type.wiki?
process_wiki_changes(post_received)
- else
+ elsif repo_type.project?
process_project_changes(post_received)
+ else
+ # Other repos don't have hooks for now
end
end
diff --git a/babel.config.js b/babel.config.js
index 78d14095b0b..df30892731d 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -38,8 +38,9 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
-// Jest is running in node environment
-if (BABEL_ENV === 'jest') {
+// Jest is running in node environment, so we need additional plugins
+const isJest = !!process.env.JEST_WORKER_ID;
+if (isJest) {
plugins.push('@babel/plugin-transform-modules-commonjs');
/*
without the following, babel-plugin-istanbul throws an error:
diff --git a/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml b/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml
new file mode 100644
index 00000000000..89d2fced6e1
--- /dev/null
+++ b/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Sidekiq Reliable Fetcher for background jobs by default
+merge_request: 27530
+author:
+type: added
diff --git a/changelogs/unreleased/11254-overflow-ce.yml b/changelogs/unreleased/11254-overflow-ce.yml
new file mode 100644
index 00000000000..dcac46000ac
--- /dev/null
+++ b/changelogs/unreleased/11254-overflow-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Hide ScopedBadge overflow notes
+merge_request: 27651
+author:
+type: fixed
diff --git a/changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml b/changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml
new file mode 100644
index 00000000000..ef9172aaf3b
--- /dev/null
+++ b/changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml
@@ -0,0 +1,5 @@
+---
+title: Add .NET Core YAML template
+merge_request: 25604
+author: Piotr Wosiek
+type: added
diff --git a/changelogs/unreleased/46048-canary-next.yml b/changelogs/unreleased/46048-canary-next.yml
new file mode 100644
index 00000000000..1a702cccff9
--- /dev/null
+++ b/changelogs/unreleased/46048-canary-next.yml
@@ -0,0 +1,5 @@
+---
+title: Adds badge for Canary environment and help link
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/48479-auto-direction-for-issue-title.yml b/changelogs/unreleased/48479-auto-direction-for-issue-title.yml
new file mode 100644
index 00000000000..0571f58ab4d
--- /dev/null
+++ b/changelogs/unreleased/48479-auto-direction-for-issue-title.yml
@@ -0,0 +1,5 @@
+---
+title: Add auto direction for issue title
+merge_request: 27378
+author: Ahmad Haghighi
+type: fixed
diff --git a/changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml b/changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml
new file mode 100644
index 00000000000..48f0a668982
--- /dev/null
+++ b/changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Make canceled jobs not retryable
+merge_request: 27503
+author:
+type: changed
diff --git a/changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml b/changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml
deleted file mode 100644
index 8b4f4894048..00000000000
--- a/changelogs/unreleased/54656-500-error-on-save-of-general-pipeline-settings-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 in general pipeline settings when passing an invalid build timeout.
-merge_request: 27416
-author:
-type: fixed
diff --git a/changelogs/unreleased/55948-help-text-formatting-wiki.yml b/changelogs/unreleased/55948-help-text-formatting-wiki.yml
new file mode 100644
index 00000000000..e1e0475a117
--- /dev/null
+++ b/changelogs/unreleased/55948-help-text-formatting-wiki.yml
@@ -0,0 +1,5 @@
+---
+title: Format extra help page text like wiki
+merge_request: 26782
+author: Bastian Blank
+type: fixed
diff --git a/changelogs/unreleased/57017-add-toast-success-message.yml b/changelogs/unreleased/57017-add-toast-success-message.yml
new file mode 100644
index 00000000000..931e7755591
--- /dev/null
+++ b/changelogs/unreleased/57017-add-toast-success-message.yml
@@ -0,0 +1,5 @@
+---
+title: Display a toast message when the Kubernetes runner has successfully upgraded.
+merge_request: 27206
+author:
+type: changed
diff --git a/changelogs/unreleased/57171-add-dashboard-settings.yml b/changelogs/unreleased/57171-add-dashboard-settings.yml
new file mode 100644
index 00000000000..f235872b35c
--- /dev/null
+++ b/changelogs/unreleased/57171-add-dashboard-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add backend support for a External Dashboard URL setting
+merge_request: 27550
+author:
+type: added
diff --git a/changelogs/unreleased/58294-discussion-notes-component.yml b/changelogs/unreleased/58294-discussion-notes-component.yml
new file mode 100644
index 00000000000..fbe08360a9a
--- /dev/null
+++ b/changelogs/unreleased/58294-discussion-notes-component.yml
@@ -0,0 +1,5 @@
+---
+title: Extract DiscussionNotes component from NoteableDiscussion
+merge_request: 27066
+author:
+type: other
diff --git a/changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml b/changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml
deleted file mode 100644
index 7b5fae016bb..00000000000
--- a/changelogs/unreleased/60540-merge-request-popover-is-not-working-on-the-to-do-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix MR popover on ToDos page
-merge_request: 27382
-author:
-type: fixed
diff --git a/changelogs/unreleased/60552-period-dropdown.yml b/changelogs/unreleased/60552-period-dropdown.yml
new file mode 100644
index 00000000000..e1b4a098ab0
--- /dev/null
+++ b/changelogs/unreleased/60552-period-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix autocomplete dropdown for usernames starting with period
+merge_request: 27533
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml b/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml
new file mode 100644
index 00000000000..b340f8408f3
--- /dev/null
+++ b/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml
@@ -0,0 +1,6 @@
+---
+title: Only show the "target branch has advanced" message when the merge request is
+ open
+merge_request: 27588
+author:
+type: fixed
diff --git a/changelogs/unreleased/60874-fix-suggestion-misalignment.yml b/changelogs/unreleased/60874-fix-suggestion-misalignment.yml
new file mode 100644
index 00000000000..f5717ac19fd
--- /dev/null
+++ b/changelogs/unreleased/60874-fix-suggestion-misalignment.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Misalignment on suggested changes diff table
+merge_request: 27612
+author:
+type: fixed
diff --git a/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml
new file mode 100644
index 00000000000..32f0e023923
--- /dev/null
+++ b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml
@@ -0,0 +1,5 @@
+---
+title: Fix base domain help text update
+merge_request: 27746
+author:
+type: fixed
diff --git a/changelogs/unreleased/bw-add-graphql-groups.yml b/changelogs/unreleased/bw-add-graphql-groups.yml
new file mode 100644
index 00000000000..f72ee1cf2b7
--- /dev/null
+++ b/changelogs/unreleased/bw-add-graphql-groups.yml
@@ -0,0 +1,5 @@
+---
+title: Add initial GraphQL query for Groups
+merge_request: 27492
+author:
+type: added
diff --git a/changelogs/unreleased/da-sentry-client-side-settings.yml b/changelogs/unreleased/da-sentry-client-side-settings.yml
new file mode 100644
index 00000000000..e36ac7c354b
--- /dev/null
+++ b/changelogs/unreleased/da-sentry-client-side-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Sentry client-side DSN to be passed on gitlab.yml
+merge_request: 27967
+author:
+type: added
diff --git a/changelogs/unreleased/feat-sentry-environment.yml b/changelogs/unreleased/feat-sentry-environment.yml
new file mode 100644
index 00000000000..44ea19375f8
--- /dev/null
+++ b/changelogs/unreleased/feat-sentry-environment.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Sentry configuration to be passed on gitlab.yml
+merge_request: 27091
+author: Roger Meier
+type: added
diff --git a/changelogs/unreleased/fix-api-ide-relative-url-root.yml b/changelogs/unreleased/fix-api-ide-relative-url-root.yml
new file mode 100644
index 00000000000..8c058645f3e
--- /dev/null
+++ b/changelogs/unreleased/fix-api-ide-relative-url-root.yml
@@ -0,0 +1,5 @@
+---
+title: Fix FE API and IDE handling of '/' relative_url_root
+merge_request: 27635
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml b/changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml
new file mode 100644
index 00000000000..58f5a9c943c
--- /dev/null
+++ b/changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml
@@ -0,0 +1,6 @@
+---
+title: Fix Blob.lazy always loading all previously-requested blobs when a new request
+ is made
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml b/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml
new file mode 100644
index 00000000000..80936245f3e
--- /dev/null
+++ b/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix webpack assets handling when relative url root is '/'
+merge_request: 27909
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml b/changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml
new file mode 100644
index 00000000000..49eaff52e5a
--- /dev/null
+++ b/changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml
@@ -0,0 +1,5 @@
+---
+title: Added list_pages method to avoid loading all wiki pages content
+merge_request: 22801
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-60827-fix-web-strategy-error.yml b/changelogs/unreleased/fj-60827-fix-web-strategy-error.yml
deleted file mode 100644
index ad707ca0225..00000000000
--- a/changelogs/unreleased/fj-60827-fix-web-strategy-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug when project export to remote url fails
-merge_request: 27614
-author:
-type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.36.0.yml b/changelogs/unreleased/gitaly-version-v1.36.0.yml
new file mode 100644
index 00000000000..22fdca8da80
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.36.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.36.0
+merge_request: 27831
+author:
+type: changed
diff --git a/changelogs/unreleased/issue-42692-deployment-chat-notifications.yml b/changelogs/unreleased/issue-42692-deployment-chat-notifications.yml
new file mode 100644
index 00000000000..3f0a96ad50e
--- /dev/null
+++ b/changelogs/unreleased/issue-42692-deployment-chat-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Add deployment events to chat notification services
+merge_request: 27338
+author:
+type: added
diff --git a/changelogs/unreleased/jc-client-gitaly-session-id.yml b/changelogs/unreleased/jc-client-gitaly-session-id.yml
new file mode 100644
index 00000000000..ae5b7144b98
--- /dev/null
+++ b/changelogs/unreleased/jc-client-gitaly-session-id.yml
@@ -0,0 +1,5 @@
+---
+title: Add gitaly session id & catfile-cache feature flag
+merge_request: 27472
+author:
+type: performance
diff --git a/changelogs/unreleased/jc-update-list-last-commits.yml b/changelogs/unreleased/jc-update-list-last-commits.yml
new file mode 100644
index 00000000000..0e72c4255ae
--- /dev/null
+++ b/changelogs/unreleased/jc-update-list-last-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Client side changes for ListLastCommitsForTree response update
+merge_request: 26880
+author:
+type: fixed
diff --git a/changelogs/unreleased/pl-upgrade-letter_opener_web.yml b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml
new file mode 100644
index 00000000000..9891344215a
--- /dev/null
+++ b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade letter_opener_web to support Rails 5.1
+merge_request: 27829
+author:
+type: fixed
diff --git a/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml b/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml
new file mode 100644
index 00000000000..03d94c39c10
--- /dev/null
+++ b/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'refactor(issue): Refactored issue tests from Karma to Jest'
+merge_request: 27673
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml b/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml
new file mode 100644
index 00000000000..9a1886797da
--- /dev/null
+++ b/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'Refactored notes tests from Karma to Jest'
+merge_request: 27648
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml
new file mode 100644
index 00000000000..20a4be8c9ad
--- /dev/null
+++ b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'Refactored Karma spec files to Jest'
+merge_request: 27688
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml
new file mode 100644
index 00000000000..9b208cbaa0e
--- /dev/null
+++ b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml
@@ -0,0 +1,5 @@
+---
+title: Remove pages domains if they weren't verified for 1 week
+merge_request: 26227
+author:
+type: added
diff --git a/changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml b/changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml
new file mode 100644
index 00000000000..adbed52db81
--- /dev/null
+++ b/changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml
@@ -0,0 +1,5 @@
+---
+title: Allow a member to have an access level equal to parent group
+merge_request: 27913
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml
new file mode 100644
index 00000000000..00f897ac4b1
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml
@@ -0,0 +1,5 @@
+---
+title: Disable method replacement in avatar loading
+merge_request: 27866
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml b/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml
new file mode 100644
index 00000000000..e855684bab1
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Disable password autocomplete in mirror repository form
+merge_request: 27542
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml
new file mode 100644
index 00000000000..f36c1d0e77e
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.4.1
+merge_request: 27627
+author:
+type: other
diff --git a/changelogs/unreleased/wiki-search-results-fix.yml b/changelogs/unreleased/wiki-search-results-fix.yml
new file mode 100644
index 00000000000..693867eb385
--- /dev/null
+++ b/changelogs/unreleased/wiki-search-results-fix.yml
@@ -0,0 +1,5 @@
+---
+title: fix wiki search result links in titles
+merge_request: 27400
+author: khm
+type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index bdac5b2a6a1..2f822805b25 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -315,6 +315,14 @@ production: &base
# path: shared/registry
# issuer: gitlab-issuer
+
+ ## Error Reporting and Logging with Sentry
+ sentry:
+ # enabled: false
+ # dsn: https://<key>@sentry.io/<project>
+ # clientside_dsn: https://<key>@sentry.io/<project>
+ # environment: 'production' # e.g. development, staging, production
+
#
# 2. GitLab CI settings
# ==========================
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 3c426cdb969..d56bd7654af 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -136,6 +136,8 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80)
Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || ''
+# / is not a valid relative URL root
+Settings.gitlab['relative_url_root'] = '' if Settings.gitlab['relative_url_root'] == '/'
Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http"
Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
@@ -216,6 +218,15 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi
Settings.registry['path'] = Settings.absolute(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'))
#
+# Error Reporting and Logging with Sentry
+#
+Settings['sentry'] ||= Settingslogic.new({})
+Settings.sentry['enabled'] ||= false
+Settings.sentry['dsn'] ||= nil
+Settings.sentry['environment'] ||= nil
+Settings.sentry['clientside_dsn'] ||= nil
+
+#
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
@@ -338,6 +349,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
+Settings.cron_jobs['pages_domain_removal_cron_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['pages_domain_removal_cron_worker']['cron'] ||= '47 0 * * *'
+Settings.cron_jobs['pages_domain_removal_cron_worker']['job_class'] = 'PagesDomainRemovalCronWorker'
+
Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 680cfa6f0ed..e5589ce0ad1 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -14,6 +14,7 @@ def configure_sentry
Raven.configure do |config|
config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn
config.release = Gitlab.revision
+ config.current_environment = Gitlab.config.sentry.environment.presence
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 2e4aa9c1053..7b69cf11288 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,5 +1,17 @@
require 'sidekiq/web'
+def enable_reliable_fetch?
+ return true unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:gitlab_sidekiq_reliable_fetcher, default_enabled: true)
+end
+
+def enable_semi_reliable_fetch_mode?
+ return true unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
+end
+
# Disable the Sidekiq Rack session since GitLab already has its own session store.
# CSRF protection still works (https://github.com/mperham/sidekiq/commit/315504e766c4fd88a29b7772169060afc4c40329).
Sidekiq::Web.set :sessions, false
@@ -45,9 +57,8 @@ Sidekiq.configure_server do |config|
ActiveRecord::Base.clear_all_connections!
end
- if Feature::FlipperFeature.table_exists? && Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
- # By default we're going to use Semi Reliable Fetch
- config.options[:semi_reliable_fetch] = Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
+ if enable_reliable_fetch?
+ config.options[:semi_reliable_fetch] = enable_semi_reliable_fetch_mode?
Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 7e1e89f3c10..dfcb5c4646e 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -110,7 +110,7 @@ module.exports = function(config) {
frameworks: ['jasmine'],
files: [
{ pattern: 'spec/javascripts/test_bundle.js', watched: false },
- { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.png)', included: false },
+ { pattern: `spec/javascripts/fixtures/**/*@(.json|.html|.png|.bmpr|.pdf)`, included: false },
],
preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
diff --git a/config/locales/en.yml b/config/locales/en.yml
index eb3b7771968..a3dceb2fb62 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -14,6 +14,8 @@ en:
token: "Auth Token"
project: "Project"
api_url: "Sentry API URL"
+ project/metrics_setting:
+ external_dashboard_url: "External dashboard URL"
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
diff --git a/config/routes.rb b/config/routes.rb
index bbf00208545..f5957f43655 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -103,6 +103,7 @@ Rails.application.routes.draw do
scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications
patch '/:application', to: 'clusters/applications#update', as: :update_applications
+ delete '/:application', to: 'clusters/applications#destroy', as: :uninstall_applications
end
get :cluster_status, format: :json
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 93d168fc595..61eb136f65b 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -218,6 +218,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :terminal
get :metrics
get :additional_metrics
+ get :metrics_dashboard
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy'
@@ -360,7 +361,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :toggle_subscription
post :mark_as_spam
post :move
- get :referenced_merge_requests
get :related_branches
get :can_create_branch
get :realtime_changes
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index e6820f49ee2..62e5526c02b 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -31,26 +31,54 @@ Please consider creating a merge request to
for them.
MARKDOWN
-def spin(team, project, category, branch_name)
+def spin_for_category(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
# Make traintainers have triple the chance to be picked as a reviewer
- reviewer = (reviewers + traintainers + traintainers).sample(random: rng)
- maintainer = maintainers.sample(random: rng)
+ reviewer = spin_for_person(reviewers + traintainers + traintainers, random: rng)
+ maintainer = spin_for_person(maintainers, random: rng)
"| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |"
end
+# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
+# selection will change on next spin
+def spin_for_person(people, random:)
+ person = nil
+ people = people.dup
+
+ people.size.times do
+ person = people.sample(random: random)
+
+ break person unless out_of_office?(person)
+
+ people -= [person]
+ end
+
+ person
+end
+
+def out_of_office?(person)
+ username = CGI.escape(person.username)
+ api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status"
+ response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty
+
+ if response.code == 200
+ response["message"]&.match(/OOO/i)
+ else
+ false # this is no worse than not checking for OOO
+ end
+rescue
+ false
+end
+
def build_list(items)
list = items.map { |filename| "* `#{filename}`" }.join("\n")
@@ -63,7 +91,7 @@ end
changes = helper.changes_by_category
-# Ignore any files that are known but uncategoried. Prompt for any unknown files
+# Ignore any files that are known but uncategorized. Prompt for any unknown files
changes.delete(:none)
categories = changes.keys - [:unknown]
@@ -92,7 +120,7 @@ if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_l
project = helper.project_name
unknown = changes.fetch(:unknown, [])
- rows = categories.map { |category| spin(team, project, category, canonical_branch_name) }
+ rows = categories.map { |category| spin_for_category(team, project, category, canonical_branch_name) }
markdown(MESSAGE)
markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
diff --git a/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
new file mode 100644
index 00000000000..e9cf2af84a5
--- /dev/null
+++ b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
@@ -0,0 +1,15 @@
+# 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 AddLetsEncryptNotificationEmailToApplicationSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :lets_encrypt_notification_email, :string
+ end
+end
diff --git a/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb b/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb
new file mode 100644
index 00000000000..16de63f207f
--- /dev/null
+++ b/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb
@@ -0,0 +1,21 @@
+# 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 AddLetsEncryptTermsOfServiceAcceptedToApplicationSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :lets_encrypt_terms_of_service_accepted, :boolean, default: false)
+ end
+
+ def down
+ remove_column :application_settings, :lets_encrypt_terms_of_service_accepted
+ end
+end
diff --git a/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb
new file mode 100644
index 00000000000..eb0e7c41f98
--- /dev/null
+++ b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddMergeTrainEnabledToCiCdSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :project_ci_cd_settings, :merge_trains_enabled
+ end
+end
diff --git a/db/migrate/20190422082247_create_project_metrics_settings.rb b/db/migrate/20190422082247_create_project_metrics_settings.rb
new file mode 100644
index 00000000000..3e21dd0a934
--- /dev/null
+++ b/db/migrate/20190422082247_create_project_metrics_settings.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateProjectMetricsSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_metrics_settings, id: :int, primary_key: :project_id, default: nil do |t|
+ t.string :external_dashboard_url, null: false
+ t.foreign_key :projects, column: :project_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20190426180107_add_deployment_events_to_services.rb b/db/migrate/20190426180107_add_deployment_events_to_services.rb
new file mode 100644
index 00000000000..1fb137fb5f9
--- /dev/null
+++ b/db/migrate/20190426180107_add_deployment_events_to_services.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddDeploymentEventsToServices < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :deployment_events, :boolean, default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:services, :deployment_events)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3a5d567ac57..2e77bbb51e5 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: 20190408163745) do
+ActiveRecord::Schema.define(version: 20190426180107) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -187,6 +187,8 @@ ActiveRecord::Schema.define(version: 20190408163745) do
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
+ t.string "lets_encrypt_notification_email"
+ t.boolean "lets_encrypt_terms_of_service_accepted", default: false, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -1610,6 +1612,7 @@ ActiveRecord::Schema.define(version: 20190408163745) do
t.integer "project_id", null: false
t.boolean "group_runners_enabled", default: true, null: false
t.boolean "merge_pipelines_enabled"
+ t.boolean "merge_trains_enabled", default: false, null: false
t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
end
@@ -1681,6 +1684,10 @@ ActiveRecord::Schema.define(version: 20190408163745) do
t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
end
+ create_table "project_metrics_settings", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t|
+ t.string "external_dashboard_url", null: false
+ end
+
create_table "project_mirror_data", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "status"
@@ -1995,6 +2002,7 @@ ActiveRecord::Schema.define(version: 20190408163745) do
t.boolean "commit_events", default: true, null: false
t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events", default: true
+ t.boolean "deployment_events", default: false, null: false
t.index ["project_id"], name: "index_services_on_project_id", using: :btree
t.index ["template"], name: "index_services_on_template", using: :btree
t.index ["type"], name: "index_services_on_type", using: :btree
@@ -2529,6 +2537,7 @@ ActiveRecord::Schema.define(version: 20190408163745) do
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
+ add_foreign_key "project_metrics_settings", "projects", on_delete: :cascade
add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
add_foreign_key "project_repositories", "projects", on_delete: :cascade
add_foreign_key "project_repositories", "shards", on_delete: :restrict
diff --git a/doc/README.md b/doc/README.md
index 14a1eeffda0..dd4909ce303 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -218,12 +218,12 @@ scales to run your tests faster.
The following documentation relates to the DevOps **Verify** stage:
-| Verify Topics | Description |
-|:---------------------------------------------------|:-----------------------------------------------------------------------------|
-| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
-| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
-| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. |
-| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |
+| Verify Topics | Description |
+|:---------------------------------------------------------|:-----------------------------------------------------------------------------|
+| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
+| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
+| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. |
+| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -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-environment-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. |
@@ -418,7 +418,7 @@ We have the following documentation to rapidly uplift your GitLab knowledge:
| Topic | Description |
|:-----------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------|
-| [GitLab Basics](gitlab-basics/README.md) | Start working on the command line and with GitLab. |
+| [GitLab basics guides](gitlab-basics/README.md) | Start working on the command line and with GitLab. |
| [GitLab Workflow](workflow/README.md) and [overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Enhance your workflow with the best of GitLab Workflow. |
| [Get started with GitLab CI/CD](ci/quick_start/README.md) | Quickly implement GitLab CI/CD. |
| [Auto DevOps](topics/autodevops/index.md) | Learn more about GitLab's Auto DevOps. |
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 05beb724d4d..48f599fa7e6 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -388,6 +388,9 @@ desired.
**Omnibus GitLab installations**
+> **Note:**
+`regionendpoint` is only required when configuring an S3 compatible service such as Minio, by entering a URL such as http://127.0.0.1:9000
+
To configure the storage driver in Omnibus:
1. Edit `/etc/gitlab/gitlab.rb`:
@@ -398,7 +401,8 @@ To configure the storage driver in Omnibus:
'accesskey' => 's3-access-key',
'secretkey' => 's3-secret-key-for-access-key',
'bucket' => 'your-s3-bucket',
- 'region' => 'your-s3-region'
+ 'region' => 'your-s3-region',
+ 'regionendpoint' => 'your-s3-regionendpoint'
}
}
```
@@ -421,6 +425,7 @@ storage:
secretkey: 'secret123'
bucket: 'gitlab-registry-bucket-AKIAKIAKI'
region: 'your-s3-region'
+ regionendpoint: 'your-s3-regionendpoint'
cache:
blobdescriptor: inmemory
delete:
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 28afaf84f5a..f318cb38ef0 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -1,6 +1,7 @@
-# Custom Git Hooks
+# Custom server-side Git hooks
-> **Note:** Custom Git hooks must be configured on the filesystem of the GitLab
+NOTE: **Note:**
+Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore [webhooks] and [CI] as an option if you do not
have filesystem access. For a user configurable Git hook interface, see
@@ -14,15 +15,14 @@ See [Git SCM Server-Side Hooks][hooks] for more information about each hook type
As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab
administrators can add custom git hooks to any GitLab project.
-## Setup
+## Create a custom Git hook for a repository
-Normally, Git hooks are placed in the repository or project's `hooks` directory.
-GitLab creates a symlink from each project's `hooks` directory to the
-gitlab-shell `hooks` directory for ease of maintenance between gitlab-shell
-upgrades. As such, custom hooks are implemented a little differently. Behavior
-is exactly the same once the hook is created, though.
-
-Follow the steps below to set up a custom hook:
+Server-side Git hooks are typically placed in the repository's `hooks`
+subdirectory. In GitLab, hook directories are are symlinked to the gitlab-shell
+`hooks` directory for ease of maintenance between gitlab-shell upgrades.
+Custom hooks are implemented differently, but the behavior is exactly the same
+once the hook is created. Follow the steps below to set up a custom hook for a
+repository:
1. Pick a project that needs a custom Git hook.
1. On the GitLab server, navigate to the project's repository directory.
@@ -42,33 +42,56 @@ Follow the steps below to set up a custom hook:
That's it! Assuming the hook code is properly implemented the hook will fire
as appropriate.
+## Set a global Git hook for all repositories
+
+To create a Git hook that applies to all of your repositories in
+your instance, set a global Git hook. Since all the repositories' `hooks`
+directories are symlinked to gitlab-shell's `hooks` directory, adding any hook
+to the gitlab-shell `hooks` directory will also apply it to all repositories. Follow
+the steps below to properly set up a custom hook all for repositories:
+
+1. On the GitLab server, navigate to the configured custom hook directory. The
+ default is in the gitlab-shell directory. The gitlab-shell `hook` directory
+ for an installation from source the path is usually
+ `/home/git/gitlab-shell/hooks`. For Omnibus installs the path is usually
+ `/opt/gitlab/embedded/service/gitlab-shell/hooks`.
+ To look in a different directory for the global custom hooks,
+ set `custom_hooks_dir` in the gitlab-shell config. For
+ Omnibus installations, this can be set in `gitlab.rb`; and in source
+ installations, this can be set in `gitlab-shell/config.yml`.
+1. Create a new directory in this location. Depending on your hook, it will be
+ either a `pre-receive.d`, `post-receive.d`, or `update.d` directory.
+1. Inside this new directory, add your hook. Hooks can be
+ in any language. Ensure the 'shebang' at the top properly reflects the language
+ type. For example, if the script is in Ruby the shebang will probably be
+ `#!/usr/bin/env ruby`.
+1. Make the hook file executable and make sure it's owned by Git.
+
+Now test the hook to see that it's functioning properly.
+
## Chained hooks support
> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
-Hooks can be also placed in `hooks/<hook_name>.d` (global) or
-`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+Hooks can be also global or be set per project directories and support a chained
execution of the hooks.
-NOTE: **Note:** `<hook_name>.d` would need to be either `pre-receive.d`,
+NOTE: **Note:**
+`<hook_name>.d` would need to be either `pre-receive.d`,
`post-receive.d`, or `update.d` to work properly. Any other names will be ignored.
-To look in a different directory for the global custom hooks (those in
-`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
-Omnibus installations, this can be set in `gitlab.rb`; and in source
-installations, this can be set in `gitlab-shell/config.yml`.
+NOTE: **Note:**
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
The hooks are searched and executed in this order:
1. `gitlab-shell/hooks` directory as known to Gitaly
-1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
+1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is symlinked to `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
-Files in `.d` directories need to be executable and not match the backup file
-pattern (`*~`).
-
The hooks of the same type are executed in order and execution stops on the
first script exiting with a non-zero value.
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 72341a5c777..d0e0e320019 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -39,7 +39,14 @@ options:
### Improving NFS performance with GitLab
-NOTE: **Note:** This is only available with GitLab 11.9 and up.
+NOTE: **Note:**
+This is only available starting in certain versions of GitLab:
+
+ * 11.5.11
+ * 11.6.11
+ * 11.7.12
+ * 11.8.8
+ * 11.9.0 and up (e.g. 11.10, 11.11, etc.)
If you are using NFS to share Git data, we recommend that you enable a
number of feature flags that will allow GitLab application processes to
diff --git a/doc/analytics/README.md b/doc/analytics/README.md
new file mode 100644
index 00000000000..6b63edb5174
--- /dev/null
+++ b/doc/analytics/README.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter)
diff --git a/doc/analytics/contribution_analytics.md b/doc/analytics/contribution_analytics.md
new file mode 100644
index 00000000000..38d71263bc1
--- /dev/null
+++ b/doc/analytics/contribution_analytics.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html).
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
index ec48bf4940b..cf02bbd9c92 100644
--- a/doc/api/graphql/index.md
+++ b/doc/api/graphql/index.md
@@ -29,7 +29,11 @@ curl --data "value=100" --header "PRIVATE-TOKEN: <your_access_token>" https://gi
## Available queries
-A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID.
+A first iteration of a GraphQL API includes the following queries
+
+1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID.
+
+1. `group` : Only basic group information is currently supported.
## GraphiQL
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index ed4b6281acc..7992af15448 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -93,6 +93,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -227,6 +235,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -351,6 +367,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -445,6 +469,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -629,6 +661,14 @@ Parameters:
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon",
"web_url" : "https://gitlab.example.com/root"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 4,
"target_project_id": 4,
"labels": [ ],
@@ -718,6 +758,7 @@ POST /projects/:id/merge_requests
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
+| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
@@ -843,6 +884,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
@@ -885,6 +927,14 @@ Must include at least one non-required attribute from above.
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1030,6 +1080,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1180,6 +1238,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1436,6 +1502,14 @@ Example response:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1557,6 +1631,14 @@ Example response:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1698,6 +1780,14 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
"web_url": "https://gitlab.example.com/barrett.krajcik"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 3,
"target_project_id": 3,
"labels": [],
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 3b76c19dc07..90d8c033cd6 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -92,7 +92,7 @@ Parameters:
- `description` (optional) - The description of a milestone
- `due_date` (optional) - The due date of the milestone
- `start_date` (optional) - The start date of the milestone
-- `state_event` (optional) - The state event of the milestone (close|activate)
+- `state_event` (optional) - The state event of the milestone (close or activate)
## Delete project milestone
diff --git a/doc/api/users.md b/doc/api/users.md
index 606003a75e2..d3e67d3d510 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -33,7 +33,7 @@ GET /users
]
```
-You can also search for users by email or username with: `/users?search=John`
+You can also search for users by name or primary email using `?search=`. For example. `/users?search=John`.
In addition, you can lookup users by username:
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 123a5e50f14..440a79c7782 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -5,71 +5,113 @@ description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Inte
# GitLab Continuous Integration (GitLab CI/CD)
-GitLab CI/CD is GitLab's built-in tool for software development using continuous methodology:
+GitLab CI/CD is a tool built into GitLab for software development
+through the [continuous methodologies](introduction/index.md#introduction-to-cicd-methodologies):
-- Continuous integration (CI).
-- Continuous delivery and deployment (CD).
-
-Within the [DevOps lifecycle](../README.md#the-entire-devops-lifecycle), GitLab CI/CD spans
-the [Verify (CI)](../README.md#verify) and [Release (CD)](../README.md#release) stages.
+- Continuous Integration (CI)
+- Continuous Delivery (CD)
+- Continuous Deployment (CD)
## Overview
-CI/CD is a vast area, so GitLab provides documentation for all levels of expertise. Consult the following table to find the right documentation for you:
-
-| Level of expertise | Resource |
-|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------|
-| New to the concepts of CI and CD | For a high-level overview, read an [introduction to CI/CD with GitLab](introduction/index.md). |
-| Familiar with GitLab CI/CD concepts | After getting familiar with GitLab CI/CD, let us walk you through a simple example in our [getting started guide](quick_start/README.md). |
-| A GitLab CI/CD expert | Jump straight to our [`.gitlab.yml`](yaml/README.md) reference. |
-
-Familiarity with GitLab Runner is also useful because it is responsible for running the jobs in your
-CI/CD pipeline. On GitLab.com, shared Runners are enabled by default so you won't need to set this up to get started.
-
-## CI/CD with Auto DevOps
-
-[Auto DevOps](../topics/autodevops/index.md) is the default minimum-configuration method for
-implementing CI/CD. Auto DevOps:
-
-- Provides simplified setup and execution of CI/CD.
-- Allows GitLab to automatically detect, build, test, deploy, and monitor your applications.
-
-## Manually configured CI/CD
-
-For complete control, you can manually configure GitLab CI/CD.
-
-### Configuration and Usage
-
-The following topics contain configuration and usage information for all features of GitLab CI/CD:
-
-| Topic | Description |
-|:--------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------|
-| [Creating and using CI/CD pipelines](pipelines.md) | Understand, visualize, create, and use CI/CD pipelines. |
-| [CI/CD Variables](variables/README.md) | Configuring and using environment variables in pipelines. |
-| [Where variables can be used](variables/where_variables_can_be_used.md) | Where and how CI/CD variables can be used. |
-| [User](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions | User access levels for performing certain CI actions. |
-| [Configuring GitLab Runners](runners/README.md) | Configuring [GitLab Runner](https://docs.gitlab.com/runner/). |
-| [Environments and deployments](environments.md) | Deploy the output of jobs into environments for reviewing, staging, and production. |
-| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
-| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Using the output of jobs. |
-| [Cache dependencies in GitLab CI/CD](caching/index.md) | Speed up pipelines using caching. |
-| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. |
-| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. |
-| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. |
-| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. |
-| [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. |
-| [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. |
-
-### GitLab Pages
-
-GitLab CI/CD can be used to build and host static websites. For more information, see the
-documentation on [GitLab Pages](../user/project/pages/index.md),
-or dive right into the [CI/CD step-by-step guide for Pages](../user/project/pages/getting_started_part_four.md).
-
-### Examples
+Continuous Integration works by pushing small code chunks to your
+application's code base hosted in a Git repository, and, to every
+push, run a pipeline of scripts to build, test, and validate the
+code changes before merging them into the main branch.
+
+Continuous Delivery and Deployment consist of a step further CI,
+deploying your application to production at every
+push to the default branch of the repository.
+
+These methodologies allow you to catch bugs and errors early in
+the development cycle, ensuring that all the code deployed to
+production complies with the code standards you established for
+your app.
+
+For a complete overview of these methodologies and GitLab CI/CD,
+read the [Introduction to CI/CD with GitLab](introduction/index.md).
+
+## Getting started
+
+GitLab CI/CD is configured by a file called `.gitlab-ci.yml` placed
+at the repository's root. The scripts set in this file are executed
+by the [GitLab Runner](https://docs.gitlab.com/runner/).
+
+To get started with GitLab CI/CD, we recommend you read through
+the following documents:
+
+- [How GitLab CI/CD works](introduction/index.md#how-gitlab-cicd-works).
+- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow).
+- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md).
+
+You can also get started by using one of the
+[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates)
+available through the UI. You can use them by creating a new file,
+choosing a template that suits your application, and adjusting it
+to your needs:
+
+![Use a .gitlab-ci.yml template](img/add_file_template_11_10.png)
+
+For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide.
+
+Once you're familiar with how GitLab CI/CD works, see the
+[`. gitlab-ci.yml` full reference](yaml/README.md)
+for all the attributes you can set and use.
+
+NOTE: **Note:**
+GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#extra-shared-runners-pipeline-minutes-quota).
+
+## GitLab CI/CD configuration
+
+GitLab CI/CD supports numerous configuration options:
+
+| Configuration | Description |
+|:--- |:--- |
+| [Pipelines](pipelines.md) | Structure your CI/CD process through pipelines. |
+| [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. |
+| [Environments](environments.md) | Deploy your application to different environments (e.g., staging, production). |
+| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. |
+| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
+| [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. |
+| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. |
+| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules. |
+| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. |
+| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. |
+| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. |
+| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. |
+| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. |
+| [`.gitlab-ci.yml` full reference](yaml/README.md) | All the attributes you can use with GitLab CI/CD. |
+
+Note that certain operations can only be performed according to the
+[user](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions.
+
+## GitLab CI/CD feature set
+
+You can also use the vast GitLab CI/CD feature set to easily configure
+it for specific purposes:
+
+| Feature | Description |
+|:--- |:--- |
+| [Auto Deploy](../topics/autodevops/index.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
+| [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. |
+| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
+| [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) **[PREMIUM]** | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
+| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
+| [CI services](services/README.md)| Link Docker containers with your base image. |
+| [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]**| Check your Docker containers for known vulnerabilities. |
+| [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]**| Analyze your dependencies for known vulnerabilities. |
+| [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. |
+| [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html) **[PREMIUM]** | Deploy your features behind Feature Flags. |
+| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. |
+| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
+| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
+| [Interactive Web Terminals](interactive_web_terminal/index.md) **[CORE ONLY]** | Open an interactive web terminal to debug the running jobs. |
+| [JUnit tests](junit_test_reports.md)| Identify script failures directly on merge requests. |
+| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
+| [Security Test reports](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate) **[ULTIMATE]** | Check for app vulnerabilities. |
+| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
+
+## GitLab CI/CD examples
GitLab provides examples of configuring GitLab CI/CD in the form of:
@@ -78,9 +120,10 @@ GitLab provides examples of configuring GitLab CI/CD in the form of:
- [`multi-project-pipelines`](https://gitlab.com/gitlab-examples/multi-project-pipelines) for examples of implementing multi-project pipelines.
- [`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps.
-### Administration
+## GitLab CI/CD administration **[CORE ONLY]**
-As a GitLab administrator, you can change the default behavior of GitLab CI/CD for:
+As a GitLab administrator, you can change the default behavior
+of GitLab CI/CD for:
- An [entire GitLab instance](../user/admin_area/settings/continuous_integration.md).
- Specific projects, using [pipelines settings](../user/project/pipelines/settings.md).
@@ -90,33 +133,22 @@ See also:
- [How to enable or disable GitLab CI/CD](enable_or_disable_ci.md).
- Other [CI administration settings](../administration/index.md#continuous-integration-settings).
-### Using Docker
-
-Docker is commonly used with GitLab CI/CD. Learn more about how to to accomplish this with the following
-documentation:
-
-| Topic | Description |
-|:-------------------------------------------------------------------------|:-------------------------------------------------------------------------|
-| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
-| [Building Docker images with GitLab CI/CD](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
-
-Related topics include:
-
-- [Docker integration](docker/README.md).
-- [CI services (linked Docker containers)](services/README.md).
+## References
-## Why GitLab CI/CD?
+### Why GitLab CI/CD?
-The following articles explain reasons to use GitLab CI/CD for your CI/CD infrastructure:
+The following articles explain reasons to use GitLab CI/CD
+for your CI/CD infrastructure:
- [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
- [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation.
-## Breaking changes
+### Breaking changes
-As GitLab CI/CD has evolved, certain breaking changes have been necessary. These are:
+As GitLab CI/CD has evolved, certain breaking changes have
+been necessary. These are:
- [CI variables renaming for GitLab 9.0](variables/deprecated_variables.md#gitlab-90-renamed-variables). Read about the
deprecated CI variables and what you should use for GitLab 9.0+.
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3e52cc786dd..6b4d4f1b9d4 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -16,11 +16,11 @@ For example:
1. Test your code.
1. Deploy your code into a testing or staging environment before you release it to the public.
-This helps prevent bugs not only in your software, but in the deployment process as well.
+This helps find bugs in your software, and also in the deployment process as well.
GitLab CI/CD is capable of not only testing or building your projects, but also
deploying them in your infrastructure, with the added benefit of giving you a
-way to track your deployments. In other words, you can always know what is
+way to track your deployments. In other words, you will always know what is
currently being deployed or has been deployed on your servers.
It's important to know that:
@@ -31,12 +31,12 @@ It's important to know that:
GitLab:
-- Provides a full history of your deployments per every environment.
+- Provides a full history of your deployments for each environment.
- Keeps track of your deployments, so you always know what is currently being deployed on your
servers.
If you have a deployment service such as [Kubernetes](../user/project/clusters/index.md)
-enabled for your project, you can use it to assist with your deployments, and
+associated with your project, you can use it to assist with your deployments, and
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
## Configuring environments
@@ -46,8 +46,8 @@ Configuring environments involves:
1. Understanding how [pipelines](pipelines.md) work.
1. Defining environments in your project's [`.gitlab-ci.yml`](yaml/README.md) file.
-The rest of this section illustrates how to configure environments and deployments using an example.
-It assumes you have already:
+The rest of this section illustrates how to configure environments and deployments using
+an example scenario. It assumes you have already:
- Created a [project](../gitlab-basics/create-project.md) in GitLab.
- Set up [a Runner](runners/README.md).
@@ -94,9 +94,8 @@ We have defined 3 [stages](yaml/README.md#stages):
- `build`
- `deploy`
-The jobs assigned to these stages will run in this order. If a job fails, then
-the jobs that are assigned to the next stage won't run, rendering the pipeline
-as failed.
+The jobs assigned to these stages will run in this order. If any job fails, then
+the pipeline fails and jobs that are assigned to the next stage won't run.
In our case:
@@ -104,15 +103,15 @@ In our case:
- Then the `build` job.
- Lastly the `deploy_staging` job.
-With this configuration, we ensure that:
+With this configuration, we:
-- The tests pass.
-- Our app is able to be built successfully.
+- Check that the tests pass.
+- Ensure that our app is able to be built successfully.
- Lastly we deploy to the staging server.
NOTE: **Note:**
The `environment` keyword is just a hint for GitLab that this job actually
-deploys to this environment's `name`. It can also have a `url` that is
+deploys to the `name` environment. It can also have a `url` that is
exposed in various places within GitLab. Each time a job that
has an environment specified succeeds, a deployment is recorded, storing
the Git SHA and environment name.
@@ -134,14 +133,13 @@ In summary, with the above `.gitlab-ci.yml` we have achieved the following:
> etc.
> Starting with GitLab 9.3, the environment URL is exposed to the Runner via
-> `$CI_ENVIRONMENT_URL`. The URL would be expanded from `.gitlab-ci.yml`, or if
-> the URL was not defined there, the external URL from the environment would be
-> used.
+> `$CI_ENVIRONMENT_URL`. The URL is expanded from `.gitlab-ci.yml`, or if
+> the URL was not defined there, the external URL from the environment is used.
### Configuring manual deployments
-Converting automatically executed job into jobs requiring to a manual action involves
-adding `when: manual` to the job's configuration.
+Adding `when: manual` to an automatically executed job's configuration converts it to
+a job requiring manual action.
To expand on the [previous example](#defining-environments), the following includes
another job that deploys our app to a production server and is
@@ -187,7 +185,7 @@ deploy_prod:
The `when: manual` action:
-- Exposes a "play" button in GitLab's UI.
+- Exposes a "play" button in GitLab's UI for that job.
- Means the `deploy_prod` job will only be triggered when the "play" button is clicked.
You can find the "play" button in the pipelines, environments, deployments, and jobs views.
@@ -200,8 +198,8 @@ You can find the "play" button in the pipelines, environments, deployments, and
| Deployments | ![Deployments manual action](img/environments_manual_action_deployments.png) |
| Jobs | ![Builds manual action](img/environments_manual_action_jobs.png) |
-Clicking on the play button in any view will trigger the `deploy_prod` job, and the deployment will be recorded under a new
-environment named `production`.
+Clicking on the play button in any view will trigger the `deploy_prod` job, and the
+deployment will be recorded as a new environment named `production`.
NOTE: **Note:**
If your environment's name is `production` (all lowercase),
@@ -209,14 +207,13 @@ it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
### Configuring dynamic environments
-Other environments are good for deploying to stable environments like staging or production.
+Regular environments are good when deploying to "stable" environments like staging or production.
-However, what about environments for branches other than `master`? Dynamic environments can be used to achieve these.
-
-Dynamic environments make it possible to create environments on the fly by
+However, for environments for branches other than `master`, dynamic environments
+can be used. Dynamic environments make it possible to create environments on the fly by
declaring their names dynamically in `.gitlab-ci.yml`.
-Dynamic environments form the basis of [Review apps](review_apps/index.md).
+Dynamic environments are a fundamental part of [Review apps](review_apps/index.md).
#### Allowed variables
@@ -237,10 +234,10 @@ For more information, see [Where variables can be used](variables/where_variable
#### Example configuration
-GitLab Runner exposes various [environment variables](variables/README.md) when a job runs and so
+GitLab Runner exposes various [environment variables](variables/README.md) when a job runs, so
you can use them as environment names.
-In the following example, a job will deploy to all branches except `master`:
+In the following example, the job will deploy to all branches except `master`:
```yaml
deploy_review:
@@ -261,28 +258,33 @@ In this example:
- The job's name is `deploy_review` and it runs on the `deploy` stage.
- We set the `environment` with the `environment:name` as `review/$CI_COMMIT_REF_NAME`.
Since the [environment name](yaml/README.md#environmentname) can contain slashes (`/`), we can
- use this pattern to distinguish between dynamic environments and the regular ones.
-- We tell the job to run [`only`](yaml/README.md#onlyexcept-basic) on branches [`except`](yaml/README.md#onlyexcept-basic) `master`.
+ use this pattern to distinguish between dynamic and regular environments.
+- We tell the job to run [`only`](yaml/README.md#onlyexcept-basic) on branches,
+ [`except`](yaml/README.md#onlyexcept-basic) `master`.
For the value of:
- `environment:name`, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`,
- which takes the value of the branch name.
-- `environment:url`, since `$CI_COMMIT_REF_NAME` itself may also contain `/`, or other characters that
- would be invalid in a domain name or URL, we use `$CI_ENVIRONMENT_SLUG` so that the environment can get a specific and distinct URL for each branch.
+ which receives the value of the branch name.
+- `environment:url`, we want a specific and distinct URL for each branch. `$CI_COMMIT_REF_NAME`
+ may contain a `/` or other characters that would be invalid in a domain name or URL,
+ so we use `$CI_ENVIRONMENT_SLUG` to get a "clean" or "safe" URL.
For example, given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
the web server to serve these requests is based on your setup.
- You could also use `$CI_COMMIT_REF_SLUG` in `environment:url`. For example, `https://$CI_COMMIT_REF_SLUG.example.com`.
- We have used `$CI_ENVIRONMENT_SLUG` here because it is guaranteed to be unique. If you're using a workflow like
- [GitLab Flow](../workflow/gitlab_flow.md), collisions are unlikely and you may prefer environment names to be more closely based on the branch name. The example
- above would give you an URL like `https://100-do-the-thing.example.com`.
+ We have used `$CI_ENVIRONMENT_SLUG` here because it is guaranteed to be unique. If
+ you're using a workflow like [GitLab Flow](../workflow/gitlab_flow.md), collisions
+ are unlikely and you may prefer environment names to be more closely based on the
+ branch name. In that case, you could use `$CI_COMMIT_REF_SLUG` in `environment:url` in
+ the example above: `https://$CI_COMMIT_REF_SLUG.example.com`, which would give a URL
+ of `https://100-do-the-thing.example.com`.
NOTE: **Note:**
-You are not bound to use the same prefix or only slashes in the dynamic
-environments' names (`/`). However, this will enable the [grouping similar environments](#grouping-similar-environments) feature.
+You are not required to use the same prefix or only slashes (`/`) in the dynamic environments'
+names. However, using this format will enable the [grouping similar environments](#grouping-similar-environments)
+feature.
### Complete example
@@ -292,7 +294,7 @@ The configuration in this section provides a full development workflow where you
- Built.
- Deployed as a Review App.
- Deployed to a staging server once the merge request is merged.
-- Finally, manually deployed to the production server.
+- Finally, able to be manually deployed to the production server.
The following combines the previous configuration examples, including:
@@ -348,8 +350,8 @@ deploy_prod:
- master
```
-A more realistic example would include copying files to a location where a
-webserver (for example, NGINX) could then read and serve.
+A more realistic example would also include copying files to a location where a
+webserver (for example, NGINX) could then acess and serve them.
The example below will copy the `public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
@@ -366,32 +368,33 @@ review_app:
This example requires that NGINX and GitLab Runner are set up on the server this job will run on.
NOTE: **Note:**
-See the [limitations](#limitations) section for some edge cases regarding naming of your branches and Review Apps.
+See the [limitations](#limitations) section for some edge cases regarding the naming of
+your branches and Review Apps.
-The complete example provides the following workflow for developers:
+The complete example provides the following workflow to developers:
- Create a branch locally.
-- Make changes and commit them
+- Make changes and commit them.
- Push the branch to GitLab.
- Create a merge request.
-Behind the scenes, GitLab runner will:
+Behind the scenes, GitLab Runner will:
- Pick up the changes and start running the jobs.
- Run the jobs sequentially as defined in `stages`:
- First, run the tests.
- If the tests succeed, build the app.
- - If the build succeeds, the app will be is deployed to an environment with a name specific to the
+ - If the build succeeds, the app is deployed to an environment with a name specific to the
branch.
So now, every branch:
- Gets its own environment.
-- Is deployed to its own location, with the added benefit of:
+- Is deployed to its own unique location, with the added benefit of:
- Having a [history of deployments](#viewing-deployment-history).
- Being able to [rollback changes](#retrying-and-rolling-back) if needed.
-For more information on using the URL, see [Using the environment URL](#using-the-environment-url).
+For more information, see [Using the environment URL](#using-the-environment-url).
### Protected environments
@@ -401,11 +404,12 @@ For more information, see [Protected environments](environments/protected_enviro
## Working with environments
-Having configured environments, GitLab provides many features to work with them. These are documented below.
+Once environments are configured, GitLab provides many features for working with them,
+as documented below.
### Viewing environments and deployments
-A list of environments and deployment statuses is available on project's **Operations > Environments** page.
+A list of environments and deployment statuses is available on each project's **Operations > Environments** page.
For example:
@@ -416,11 +420,11 @@ This example shows:
- The environment's name with a link to its deployments.
- The last deployment ID number and who performed it.
- The job ID of the last deployment with its respective job name.
-- The commit information of the last deployment such as who committed, to what
+- The commit information of the last deployment, such as who committed it, to what
branch, and the Git SHA of the commit.
- The exact time the last deployment was performed.
-- A button that takes you to the URL that you have defined under the
- `environment` keyword in `.gitlab-ci.yml`.
+- A button that takes you to the URL that you defined under the `environment` keyword
+ in `.gitlab-ci.yml`.
- A button that re-deploys the latest deployment, meaning it runs the job
defined by the environment name for that specific commit.
@@ -432,8 +436,8 @@ deployments, but an environment can have multiple deployments.
> - While you can create environments manually in the web interface, we recommend
> that you define your environments in `.gitlab-ci.yml` first. They will
> be automatically created for you after the first deploy.
-> - The environments page can only be viewed by Reporters and above. For more
-> information on the permissions, see the [permissions documentation](../user/permissions.md).
+> - The environments page can only be viewed by users with [Reporter permission](../user/permissions.md#project-members-permissions)
+> and above. For more information on permissions, see the [permissions documentation](../user/permissions.md).
> - Only deploys that happen after your `.gitlab-ci.yml` is properly configured
> will show up in the **Environment** and **Last deployment** lists.
@@ -442,7 +446,7 @@ deployments, but an environment can have multiple deployments.
GitLab keeps track of your deployments, so you:
- Always know what is currently being deployed on your servers.
-- Can have the full history of your deployments per every environment.
+- Can have the full history of your deployments for every environment.
Clicking on an environment shows the history of its deployments. Here's an example **Environments** page
with multiple deployments:
@@ -460,9 +464,9 @@ To retry or rollback a deployment:
1. Navigate to **Operations > Environments**.
1. Click on the environment.
-1. On the page that lists the deployment history for the environment, click the:
- - **Rollback** button against a previously successful deployment, to roll back to that deployment.
- - **Retry** button against the last deployment, to retry that deployment.
+1. In the deployment history list for the environment, click the:
+ - **Retry** button next to the last deployment, to retry that deployment.
+ - **Rollback** button next to a previously successful deployment, to roll back to that deployment.
NOTE: **Note:**
The defined deployment process in the job's `script` determines whether the rollback succeeds or not.
@@ -470,9 +474,7 @@ The defined deployment process in the job's `script` determines whether the roll
### Using the environment URL
The [environment URL](yaml/README.md#environmenturl) is exposed in a few
-places within GitLab.
-
-These are:
+places within GitLab:
- In a merge request widget as a link:
![Environment URL in merge request](img/environments_mr_review_app.png)
@@ -493,27 +495,28 @@ For example:
#### Going from source files to public pages
With GitLab's [Route Maps](review_apps/index.md#route-maps) you can go directly
-from source files to public pages on the environment set for Review Apps.
+from source files to public pages in the environment set for Review Apps.
### Stopping an environment
Stopping an environment:
-- Moves it from the list of **Available** environments to the list of **Stopped** environments on the [**Environments** page](#viewing-environments-and-deployments).
+- Moves it from the list of **Available** environments to the list of **Stopped**
+ environments on the [**Environments** page](#viewing-environments-and-deployments).
- Executes an [`on_stop` action](yaml/README.md#environmenton_stop), if defined.
This is often used when multiple developers are working on a project at the same time,
each of them pushing to their own branches, causing many dynamic environments to be created.
NOTE: **Note:**
-Starting with GitLab 8.14, dynamic environments will be stopped automatically
+Starting with GitLab 8.14, dynamic environments are stopped automatically
when their associated branch is deleted.
#### Automatically stopping an environment
Environments can be stopped automatically using special configuration.
-Consider the following example where the `deploy_review` calls the `stop_review`
+Consider the following example where the `deploy_review` job calls `stop_review`
to clean up and stop the environment:
```yaml
@@ -542,14 +545,14 @@ stop_review:
action: stop
```
-Setting the [`GIT_STRATEGY`](yaml/README.md#git-strategy) to `none` is necessary on the
-`stop_review` job so that the [GitLab Runner](https://docs.gitlab.com/runner/) won't try to check out the code
-after the branch is deleted.
+Setting the [`GIT_STRATEGY`](yaml/README.md#git-strategy) to `none` is necessary in the
+`stop_review` job so that the [GitLab Runner](https://docs.gitlab.com/runner/) won't
+try to check out the code after the branch is deleted.
When you have an environment that has a stop action defined (typically when
the environment describes a Review App), GitLab will automatically trigger a
stop action when the associated branch is deleted. The `stop_review` job must
-be in the same `stage` as the `deploy_review` one in order for the environment
+be in the same `stage` as the `deploy_review` job in order for the environment
to automatically stop.
You can read more in the [`.gitlab-ci.yml` reference](yaml/README.md#environmenton_stop).
@@ -562,8 +565,8 @@ As documented in [Configuring dynamic environments](#configuring-dynamic-environ
prepend environment name with a word, followed by a `/`, and finally the branch
name, which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
-In short, environments that are named like `type/foo` are presented under a
-group named `type`.
+In short, environments that are named like `type/foo` are all presented under the same
+group, named `type`.
In our [minimal example](#example-configuration), we named the environments `review/$CI_COMMIT_REF_NAME`
where `$CI_COMMIT_REF_NAME` is the branch name. Here is a snippet of the example:
@@ -588,13 +591,14 @@ exist, you should see something like:
>
> - For the monitoring dashboard to appear, you need to:
> - Enable the [Prometheus integration](../user/project/integrations/prometheus.md).
-> - Configure Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/index.md)
+> - Configure Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/index.md).
> - With GitLab 9.2, all deployments to an environment are shown directly on the monitoring dashboard.
-If you have enabled [Prometheus for monitoring system and response metrics](../user/project/integrations/prometheus.md), you can monitor the performance behavior of your app running in each environment.
+If you have enabled [Prometheus for monitoring system and response metrics](../user/project/integrations/prometheus.md),
+you can monitor the behavior of your app running in each environment.
-Once configured, GitLab will attempt to retrieve [supported performance metrics](../user/project/integrations/prometheus_library/index.md) for any
-environment that has had a successful deployment. If monitoring data was
+Once configured, GitLab will attempt to retrieve [supported performance metrics](../user/project/integrations/prometheus_library/index.md)
+for any environment that has had a successful deployment. If monitoring data was
successfully retrieved, a **Monitoring** button will appear for each environment.
![Environment Detail with Metrics](img/deployments_view.png)
@@ -604,8 +608,8 @@ Clicking on the **Monitoring** button will display a new page showing up to the
after initial deployment.
All deployments to an environment are shown directly on the monitoring dashboard,
-which allows easy correlation between any changes in performance and a new
-version of the app, all without leaving GitLab.
+which allows easy correlation between any changes in performance and new
+versions of the app, all without leaving GitLab.
![Monitoring dashboard](img/environments_monitoring.png)
@@ -617,8 +621,8 @@ If you deploy to your environments with the help of a deployment service (for ex
the [Kubernetes integration](../user/project/clusters/index.md)), GitLab can open
a terminal session to your environment.
-This is a powerful feature that allows you to debug issues without leaving the comfort of your web browser. To
-enable it, just follow the instructions given in the service integration
+This is a powerful feature that allows you to debug issues without leaving the comfort
+of your web browser. To enable it, just follow the instructions given in the service integration
documentation.
Once enabled, your environments will gain a "terminal" button:
@@ -663,8 +667,9 @@ 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](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-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](https://docs.gitlab.com/ee/ci/variables/README.md#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.
@@ -696,9 +701,8 @@ In this case, `review/feature-1` spec takes precedence over `review/*` and `*` s
## Limitations
-You are limited to use only the [CI predefined variables](variables/README.md) in the
-`environment: name`. If you try to re-use variables defined inside `script`
-as part of the environment name, it will not work.
+In the `environment: name`, you are limited to only the [predefined environment variables](variables/predefined_variables.md).
+Re-using variables defined inside `script` as part of the environment name will not work.
## Further reading
@@ -707,3 +711,4 @@ Below are some links you may find interesting:
- [The `.gitlab-ci.yml` definition of environments](yaml/README.md#environment)
- [A blog post on Deployments & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
- [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
+- [Deploy Boards for your applications running on Kubernetes](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]**
diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md
index 219af4ced9d..ab5c0e2dbad 100644
--- a/doc/ci/environments/protected_environments.md
+++ b/doc/ci/environments/protected_environments.md
@@ -9,8 +9,8 @@
- Some of them are just for testing.
- Others are for production.
-Because deploy jobs can be raised by different users with different roles, it is important that
-specific environments are "protected" to avoid unauthorized people affecting them.
+Since deploy jobs can be raised by different users with different roles, it is important that
+specific environments are "protected" to prevent unauthorized people from affecting them.
By default, a protected environment does one thing: it ensures that only people
with the right privileges can deploy to it, thus keeping it safe.
@@ -28,14 +28,14 @@ To protect an environment:
1. Navigate to your project's **Settings > CI/CD**.
1. Expand the **Protected Environments** section.
1. From the **Environment** dropdown menu, select the environment you want to protect.
-1. In the **Allowed to Deploy** dropdown menu, select the role, users, or groups you want to have deploy access.
- There are some considerations to have in mind:
- - There are two roles to choose from:
- - **Maintainers**: will allow access to all maintainers in the project.
- - **Developers**: will allow access to all maintainers and all developers in the project.
- - You can only select groups that are associated with the project.
- - Only users that have at least Developer permission level will appear on
- the **Allowed to Deploy** dropdown menu.
+1. In the **Allowed to Deploy** dropdown menu, select the role, users, or groups you
+ want to give deploy access to. Keep in mind that:
+ - There are two roles to choose from:
+ - **Maintainers**: will allow access to all maintainers in the project.
+ - **Developers**: will allow access to all maintainers and all developers in the project.
+ - You can only select groups that are already associated with the project.
+ - Only users that have at least Developer permission level will appear in
+ the **Allowed to Deploy** dropdown menu.
1. Click the **Protect** button.
The protected environment will now appear in the list of protected environments.
@@ -44,5 +44,6 @@ The protected environment will now appear in the list of protected environments.
Maintainers can:
-- Update existing protected environments at any time by changing the access on **Allowed to deploy** dropdown menu.
-- Unprotect a protected environment by clicking the **Unprotect** button of the environment to unprotect.
+- Update existing protected environments at any time by changing the access in the
+ **Allowed to Deploy** dropdown menu.
+- Unprotect a protected environment by clicking the **Unprotect** button for that environment.
diff --git a/doc/ci/img/add_file_template_11_10.png b/doc/ci/img/add_file_template_11_10.png
new file mode 100644
index 00000000000..ca04d72615b
--- /dev/null
+++ b/doc/ci/img/add_file_template_11_10.png
Binary files differ
diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png
index 45d882b536c..12090434bef 100644
--- a/doc/ci/img/deployments_view.png
+++ b/doc/ci/img/deployments_view.png
Binary files differ
diff --git a/doc/ci/img/environments_available.png b/doc/ci/img/environments_available.png
index 7ab92838ece..48fc6effc2d 100644
--- a/doc/ci/img/environments_available.png
+++ b/doc/ci/img/environments_available.png
Binary files differ
diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png
index 61b7e9fe77c..6a7b7ce5679 100644
--- a/doc/ci/img/environments_mr_review_app.png
+++ b/doc/ci/img/environments_mr_review_app.png
Binary files differ
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index d03c0b68daf..799217c9a08 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -71,11 +71,11 @@ merge request widget.
NOTE: **Note:**
If you also want the ability to browse JUnit output files, include the
-[`artifacts:paths`](yaml/README.md#artifactspaths) keyword.
+[`artifacts:paths`](yaml/README.md#artifactspaths) keyword. An example of this is shown in the Ruby example below.
### Ruby example
-Use the following job in `.gitlab-ci.yml`:
+Use the following job in `.gitlab-ci.yml`. This includes the `artifacts:paths` keyword to provide a link to the JUnit output file.
```yaml
## Use https://github.com/sj26/rspec_junit_formatter to generate a JUnit report with rspec
@@ -85,6 +85,8 @@ ruby:
- bundle install
- rspec spec/lib/ --format RspecJunitFormatter --out rspec.xml
artifacts:
+ paths:
+ - rspec.xml
reports:
junit: rspec.xml
```
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 2ffa3d4edc7..07129eb4186 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -266,6 +266,9 @@ Clicking on an individual job will show you its job trace, and allow you to:
- Retry the job.
- Erase the job trace.
+NOTE: **Note:**
+To prevent jobs from being bypassed or run out of order, canceled jobs can only be retried when the whole pipeline they belong to is retried.
+
### Seeing the failure reason for jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782) in GitLab 10.7.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 9983b015b31..6313ffc584d 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -143,14 +143,16 @@ This means that the value of the variable will be hidden in job logs,
though it must match certain requirements to do so:
- The value must be in a single line.
-- The value must not have escape characters.
+- The value must contain only letters, numbers, or underscores.
+- The value must not have escape characters, such as `\"`
- The value must not use variables.
- The value must not have any whitespace.
- The value must be at least 8 characters long.
-If the value does not meet the requirements above, then the CI variable will fail to save.
-In order to save, either alter the value to meet the masking requirements
-or disable `Masked` for the variable.
+The above rules are validated using the regex `/\A\w{8,}\z/`. If the value
+does not meet the requirements above, then the CI variable will fail to save.
+In order to save, either alter the value to meet the masking requirements or
+disable `Masked` for the variable.
### Syntax of environment variables in job scripts
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 40458137752..4e902c042e6 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -112,3 +112,7 @@ future GitLab releases.**
| `GITLAB_USER_NAME` | 10.0 | all | The real name of the user who started the job |
| `RESTORE_CACHE_ATTEMPTS` | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
| `GITLAB_FEATURES` | 10.6 | all | The comma separated list of licensed features available for your instance and plan |
+
+[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
+[registry]: ../../user/project/container_registry.md
+[dependent-repositories]: ../../user/project/new_ci_build_permissions_model.md#dependent-repositories
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 2e85e34f17b..03383d11c14 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2061,12 +2061,12 @@ from another project:
```yaml
include:
- template: Bash.gitlab-ci.yml
- - project: /group/my-project
+ - project: group/my-project
file: /templates/docker-workflow.yml
```
-The `/templates/docker-workflow.yml` present in `/group/my-project` includes two local files
-of the `/group/my-project`:
+The `/templates/docker-workflow.yml` present in `group/my-project` includes two local files
+of the `group/my-project`:
```yaml
include:
@@ -2074,14 +2074,14 @@ include:
- local: : /templates/docker-testing.yml
```
-Our `/templates/docker-build.yml` present in `/group/my-project` adds a `docker-build` job:
+Our `/templates/docker-build.yml` present in `group/my-project` adds a `docker-build` job:
```yaml
docker-build:
script: docker build -t my-image .
```
-Our second `/templates/docker-test.yml` present in `/group/my-project` adds a `docker-test` job:
+Our second `/templates/docker-test.yml` present in `group/my-project` adds a `docker-test` job:
```yaml
docker-test:
@@ -2479,9 +2479,9 @@ 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, your project might require the code in a
-specific directory (Go projects, for example). In that case, you can specify
+By default, GitLab Runner clones the repository in a unique subpath of the
+`$CI_BUILDS_DIR` directory. However, your project might require the code in a
+specific directory (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:
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 4e2213f7742..c4e5995714d 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -23,11 +23,6 @@ one of the [Merge request coaches][team].
If you need assistance with security scans or comments, feel free to include the
Security Team (`@gitlab-com/gl-security`) in the review.
-The `danger-review` CI job will randomly pick a reviewer and a maintainer for
-each area of the codebase that your merge request seems to touch. It only makes
-recommendations - feel free to override it if you think someone else is a better
-fit!
-
Depending on the areas your merge request touches, it must be **approved** by one
or more [maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer):
@@ -37,6 +32,26 @@ widget. Reviewers can add their approval by [approving additionally](https://doc
Getting your merge request **merged** also requires a maintainer. If it requires
more than one approval, the last maintainer to review and approve it will also merge it.
+### Reviewer roulette
+
+The `danger-review` CI job will randomly pick a reviewer and a maintainer for
+each area of the codebase that your merge request seems to touch. It only makes
+recommendations - feel free to override it if you think someone else is a better
+fit!
+
+It picks reviewers and maintainers from the list at the
+[engineering projects](https://about.gitlab.com/handbook/engineering/projects/)
+page, with these behaviours:
+
+1. It will not pick people whose [GitLab status](../user/profile/#current-status)
+ contains the string 'OOO'.
+2. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
+ are three times as likely to be picked as other reviewers.
+3. It always picks the same reviewers and maintainers for the same
+ branch name (unless their OOO status changes, as in point 1). It
+ removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so
+ that it can be stable for backport branches.
+
### Approval guidelines
As described in the section on the responsibility of the maintainer below, you
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index fbca99fbfea..9d8d5afedad 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -362,16 +362,23 @@ For other punctuation rules, please refer to the
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](LINK)`.
-### Links to confidential issues
+### Links requiring permissions
-Don't link directly to [confidential issues](../../user/project/issues/confidential_issues.md). These will fail for:
+Don't link directly to:
+
+- [Confidential issues](../../user/project/issues/confidential_issues.md).
+- Project features that require [special permissions](../../user/permissions.md) to view.
+
+These will fail for:
- Those without sufficient permissions.
- Automated link checkers.
Instead:
-- Mention in the text that the information is contained in a confidential issue. This will reduce confusion.
+- To reduce confusion, mention in the text that the information is either:
+ - Contained in a confidential issue.
+ - Requires special permission to a project to view.
- Provide a link in back ticks (`` ` ``) so that those with access to the issue can easily navigate to it.
Example:
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 223585ebb55..38d56322de8 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -300,7 +300,7 @@ file in. Once the changes are on master, they will be picked up by
[Crowdin](http://translate.gitlab.com) and be presented for translation.
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
-and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
+and regenerate it using the same command.
### Validating PO files
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
index 51fe19c3d9e..fc7aaedca29 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -99,13 +99,13 @@ subgraph gitlab-qa pipeline
1. When packages are ready, and available in the registry, a final step in the
[Omnibus GitLab][omnibus-gitlab] pipeline, triggers a new
- [GitLab QA pipeline][gitlab-qa-pipelines]. It also waits for a resulting status.
+ GitLab QA pipeline (those with access can view them at `https://gitlab.com/gitlab-org/gitlab-qa/pipelines`). It also waits for a resulting status.
1. GitLab QA pulls images from the registry, spins-up containers and runs tests
against a test environment that has been just orchestrated by the `gitlab-qa`
tool.
-1. The result of the [GitLab QA pipeline][gitlab-qa-pipelines] is being
+1. The result of the GitLab QA pipeline is being
propagated upstream, through Omnibus, back to the CE / EE merge request.
#### Using the `review-qa-all` jobs
@@ -146,7 +146,6 @@ you can find an issue you would like to work on in
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
-[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
[quality-nightly-pipelines]: https://gitlab.com/gitlab-org/quality/nightly/pipelines
[quality-staging-pipelines]: https://gitlab.com/gitlab-org/quality/staging/pipelines
diff --git a/doc/development/testing_guide/img/review_apps_cicd_architecture.png b/doc/development/testing_guide/img/review_apps_cicd_architecture.png
index 87e472076f3..1ee28d3db91 100644
--- a/doc/development/testing_guide/img/review_apps_cicd_architecture.png
+++ b/doc/development/testing_guide/img/review_apps_cicd_architecture.png
Binary files differ
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 55ca502f84a..7ad33f05f77 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -6,7 +6,7 @@ Review Apps are automatically deployed by each pipeline, both in
## How does it work?
-### CD/CD architecture diagram
+### CI/CD architecture diagram
![Review Apps CI/CD architecture](img/review_apps_cicd_architecture.png)
@@ -14,23 +14,29 @@ Review Apps are automatically deployed by each pipeline, both in
<summary>Show mermaid source</summary>
<pre>
graph TD
- B1 -.->|2. once gitlab:assets:compile is done,<br />triggers a CNG-mirror pipeline and wait for it to be done| A2
- C1 -.->|2. once review-build-cng is done,<br />Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline| A3
-
-subgraph gitlab-ce/ee `test` stage
- A1[gitlab:assets:compile]
- B1[review-build-cng] -->|1. wait for| A1
- C1[review-deploy] -->|1. wait for| B1
- D1[review-qa-smoke] -->|1. wait for| C1
- D1[review-qa-smoke] -.->|2. once review-deploy is done| E1>gitlab-qa runs the smoke<br/>suite against the Review App]
+ build-qa-image -.->|once the `prepare` stage is done| gitlab:assets:compile
+ review-build-cng -->|triggers a CNG-mirror pipeline and wait for it to be done| CNG-mirror
+ review-build-cng -.->|once the `test` stage is done| review-deploy
+ review-deploy -.->|once the `review` stage is done| review-qa-smoke
+
+subgraph 1. gitlab-ce/ee `prepare` stage
+ build-qa-image
end
-subgraph CNG-mirror pipeline
- A2>Cloud Native images are built];
+subgraph 2. gitlab-ce/ee `test` stage
+ gitlab:assets:compile -->|plays dependent job once done| review-build-cng
end
-subgraph GCP `gitlab-review-apps` project
- A3>"Cloud Native images are deployed to the<br />`review-apps-ce` or `review-apps-ee` Kubernetes (GKE) cluster"];
+subgraph 3. gitlab-ce/ee `review` stage
+ review-deploy["review-deploy<br /><br />Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br /><br />Cloud Native images are deployed to the `review-apps-ce` or `review-apps-ee`<br />Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
+ end
+
+subgraph 4. gitlab-ce/ee `qa` stage
+ review-qa-smoke[review-qa-smoke<br /><br />gitlab-qa runs the smoke suite against the Review App.]
+ end
+
+subgraph CNG-mirror pipeline
+ CNG-mirror>Cloud Native images are built];
end
</pre>
</details>
@@ -38,40 +44,37 @@ subgraph GCP `gitlab-review-apps` project
### Detailed explanation
1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
- [`review-build-cng`][review-build-cng] and
- [`review-deploy`][review-deploy] jobs are automatically started.
- - The [`review-deploy`][review-deploy] job waits for the
- [`review-build-cng`][review-build-cng] job to finish.
- - The [`review-build-cng`][review-build-cng] job waits for the
- [`gitlab:assets:compile`][gitlab:assets:compile] job to finish since the
- [`CNG-mirror`][cng-mirror] pipeline triggered in the following step depends on it.
-1. Once the [`gitlab:assets:compile`][gitlab:assets:compile] job is done,
- [`review-build-cng`][review-build-cng] [triggers a pipeline][cng-pipeline]
- in the [`CNG-mirror`][cng-mirror] project.
- - The [`CNG-mirror`][cng-pipeline] pipeline creates the Docker images of
- each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.)
- based on the commit from the [GitLab pipeline][gitlab-pipeline] and store
- them in its [registry][cng-mirror-registry].
- - We use the [`CNG-mirror`][cng-mirror] project so that the `CNG`, (**C**loud
- **N**ative **G**itLab), project's registry is not overloaded with a
- lot of transient Docker images.
-1. Once the [`review-build-cng`][review-build-cng] job is done, the
- [`review-deploy`][review-deploy] job deploys the Review App using
- [the official GitLab Helm chart][helm-chart] to the
- [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee]
- Kubernetes cluster on GCP.
- - The actual scripts used to deploy the Review App can be found at
- [`scripts/review_apps/review-apps.sh`][review-apps.sh].
- - These scripts are basically
- [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
- default CNG images are overridden with the images built and stored in the
- [`CNG-mirror` project's registry][cng-mirror-registry].
- - Since we're using [the official GitLab Helm chart][helm-chart], this means
- you get a dedicated environment for your branch that's very close to what
- it would look in production.
+ [`gitlab:assets:compile`][gitlab:assets:compile] job is automatically started.
+ - Once it's done, it starts the [`review-build-cng`][review-build-cng]
+ manual job since the [`CNG-mirror`][cng-mirror] pipeline triggered in the
+ following step depends on it.
+1. The [`review-build-cng`][review-build-cng] job [triggers a pipeline][cng-mirror-pipeline]
+ in the [`CNG-mirror`][cng-mirror] project.
+ - The [`CNG-mirror`][cng-mirror-pipeline] pipeline creates the Docker images of
+ each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.)
+ based on the commit from the [GitLab pipeline][gitlab-pipeline] and stores
+ them in its [registry][cng-mirror-registry].
+ - We use the [`CNG-mirror`][cng-mirror] project so that the `CNG`, (**C**loud
+ **N**ative **G**itLab), project's registry is not overloaded with a
+ lot of transient Docker images.
+ - Note that the official CNG images are built by the `cloud-native-image`
+ job, which runs only for tags, and triggers itself a [`CNG`][cng] pipeline.
+1. Once the `test` stage is done, the [`review-deploy`][review-deploy] job
+ deploys the Review App using [the official GitLab Helm chart][helm-chart] to
+ the [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee]
+ Kubernetes cluster on GCP.
+ - The actual scripts used to deploy the Review App can be found at
+ [`scripts/review_apps/review-apps.sh`][review-apps.sh].
+ - These scripts are basically
+ [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
+ default CNG images are overridden with the images built and stored in the
+ [`CNG-mirror` project's registry][cng-mirror-registry].
+ - Since we're using [the official GitLab Helm chart][helm-chart], this means
+ you get a dedicated environment for your branch that's very close to what
+ it would look in production.
1. Once the [`review-deploy`][review-deploy] job succeeds, you should be able to
- use your Review App thanks to the direct link to it from the MR widget. To log
- into the Review App, see "Log into my Review App?" below.
+ use your Review App thanks to the direct link to it from the MR widget. To log
+ into the Review App, see "Log into my Review App?" below.
**Additional notes:**
@@ -82,71 +85,69 @@ subgraph GCP `gitlab-review-apps` project
- If the Review App deployment fails, you can simply retry it (there's no need
to run the [`review-stop`][gitlab-ci-yml] job first).
- The manual [`review-stop`][gitlab-ci-yml] in the `test` stage can be used to
- stop a Review App manually, and is also started by GitLab once a branch is
- deleted.
-- Review Apps are cleaned up regularly using a pipeline schedule that runs
+ stop a Review App manually, and is also started by GitLab once a merge
+ request's branch is deleted after being merged.
+- Review Apps are cleaned up regularly via a pipeline schedule that runs
the [`schedule:review-cleanup`][gitlab-ci-yml] job.
## QA runs
-On every [pipeline][gitlab-pipeline] during the `test` stage, the
-`review-qa-smoke` job is automatically started: it runs the QA smoke suite.
-You can also manually start the `review-qa-all`: it runs the QA full suite.
+On every [pipeline][gitlab-pipeline] in the `qa` stage (which comes after the
+`review` stage), the `review-qa-smoke` job is automatically started and it runs
+the QA smoke suite.
-Note that both jobs first wait for the `review-deploy` job to be finished.
+You can also manually start the `review-qa-all`: it runs the full QA suite.
## Performance Metrics
-On every [pipeline][gitlab-pipeline] during the `test` stage, the
+On every [pipeline][gitlab-pipeline] in the `qa` stage, the
`review-performance` job is automatically started: this job does basic
-browser performance testing using [Sitespeed.io Container](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html) .
+browser performance testing using a
+[Sitespeed.io Container](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
-This job waits for the `review-deploy` job to be finished.
+## How to:
-## How to?
-
-### Log into my Review App?
+### Log into my Review App
The default username is `root` and its password can be found in the 1Password
secure note named **gitlab-{ce,ee} Review App's root password**.
-### Enable a feature flag for my Review App?
+### Enable a feature flag for my Review App
1. Open your Review App and log in as documented above.
1. Create a personal access token.
1. Enable the feature flag using the [Feature flag API](../../api/features.md).
-### Find my Review App slug?
+### Find my Review App slug
1. Open the `review-deploy` job.
1. Look for `Checking for previous deployment of review-*`.
1. For instance for `Checking for previous deployment of review-qa-raise-e-12chm0`,
- your Review App slug would be `review-qa-raise-e-12chm0` in this case.
+ your Review App slug would be `review-qa-raise-e-12chm0` in this case.
-### Run a Rails console?
+### Run a Rails console
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps)
- , e.g. `review-29951-issu-id2qax`.
-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`.
+ , e.g. `review-qa-raise-e-12chm0`.
+1. Find and open the `task-runner` Deployment, e.g. `review-qa-raise-e-12chm0-task-runner`.
+1. Click on the Pod in the "Managed pods" section, e.g. `review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
- default command or
- - 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
- - Replace `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`
- with your Pod's name.
+ default command or
+ - Run `kubectl exec --namespace review-apps-ce review-qa-raise-e-12chm0-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
+ - Replace `review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz`
+ with your Pod's name.
-### Dig into a Pod's logs?
+### Dig into a Pod's logs
-1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps)
- , e.g. `review-1979-1-mul-dnvlhv`.
+1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps),
+ e.g. `review-qa-raise-e-12chm0`.
1. Find and open the `migrations` Deployment, e.g.
- `review-1979-1-mul-dnvlhv-migrations.1`.
+ `review-qa-raise-e-12chm0-migrations.1`.
1. Click on the Pod in the "Managed pods" section, e.g.
- `review-1979-1-mul-dnvlhv-migrations.1-nqwtx`.
+ `review-qa-raise-e-12chm0-migrations.1-nqwtx`.
1. Click on the `Container logs` link.
## Frequently Asked Questions
@@ -182,7 +183,8 @@ find a way to limit it to only us.**
[review-build-cng]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511623
[review-deploy]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511624
[cng-mirror]: https://gitlab.com/gitlab-org/build/CNG-mirror
-[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657
+[cng]: https://gitlab.com/gitlab-org/build/CNG
+[cng-mirror-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657
[cng-mirror-registry]: https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry
[helm-chart]: https://gitlab.com/charts/gitlab/
[review-apps-ce]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 4e15f7cfd49..aa008d6f768 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,18 +2,34 @@
comments: false
---
-# GitLab basics
-
-Step-by-step guides on the basics of working with Git and GitLab.
-
-- [Command line basics](command-line-commands.md)
-- [Start using Git on the command line](start-using-git.md)
-- [Create and add your SSH Keys](create-your-ssh-keys.md)
-- [Create a project](create-project.md)
-- [Create a group](../user/group/index.md#create-a-new-group)
-- [Create a branch](create-branch.md)
-- [Fork a project](fork-project.md)
-- [Add a file](add-file.md)
-- [Add an image](add-image.md)
-- [Create an issue](../user/project/issues/create_new_issue.md)
-- [Create a merge request](add-merge-request.md)
+# GitLab basics guides
+
+This section provides resources to help you start with GitLab by focusing on basic functionality.
+
+This documentation is split into the following groups:
+
+- [GitLab-specific functionality](#gitlab-basics), for basic GitLab features.
+- [General Git functionality](#git-basics), for working with Git in conjunction with GitLab.
+
+## GitLab basics
+
+The following are guides to basic GitLab functionality:
+
+- [Create and add your SSH Keys](create-your-ssh-keys.md), for enabling Git over SSH.
+- [Create a project](create-project.md), to start using GitLab.
+- [Create a group](../user/group/index.md#create-a-new-group), to combine and administer projects together.
+- [Create a branch](create-branch.md), to make changes to files stored in a project's repository.
+- [Fork a project](fork-project.md), to duplicate projects so they can be worked on in parallel.
+- [Add a file](add-file.md), to add new files to a project's repository.
+- [Add an image](add-image.md), to add new images to a project's repository.
+- [Create an issue](../user/project/issues/create_new_issue.md), to start collaborating within a project.
+- [Create a merge request](add-merge-request.md), to request changes made in a branch be merged into a project's repository.
+
+## Git basics
+
+If you're unfamiliar with the command line, these resources will help:
+
+- [Command line basics](command-line-commands.md), for those unfamiliar with the command line interface.
+- [Start using Git on the command line](start-using-git.md), for some simple Git commands.
+
+More Git resources are available at GitLab's [Git documentation](../topics/git/index.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 13efe68ef4e..60a8ffacd76 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -311,7 +311,7 @@ the guide here.
sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig
# Disable Redis listening on TCP by setting 'port' to 0
-sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
+sudo sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
# Enable Redis socket for default Debian / Ubuntu path
echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
@@ -320,9 +320,9 @@ echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf
# Create the directory which contains the socket
-mkdir /var/run/redis
-chown redis:redis /var/run/redis
-chmod 755 /var/run/redis
+sudo mkdir -p /var/run/redis
+sudo chown redis:redis /var/run/redis
+sudo chmod 755 /var/run/redis
# Persist the directory which contains the socket, if applicable
if [ -d /etc/tmpfiles.d ]; then
@@ -384,7 +384,7 @@ sudo chmod -R u+rwX tmp/pids/
sudo chmod -R u+rwX tmp/sockets/
# Create the public/uploads/ directory
-sudo -u git -H mkdir public/uploads/
+sudo -u git -H mkdir -p public/uploads/
# Make sure only the GitLab user has access to the public/uploads/ directory
# now that files in public/uploads are served by gitlab-workhorse
@@ -540,6 +540,7 @@ sudo -u git -H make
```sh
# Fetch Gitaly source with Git and compile with Go
+cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production
```
@@ -567,6 +568,7 @@ For more information about configuring Gitaly see
### Initialize Database and Activate Advanced Features
```sh
+cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
diff --git a/doc/integration/github.md b/doc/integration/github.md
index bee68688ace..e145afbdd5e 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -19,7 +19,7 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
1. Provide the required details.
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- - Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com`
+ - Homepage URL: The URL of your GitLab installation. For example, `https://gitlab.example.com`.
- Application description: Fill this in if you wish.
- Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth/github/callback`. Please make sure the port is included if your GitLab instance is not configured on default port.
![Register OAuth App](img/github_register_app.png)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 1c559b5263c..69834f7ae47 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -770,6 +770,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. |
+| `ROLLOUT_RESOURCE_TYPE` | From GitLab 11.9, this variable allows specification of the resource type being deployed when using a custom helm chart. Default value is `deployment`. |
| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. |
TIP: **Tip:**
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
index d1729d70158..71651fcf421 100644
--- a/doc/topics/git/troubleshooting_git.md
+++ b/doc/topics/git/troubleshooting_git.md
@@ -78,6 +78,33 @@ git push
In case you're running an older version of Git (< 2.9), consider upgrading
to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]).
+## `ssh_exchange_identification` error
+
+Users may experience the following error when attempting to push or pull
+using Git over SSH:
+
+```
+Please make sure you have the correct access rights
+and the repository exists.
+...
+ssh_exchange_identification: read: Connection reset by peer
+fatal: Could not read from remote repository.
+```
+
+This error usually indicates that SSH daemon's `MaxStartups` value is throttling
+SSH connections. This setting specifies the maximum number of unauthenticated
+connections to the SSH daemon. This affects users with proper authentication
+credentials (SSH keys) because every connection is 'unauthenticated' in the
+beginning. The default value is `10`.
+
+Increase `MaxStartups` by adding or modifying the value in `/etc/ssh/sshd_config`:
+
+```
+MaxStartups 100
+```
+
+Restart SSHD for the change to take effect.
+
## Timeout during git push/pull
If pulling/pushing from/to your repository ends up taking more than 50 seconds,
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 00cea22e4e1..f924ff8dfde 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -16,7 +16,7 @@ The Admin Area is made up of the following sections:
| Section | Description |
|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Overview | View your GitLab Dashboard, and maintain projects, users, groups, jobs, runners, and Gitaly servers. |
+| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), users, groups, jobs, runners, and Gitaly servers. |
| Monitoring | View GitLab system information, and information on background jobs, logs, [health checks](monitoring/health_check.md), request profiles, and audit logs. |
| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
| System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. |
@@ -27,3 +27,47 @@ The Admin Area is made up of the following sections:
| Labels | Create and maintain [labels](labels.md) for your GitLab instance. |
| Appearance | Customize [GitLab's appearance](../../customization/index.md). |
| Settings | Modify the [settings](settings/index.md) for your GitLab instance. |
+
+## Admin Dashboard
+
+The Dashboard provides statistics and system information about the GitLab instance.
+
+To access the Dashboard, either:
+
+- Click the Admin Area icon (the wrench icon).
+- Visit `/admin` on your self-managed instance.
+
+The Dashboard is the default view of the Admin Area, and is made up of the following sections:
+
+| Section | Description |
+|------------|---------------|
+| Projects | The total number of projects, up to 10 of the latest projects, and the option of creating a new project. |
+| Users | The total number of users, up to 10 of the latest users, and the option of creating a new user. |
+| Groups | The total number of groups, up to 10 of the latest groups, and the option of creating a new group. |
+| Statistics | Totals of all elements of the GitLab instance. |
+| Features | All features available on the GitLab instance. Enabled features are marked with a green circle icon, and disabled features are marked with a power icon. |
+| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. |
+
+## Administer Projects
+
+You can administer all projects in the GitLab instance from the Admin Area's Projects page.
+
+To access the Projects page, go to **Admin Area > Overview > Projects**.
+
+Click the **All**, **Private**, **Internal**, or **Public** tab to list only projects of that
+criteria.
+
+By default, all projects are listed, in reverse order of when they were last updated. For each
+project, the name, namespace, description, and size is listed, also options to **Edit** or
+**Delete** it.
+
+Sort projects by **Name**, **Last created**, **Oldest created**, **Last updated**, **Oldest
+updated**, **Owner**, and choose to hide or show archived projects.
+
+In the **Filter by name** field, type the project name you want to find, and GitLab will filter
+them as you type.
+
+Select from the **Namespace** dropdown to filter only projects in that namespace.
+
+You can combine the filter options. For example, click the **Public** tab, and enter `score` in
+the **Filter by name...** input box to list only public projects with `score` in their name. \ No newline at end of file
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 6054ab97dc1..2d887673fd6 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -68,7 +68,7 @@ together in a single list view.
You can create a group in GitLab from:
-1. The Groups page: expand the left menu, click **Groups**, and click the green button **New group**:
+1. The Groups page: from the top menu, click **Groups**, and click the green button **New group**:
![new group from groups page](img/new_group_from_groups.png)
@@ -80,14 +80,21 @@ Add the following information:
![new group info](img/create_new_group_info.png)
-1. Set the **Group path** which will be the **namespace** under which your projects
- will be hosted (path can contain only letters, digits, underscores, dashes
- and dots; it cannot start with dashes or end in dot).
-1. The **Group name** will populate with the path. Optionally, you can change
- it. This is the name that will display in the group views.
-1. Optionally, you can add a description so that others can briefly understand
+1. The **Group name** will populate the URL automatically. Optionally, you can change it.
+ This is the name that is displayed in the group views.
+ The name can contain only:
+ - Alphanumeric characters.
+ - Underscores.
+ - Dashes and dots.
+ - Spaces.
+1. The **Group URL**, which will be the namespace under which your projects will be hosted.
+ The URL can contain only:
+ - Alphanumeric characters.
+ - Underscores.
+ - Dashes and dots. It cannot start with dashes or end in dot.
+1. Optionally, you can add a brief description to tell others
what this group is about.
-1. Optionally, choose an avatar for your project.
+1. Optionally, choose an avatar for your group.
1. Choose the [visibility level](../../public_access/public_access.md).
## Add users to a group
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 0db95e5a64c..0677fe622f2 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -257,11 +257,11 @@ GitLab will create the necessary service accounts and privileges in order to ins
NOTE: **Note:**
Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5.
-- When you install Helm Tiller into your cluster, the `tiller` service account
+- When you install Helm into your cluster, the `tiller` service account
will be created with `cluster-admin` privileges in the `gitlab-managed-apps`
namespace. This service account will be added to the installed Helm Tiller and will
be used by Helm to install and run [GitLab managed applications](#installing-applications).
- Helm Tiller will also create additional service accounts and other resources for each
+ Helm will also create additional service accounts and other resources for each
installed application. Consult the documentation of the Helm charts for each application
for details.
@@ -315,25 +315,29 @@ install it manually.
## Installing applications
GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
-be added directly to your configured cluster. Those applications are
+be added directly to your configured cluster. These applications are
needed for [Review Apps](../../../ci/review_apps/index.md) and
-[deployments](../../../ci/environments.md). You can install them after you
+[deployments](../../../ci/environments.md) when using [Auto DevOps](../../../topics/autodevops/index.md).
+You can install them after you
[create a cluster](#adding-and-creating-a-new-gke-cluster-via-gitlab).
+Applications managed by GitLab will be installed onto the `gitlab-managed-apps` namespace. This differrent
+from the namespace used for project deployments. It is only created once and its name is not configurable.
+
To see a list of available applications to install:
1. Navigate to your project's **Operations > Kubernetes**.
1. Select your cluster.
-Install Helm Tiller first because it's used to install other applications.
+Install Helm first as it's used to install other applications.
NOTE: **Note:**
-As of GitLab 11.6, Helm Tiller will be upgraded to the latest version supported
+As of GitLab 11.6, Helm will be upgraded to the latest version supported
by GitLab before installing any of the applications.
| Application | GitLab version | Description | Helm Chart |
| ----------- | :------------: | ----------- | --------------- |
-| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
+| [Helm](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
| [Cert-Manager](https://docs.cert-manager.io/en/latest/) | 11.6+ | Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
@@ -345,9 +349,9 @@ With the exception of Knative, the applications will be installed in a dedicated
namespace called `gitlab-managed-apps`.
CAUTION: **Caution:**
-If you have an existing Kubernetes cluster with Tiller already installed,
+If you have an existing Kubernetes cluster with Helm already installed,
you should be careful as GitLab cannot detect it. In this case, installing
-Tiller via the applications will result in the cluster having it twice, which
+Helm via the applications will result in the cluster having it twice, which
can lead to confusion during deployments.
### Upgrading applications
@@ -384,7 +388,7 @@ 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.
+- Ensure certificates are not out of sync. When installing applications, GitLab expects a new cluster with no previous installation of Helm.
You can confirm that the certificates match via `kubectl`:
diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md
index 6fc083170b6..23f1ce7a15a 100644
--- a/doc/user/project/integrations/custom_issue_tracker.md
+++ b/doc/user/project/integrations/custom_issue_tracker.md
@@ -7,9 +7,9 @@ in the table below.
| Field | Description |
| ----- | ----------- |
-| `title` | A title for the issue tracker (to differentiate between instances, for example) |
+| `title` | A title for the issue tracker (to differentiate between instances, for example). |
| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | Currently unused. Will be changed in a future release. |
+| `project_url` | The URL to the project in the custom issue tracker. |
| `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. |
| `new_issue_url` | Currently unused. Will be changed in a future release. |
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index ad47b848bea..31020de5208 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -151,7 +151,7 @@ Create lists for each of your team members and quickly drag-and-drop issues onto
## Permissions
-[Developers and up](../permissions.md) can use all the functionality of the
+[Reporters and up](../permissions.md) can use all the functionality of the
Issue Board, that is, create or delete lists and drag issues from one list to another.
## GitLab Enterprise features for Issue Boards
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 9c045dfcce4..73d61bedde1 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -42,7 +42,7 @@ Group labels appear in every label list page of the group's child projects.
### New project label from sidebar
-From the sidebar of an issue or a merge request, you can create a create a new **project label** inline immediately, instead of navigating to the project label list page.
+From the sidebar of an issue or a merge request, you can create a new **project label** inline immediately, instead of navigating to the project label list page.
![Labels inline](img/new_label_from_sidebar.gif)
diff --git a/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png b/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png
new file mode 100644
index 00000000000..9ae6e350798
--- /dev/null
+++ b/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index ba7d05a7ad7..2765a32c845 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -169,6 +169,28 @@ can easily apply them to the codebase directly from the UI. Read
through the documentation on [Suggest changes](../../discussions/index.md#suggest-changes)
to learn more.
+## Multiple assignees **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/2004)
+in [GitLab Starter 11.11](https://about.gitlab.com/pricing).
+
+Multiple people often review merge requests at the same time. GitLab allows you to have multiple assignees for merge requests to indicate everyone that is reviewing or accountable for it.
+
+![multiple assignees for merge requests sidebar](img/multiple_assignees_for_merge_requests_sidebar.png)
+
+To assign multiple assignees to a merge request:
+
+1. From a merge request, expand the right sidebar and locate the **Assignees** section.
+1. Click on **Edit** and from the dropdown menu, select as many users as you want
+to assign the merge request to.
+
+Similarly, assignees are removed by deselecting them from the same dropdown menu.
+
+It's also possible to manage multiple assignees:
+
+- When creating a merge request.
+- Using [quick actions](../quick_actions.md#quick-actions-for-issues-and-merge-requests).
+
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index f552f60a07e..87cd4941ae6 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -54,7 +54,7 @@ Of course, before building it, you had to install Jekyll in your computer.
For that, you had to open your terminal and run `gem install jekyll`.
Right? GitLab CI + GitLab Runner do the same thing. But you need to
write in the `.gitlab-ci.yml` the script you want to run so
-GitLab Runner will do it for you. It looks more complicated then it
+GitLab Runner will do it for you. It looks more complicated than it
is. What you need to tell the Runner:
```
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 88f4de891a1..2040e2ee004 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -26,9 +26,10 @@ discussions, and descriptions:
| `/award :emoji:` | Toggle emoji award | ✓ | ✓ |
| `/assign me` | Assign yourself | ✓ | ✓ |
| `/assign @user` | Assign one user | ✓ | ✓ |
-| `/assign @user1 @user2` | Assign multiple users **[STARTER]** | ✓ | |
-| `/unassign` | Remove assignee(s) | ✓ | ✓ |
-| `/reassign @user1 @user2` | Change assignee | ✓ | ✓ |
+| `/assign @user1 @user2` | Assign multiple users **[STARTER]** | ✓ | ✓ |
+| `/unassign @user1 @user2` | Remove assignee(s) **[STARTER]** | ✓ | ✓ |
+| `/reassign @user1 @user2` | Change assignee **[STARTER]** | ✓ | ✓ |
+| `/unassign` | Remove current assignee | ✓ | ✓ |
| `/milestone %milestone` | Set milestone | ✓ | ✓ |
| `/remove_milestone` | Remove milestone | ✓ | ✓ |
| `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ |
diff --git a/jest.config.js b/jest.config.js
index fdbbe977f0b..0868547e654 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -24,6 +24,7 @@ module.exports = {
'^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
'^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
'\\.(jpg|jpeg|png|svg)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
+ 'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
diff --git a/lib/api/api.rb b/lib/api/api.rb
index bf8ddba6f0d..a572cca24e9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -134,6 +134,7 @@ module API
mount ::API::Pipelines
mount ::API::PipelineSchedules
mount ::API::ProjectClusters
+ mount ::API::ProjectEvents
mount ::API::ProjectExport
mount ::API::ProjectImport
mount ::API::ProjectHooks
diff --git a/lib/api/events.rb b/lib/api/events.rb
index b98aa9f31e1..e4c017fab42 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -4,34 +4,11 @@ module API
class Events < Grape::API
include PaginationParams
include APIGuard
+ helpers ::API::Helpers::EventsHelpers
- helpers do
- params :event_filter_params do
- optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
- optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
- optional :before, type: Date, desc: 'Include only events created before this date'
- optional :after, type: Date, desc: 'Include only events created after this date'
- end
-
- params :sort_params do
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return events sorted in ascending and descending order'
- end
-
- def present_events(events)
- events = paginate(events)
-
- present events, with: Entities::Event
- end
-
- def find_events(source)
- EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute
- end
- end
+ allow_access_with_scope :read_user, if: -> (request) { request.get? }
resource :events do
- allow_access_with_scope :read_user, if: -> (request) { request.get? }
-
desc "List currently authenticated user's events" do
detail 'This feature was introduced in GitLab 9.3.'
success Entities::Event
@@ -55,8 +32,6 @@ module API
requires :id, type: String, desc: 'The ID or Username of the user'
end
resource :users do
- allow_access_with_scope :read_user, if: -> (request) { request.get? }
-
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
@@ -76,25 +51,5 @@ module API
present_events(events)
end
end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc "List a Project's visible events" do
- success Entities::Event
- end
- params do
- use :pagination
- use :event_filter_params
- use :sort_params
- end
-
- get ":id/events" do
- events = find_events(user_project)
-
- present_events(events)
- end
- end
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8a21d44b4bf..7e4539d0419 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -67,10 +67,6 @@ module API
initial_current_user != current_user
end
- def user_namespace
- @user_namespace ||= find_namespace!(params[:id])
- end
-
def user_group
@group ||= find_group!(params[:id])
end
diff --git a/lib/api/helpers/events_helpers.rb b/lib/api/helpers/events_helpers.rb
new file mode 100644
index 00000000000..bf3b76bb92d
--- /dev/null
+++ b/lib/api/helpers/events_helpers.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module EventsHelpers
+ extend Grape::API::Helpers
+
+ params :event_filter_params do
+ optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
+ optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
+ optional :before, type: Date, desc: 'Include only events created before this date'
+ optional :after, type: Date, desc: 'Include only events created after this date'
+ end
+
+ params :sort_params do
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return events sorted in ascending and descending order'
+ end
+
+ def present_events(events)
+ events = paginate(events)
+
+ present events, with: Entities::Event
+ end
+
+ def find_events(source)
+ EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute
+ end
+ end
+ end
+end
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 3cc09f6ac3f..77ecb3e7cde 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -44,6 +44,8 @@ module API
requires :id, type: String, desc: "Namespace's ID or path"
end
get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ user_namespace = find_namespace!(params[:id])
+
present user_namespace, with: Entities::Namespace, current_user: current_user
end
end
diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb
new file mode 100644
index 00000000000..734311e1142
--- /dev/null
+++ b/lib/api/project_events.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module API
+ class ProjectEvents < Grape::API
+ include PaginationParams
+ include APIGuard
+ helpers ::API::Helpers::EventsHelpers
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "List a Project's visible events" do
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+
+ get ":id/events" do
+ events = find_events(user_project)
+
+ present_events(events)
+ end
+ end
+ end
+end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 994074ddc67..5724adb2c40 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -33,7 +33,8 @@ module API
authorize! :read_wiki, user_project
entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
- present user_project.wiki.pages, with: entity
+
+ present user_project.wiki.list_pages(load_content: params[:with_content]), with: entity
end
desc 'Get a wiki page' do
diff --git a/lib/gitlab/action_view_output/context.rb b/lib/gitlab/action_view_output/context.rb
new file mode 100644
index 00000000000..9fbc9811636
--- /dev/null
+++ b/lib/gitlab/action_view_output/context.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# This file was simplified from https://raw.githubusercontent.com/rails/rails/195f39804a7a4a0034f25e8704220e03d95a752a/actionview/lib/action_view/context.rb.
+#
+# It is only needed by modules that need to call ActionView helper
+# methods (e.g. those in
+# https://github.com/rails/rails/tree/c4d3e202e10ae627b3b9c34498afb45450652421/actionview/lib/action_view/helpers)
+# to generate tags outside of a Rails controller (e.g. API, Sidekiq,
+# etc.).
+#
+# In Rails 5, ActionView::Context automatically includes CompiledTemplates.
+# This means that any module that includes ActionView::Context is now a descendant
+# of CompiledTemplates.
+#
+# When a partial is rendered for the first time, it runs
+# Module#module_eval, which will evaluate a string source that defines a
+# new method. For example:
+#
+# def _app_views_profiles_show_html_haml___1285955918103175884_70307801785400(local_assigns, output_buffer)
+# "hello world"
+# end
+#
+# When a new method is defined, the Ruby interpreter clears the method
+# cache for all descendants, and all methods for those modules will have
+# to be redefined. This can lead to a significant performance penalty.
+#
+# Rails 6 fixes this behavior by moving out the `include
+# CompiledTemplates` into ActionView::Base so that including `ActionView::Context`
+# doesn't quietly affect other modules in this way.
+
+if Rails::VERSION::STRING.start_with?('6')
+ raise 'This module is no longer needed in Rails 6. Use ActionView::Context instead.'
+end
+
+module Gitlab
+ module ActionViewOutput
+ module Context
+ attr_accessor :output_buffer, :view_flow
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 2a90cc9a06c..fd7fac5dcab 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -30,6 +30,7 @@ dast:
- |
function dast_run() {
docker run \
+ --env DAST_FULL_SCAN_ENABLED \
--env DAST_TARGET_AVAILABILITY_TIMEOUT \
--volume "$PWD:/output" \
--volume /var/run/docker.sock:/var/run/docker.sock \
@@ -46,7 +47,8 @@ dast:
--auth-username $DAST_USERNAME \
--auth-password $DAST_PASSWORD \
--auth-username-field $DAST_USERNAME_FIELD \
- --auth-password-field $DAST_PASSWORD_FIELD
+ --auth-password-field $DAST_PASSWORD_FIELD \
+ --auth-exclude-urls $DAST_AUTH_EXCLUDE_URLS
else
dast_run
fi
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 7f80a6e9285..263221329ab 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -20,16 +20,26 @@ dependency_scanning:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
+ - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
+ function propagate_env_vars() {
+ CURRENT_ENV=$(printenv)
+
+ for VAR_NAME; do
+ echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
+ done
+ }
- |
docker run \
- --env DS_ANALYZER_IMAGES \
- --env DS_ANALYZER_IMAGE_PREFIX \
- --env DS_ANALYZER_IMAGE_TAG \
- --env DS_DEFAULT_ANALYZERS \
- --env DEP_SCAN_DISABLE_REMOTE_CHECKS \
- --env DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- --env DS_PULL_ANALYZER_IMAGE_TIMEOUT \
- --env DS_RUN_ANALYZER_TIMEOUT \
+ $(propagate_env_vars \
+ DS_ANALYZER_IMAGES \
+ DS_ANALYZER_IMAGE_PREFIX \
+ DS_ANALYZER_IMAGE_TAG \
+ DS_DEFAULT_ANALYZERS \
+ DEP_SCAN_DISABLE_REMOTE_CHECKS \
+ DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ DS_PULL_ANALYZER_IMAGE_TIMEOUT \
+ DS_RUN_ANALYZER_TIMEOUT \
+ ) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_VERSION" /code
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index b941e89991e..f0152cd4537 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -20,18 +20,28 @@ sast:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
+ - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
+ function propagate_env_vars() {
+ CURRENT_ENV=$(printenv)
+
+ for VAR_NAME; do
+ echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
+ done
+ }
- |
docker run \
- --env SAST_ANALYZER_IMAGES \
- --env SAST_ANALYZER_IMAGE_PREFIX \
- --env SAST_ANALYZER_IMAGE_TAG \
- --env SAST_DEFAULT_ANALYZERS \
- --env SAST_BRAKEMAN_LEVEL \
- --env SAST_GOSEC_LEVEL \
- --env SAST_FLAWFINDER_LEVEL \
- --env SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- --env SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
- --env SAST_RUN_ANALYZER_TIMEOUT \
+ $(propagate_env_vars \
+ SAST_ANALYZER_IMAGES \
+ SAST_ANALYZER_IMAGE_PREFIX \
+ SAST_ANALYZER_IMAGE_TAG \
+ SAST_DEFAULT_ANALYZERS \
+ SAST_BRAKEMAN_LEVEL \
+ SAST_GOSEC_LEVEL \
+ SAST_FLAWFINDER_LEVEL \
+ SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
+ SAST_RUN_ANALYZER_TIMEOUT \
+ ) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
diff --git a/lib/gitlab/ci/templates/dotNET-Core.yml b/lib/gitlab/ci/templates/dotNET-Core.yml
new file mode 100644
index 00000000000..558ca3d22e1
--- /dev/null
+++ b/lib/gitlab/ci/templates/dotNET-Core.yml
@@ -0,0 +1,107 @@
+# This is a simple example illustrating how to build and test .NET Core project
+# with GitLab Continuous Integration / Continuous Delivery.
+
+# ### Specify the Docker image
+#
+# Instead of installing .NET Core SDK manually, a docker image is used
+# with already pre-installed .NET Core SDK.
+# The 'latest' tag targets the latest available version of .NET Core SDK image.
+# If preferred, you can explicitly specify version of .NET Core e.g. using '2.2-sdk' tag.
+#
+# See other available tags for .NET Core: https://hub.docker.com/r/microsoft/dotnet
+# Learn more about Docker tags: https://docs.docker.com/glossary/?term=tag
+# and the Docker itself: https://opensource.com/resources/what-docker
+image: microsoft/dotnet:latest
+
+# ### Define variables
+#
+variables:
+ # 1) Name of directory where restore and build objects are stored.
+ OBJECTS_DIRECTORY: 'obj'
+ # 2) Name of directory used for keeping restored dependencies.
+ NUGET_PACKAGES_DIRECTORY: '.nuget'
+ # 3) A relative path to the source code from project repository root.
+ # NOTE: Please edit this path so it matches the structure of your project!
+ SOURCE_CODE_PATH: '*/*/'
+
+# ### Define stage list
+#
+# In this example there are only two stages.
+# Initially, the project will be built and then tested.
+stages:
+ - build
+ - test
+
+# ### Define global cache rule
+#
+# Before building the project, all dependencies (e.g. third-party NuGet packages)
+# must be restored. Jobs on GitLab.com's Shared Runners are executed on autoscaled machines.
+# Each machine is used only once (for security reasons) and after that it is removed.
+# What that means is that before every job a dependency restore must be performed
+# because restored dependencies are removed along with machines. Fortunately,
+# GitLab provides cache mechanism with the aim of keeping restored dependencies
+# for other jobs. This example shows how to configure cache to pass over restored
+# dependencies for re-use.
+#
+# With global cache rule, cached dependencies will be downloaded before every job
+# and then unpacked to the paths as specified below.
+cache:
+ # Per-stage and per-branch caching.
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
+ paths:
+ # Specify three paths that should be cached:
+ #
+ # 1) Main JSON file holding information about package dependency tree, packages versions,
+ # frameworks etc. It also holds information where to the dependencies were restored.
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
+ # 2) Other NuGet and MSBuild related files. Also needed.
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
+ # 3) Path to the directory where restored dependencies are kept.
+ - '$NUGET_PACKAGES_DIRECTORY'
+ # 'pull-push' policy means that latest cache will be downloaded (if exists)
+ # before executing the job, and a newer version will be uploaded afterwards.
+ # Such setting saves time when there are no changes in referenced third-party
+ # packages. For example if you run a pipeline with changes in your code,
+ # but with no changes within third-party packages which your project is using,
+ # then project restore will happen in next to no time as all required dependencies
+ # will already be there — unzipped from cache. 'pull-push' policy is a default
+ # cache policy, you do not have to specify it explicitly.
+ policy: pull-push
+
+# ### Restore project dependencies
+#
+# NuGet packages by default are restored to '.nuget/packages' directory
+# in the user's home directory. That directory is out of scope of GitLab caching.
+# To get around this a custom path can be specified using '--packages <PATH>' option
+# for 'dotnet restore' command. In this example a temporary directory is created
+# in the root of project repository, so it's content can be cached.
+#
+# Learn more about GitLab cache: https://docs.gitlab.com/ee/ci/caching/index.html
+before_script:
+ - 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
+
+build:
+ stage: build
+ # ### Build all projects discovered from solution file.
+ #
+ # Note: this will fail if you have any projects in your solution that are not
+ # .NET Core based projects e.g. WCF service, which is based on .NET Framework,
+ # not .NET Core. In such scenario you will need to build every .NET Core based
+ # project by explicitly specifying a relative path to the directory
+ # where it is located e.g. 'dotnet build ./src/ConsoleApp'.
+ # Only one project path can be passed as a parameter to 'dotnet build' command.
+ script:
+ - 'dotnet build --no-restore'
+
+tests:
+ stage: test
+ # ### Run the tests
+ #
+ # You can either run tests for all test projects that are defined in your solution
+ # with 'dotnet test' or run tests only for specific project by specifying
+ # a relative path to the directory where it is located e.g. 'dotnet test ./test/UnitTests'.
+ #
+ # You may want to define separate testing jobs for different types of testing
+ # e.g. integration tests, unit tests etc.
+ script:
+ - 'dotnet test --no-restore'
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 833aa75adb5..aab10aef398 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -27,13 +27,9 @@ module Gitlab
# don't expose `file` attribute at all (stems from what the runner
# expects).
#
- # If the `variable_masking` feature is enabled we expose the `masked`
- # attribute, otherwise it's not exposed.
- #
def to_runner_variable
@variable.reject do |hash_key, hash_value|
- (hash_key == :file && hash_value == false) ||
- (hash_key == :masked && !Feature.enabled?(:variable_masking))
+ hash_key == :file && hash_value == false
end
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index d347f3c13a4..68890aa8e30 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -99,15 +99,15 @@ module Gitlab
end
CATEGORY_LABELS = {
- docs: "~Documentation",
+ docs: "~Documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
none: "",
qa: "~QA"
}.freeze
# rubocop:disable Style/RegexpLiteral
CATEGORIES = {
- %r{\Adoc/} => :docs,
- %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
+ %r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
+ %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :none, # To reinstate roulette for documentation, set to `:docs`.
%r{\A(ee/)?app/(assets|views)/} => :frontend,
%r{\A(ee/)?public/} => :frontend,
@@ -148,7 +148,7 @@ module Gitlab
# Fallbacks in case the above patterns miss anything
%r{\.rb\z} => :backend,
- %r{\.(md|txt)\z} => :docs,
+ %r{\.(md|txt)\z} => :none, # To reinstate roulette for documentation, set to `:docs`.
%r{\.js\z} => :frontend
}.freeze
# rubocop:enable Style/RegexpLiteral
diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb
new file mode 100644
index 00000000000..26705dd1f6f
--- /dev/null
+++ b/lib/gitlab/data_builder/deployment.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DataBuilder
+ module Deployment
+ extend self
+
+ def build(deployment)
+ {
+ object_kind: 'deployment',
+ status: deployment.status,
+ deployable_id: deployment.deployable_id,
+ deployable_url: Gitlab::UrlBuilder.build(deployment.deployable),
+ environment: deployment.environment.name,
+ project: deployment.project.hook_attrs,
+ short_sha: deployment.short_sha,
+ user: deployment.user.hook_attrs,
+ commit_url: Gitlab::UrlBuilder.build(deployment.commit)
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index af385d7d4ca..40bda3410e1 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -58,7 +58,10 @@ module Gitlab
# }
#
# rubocop:disable Metrics/ParameterLists
- def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {})
+ def build(
+ project:, user:, ref:, oldrev: nil, newrev: nil,
+ commits: [], commits_count: nil, message: nil, push_options: {})
+
commits = Array(commits)
# Total commits count
@@ -113,7 +116,12 @@ module Gitlab
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
commits = project.repository.commits(project.default_branch.to_s, limit: 3)
- build(project, user, commits.last&.id, commits.first&.id, ref, commits)
+ build(project: project,
+ user: user,
+ oldrev: commits.last&.id,
+ newrev: commits.first&.id,
+ ref: ref,
+ commits: commits)
end
def sample_data
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 2770469ca9f..9fc2217ad43 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -16,6 +16,7 @@ module Gitlab
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
+ metrics_dashboard: %r{\A\.gitlab/dashboards/[^/]+\.yml\z},
xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
# Configuration files
diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb
index 8eb3c28ab70..d0577d7a4ff 100644
--- a/lib/gitlab/git/object_pool.rb
+++ b/lib/gitlab/git/object_pool.rb
@@ -40,6 +40,10 @@ module Gitlab
@repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY, gl_project_path)
end
+ def fetch
+ object_pool_service.fetch(source_repository)
+ end
+
private
def object_pool_service
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index c12cb6a6434..55bd77f6c4a 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -118,6 +118,12 @@ module Gitlab
gitaly_repository_client.exists?
end
+ def create_repository
+ wrapped_gitaly_errors do
+ gitaly_repository_client.create_repository
+ end
+ end
+
# Returns an Array of branch names
# sorted by name ASC
def branch_names
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index a0dd4a24363..c1bcd8e934a 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -86,9 +86,14 @@ module Gitlab
end
end
- def pages(limit: 0, sort: nil, direction_desc: false)
+ def list_pages(limit: 0, sort: nil, direction_desc: false, load_content: false)
wrapped_gitaly_errors do
- gitaly_get_all_pages(limit: limit, sort: sort, direction_desc: direction_desc)
+ gitaly_list_pages(
+ limit: limit,
+ sort: sort,
+ direction_desc: direction_desc,
+ load_content: load_content
+ )
end
end
@@ -168,10 +173,17 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file)
end
- 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|
+ def gitaly_list_pages(limit: 0, sort: nil, direction_desc: false, load_content: false)
+ params = { limit: limit, sort: sort, direction_desc: direction_desc }
+
+ gitaly_pages =
+ if load_content
+ gitaly_wiki_client.load_all_pages(params)
+ else
+ gitaly_wiki_client.list_all_pages(params)
+ end
+
+ gitaly_pages.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 cb80ed64eff..4b626509008 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(console_messages: check_for_console_messages(cmd))
+ success_result(cmd)
end
def guest_can_download_code?
@@ -365,6 +365,10 @@ module Gitlab
protected
+ def success_result(cmd)
+ ::Gitlab::GitAccessResult::Success.new(console_messages: check_for_console_messages(cmd))
+ end
+
def changes_list
@changes_list ||= Gitlab::ChangesList.new(changes == ANY ? [] : changes)
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c432317eb24..d34b50c5215 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -31,6 +31,9 @@ module Gitlab
MAXIMUM_GITALY_CALLS = 30
CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
+ SERVER_FEATURE_CATFILE_CACHE = 'catfile-cache'.freeze
+ SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE].freeze
+
MUTEX = Mutex.new
define_histogram :gitaly_controller_action_duration_seconds do
@@ -219,6 +222,7 @@ module Gitlab
metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
metadata['x-gitlab-correlation-id'] = Labkit::Correlation::CorrelationId.current_id if Labkit::Correlation::CorrelationId.current_id
+ metadata['gitaly-session-id'] = session_id if feature_enabled?(SERVER_FEATURE_CATFILE_CACHE)
metadata.merge!(server_feature_flags)
@@ -235,7 +239,9 @@ module Gitlab
result
end
- SERVER_FEATURE_FLAGS = %w[].freeze
+ def self.session_id
+ Gitlab::SafeRequestStore[:gitaly_session_id] ||= SecureRandom.uuid
+ end
def self.server_feature_flags
SERVER_FEATURE_FLAGS.map do |f|
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 6b8e58e6199..8ccefb00d20 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -55,13 +55,13 @@ module Gitlab
def get_blobs(revision_paths, limit = -1)
return [] if revision_paths.empty?
- revision_paths.map! do |rev, path|
+ request_revision_paths = revision_paths.map do |rev, path|
Gitaly::GetBlobsRequest::RevisionPath.new(revision: rev, path: encode_binary(path))
end
request = Gitaly::GetBlobsRequest.new(
repository: @gitaly_repo,
- revision_paths: revision_paths,
+ revision_paths: request_revision_paths,
limit: limit
)
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 0d5debfcd01..2896b7e1ce0 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -174,7 +174,7 @@ module Gitlab
response.each_with_object({}) do |gitaly_response, hsh|
gitaly_response.commits.each do |commit_for_tree|
- hsh[commit_for_tree.path] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit)
+ hsh[commit_for_tree.path_bytes] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit)
end
end
end
diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb
index ce1fb4d68ae..d7fac26bc13 100644
--- a/lib/gitlab/gitaly_client/object_pool_service.rb
+++ b/lib/gitlab/gitaly_client/object_pool_service.rb
@@ -33,6 +33,15 @@ module Gitlab
GitalyClient.call(storage, :object_pool_service, :link_repository_to_object_pool,
request, timeout: GitalyClient.fast_timeout)
end
+
+ def fetch(repository)
+ request = Gitaly::FetchIntoObjectPoolRequest.new(
+ object_pool: object_pool,
+ origin: repository.gitaly_repository
+ )
+
+ GitalyClient.call(storage, :object_pool_service, :fetch_into_object_pool, request)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index e036cdcd800..ce9faad825c 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -87,7 +87,27 @@ module Gitlab
wiki_page_from_iterator(response)
end
- def get_all_pages(limit: 0, sort: nil, direction_desc: false)
+ def list_all_pages(limit: 0, sort: nil, direction_desc: false)
+ sort_value = Gitaly::WikiListPagesRequest::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::WikiListPagesRequest.new(params)
+ stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_list_pages, request, timeout: GitalyClient.medium_timeout)
+ stream.each_with_object([]) do |message, pages|
+ page = message.page
+
+ next unless page
+
+ wiki_page = GitalyClient::WikiPage.new(page.to_h)
+ version = new_wiki_page_version(page.version)
+
+ pages << [wiki_page, version]
+ end
+ end
+
+ def load_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 }
@@ -95,6 +115,7 @@ module Gitlab
request = Gitaly::WikiGetAllPagesRequest.new(params)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout)
+
pages = []
loop do
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index a56ca1e39e7..04dabe423e8 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Gitlab
- module GlRepository
+ class GlRepository
+ include Singleton
+
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccess,
@@ -19,7 +21,7 @@ module Gitlab
}.freeze
def self.types
- TYPES
+ instance.types
end
def self.parse(gl_repository)
@@ -39,5 +41,11 @@ module Gitlab
def self.default_type
PROJECT
end
+
+ def types
+ TYPES
+ end
+
+ private_class_method :instance
end
end
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 7abe6c29a25..19915980d7f 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Gitlab
- module GlRepository
+ class GlRepository
class RepoType
attr_reader :name,
:access_checker_class,
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index e00309e7946..582c3065189 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -15,7 +15,12 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
- gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
+
+ if Gitlab::CurrentSettings.clientside_sentry_enabled
+ gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn
+ gon.sentry_environment = Gitlab.config.sentry.environment
+ end
+
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab.revision
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
diff --git a/lib/gitlab/graphql/tracing.rb b/lib/gitlab/graphql/generic_tracing.rb
index 6b505e4262b..936b22d5afa 100644
--- a/lib/gitlab/graphql/tracing.rb
+++ b/lib/gitlab/graphql/generic_tracing.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
+# This class is used as a hook to observe graphql runtime events. From this
+# hook both gitlab metrics and opentracking measurements are generated
+
module Gitlab
module Graphql
- class Tracing < GraphQL::Tracing::PlatformTracing
+ class GenericTracing < GraphQL::Tracing::PlatformTracing
self.platform_keys = {
'lex' => 'graphql.lex',
'parse' => 'graphql.parse',
@@ -21,17 +24,30 @@ module Gitlab
end
def platform_trace(platform_key, key, data, &block)
+ tags = { platform_key: platform_key, key: key }
start = Gitlab::Metrics::System.monotonic_time
- yield
+ with_labkit_tracing(tags, &block)
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
- graphql_duration_seconds.observe({ platform_key: platform_key, key: key }, duration)
+ graphql_duration_seconds.observe(tags, duration)
end
private
+ def with_labkit_tracing(tags, &block)
+ return yield unless Labkit::Tracing.enabled?
+
+ name = "#{tags[:platform_key]}.#{tags[:key]}"
+ span_tags = {
+ 'component' => 'web',
+ 'span.kind' => 'server'
+ }.merge(tags.stringify_keys)
+
+ Labkit::Tracing.with_tracing(operation_name: name, tags: span_tags, &block)
+ end
+
def graphql_duration_seconds
@graphql_duration_seconds ||= Gitlab::Metrics.histogram(
:graphql_duration_seconds,
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index ce268793128..c6d4fda4af5 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -75,6 +75,7 @@ project_tree:
- :project_badges
- :ci_cd_settings
- :error_tracking_setting
+ - :metrics_setting
# Only include the following attributes for the models specified.
included_attributes:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 61a1aa6da5a..e1e70a008d9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -25,7 +25,8 @@ module Gitlab
metrics: 'MergeRequest::Metrics',
ci_cd_settings: 'ProjectCiCdSetting',
error_tracking_setting: 'ErrorTracking::ProjectErrorTrackingSetting',
- links: 'Releases::Link' }.freeze
+ links: 'Releases::Link',
+ metrics_setting: 'ProjectMetricsSetting' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index 7dfd9ed4f35..ff1dadf9247 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -22,6 +22,13 @@ module Gitlab
alias_method :update, :install
+ def uninstall(command)
+ namespace.ensure_exists!
+
+ delete_pod!(command.pod_name)
+ kubeclient.create_pod(command.pod_resource)
+ end
+
##
# Returns Pod phase
#
diff --git a/lib/gitlab/metrics/dashboard/base_service.rb b/lib/gitlab/metrics/dashboard/base_service.rb
new file mode 100644
index 00000000000..94aabd0466c
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/base_service.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+module Gitlab
+ module Metrics
+ module Dashboard
+ class BaseService < ::BaseService
+ DASHBOARD_LAYOUT_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ def get_dashboard
+ return error("#{dashboard_path} could not be found.", :not_found) unless path_available?
+
+ success(dashboard: process_dashboard)
+ rescue DASHBOARD_LAYOUT_ERROR => e
+ error(e.message, :unprocessable_entity)
+ end
+
+ # Summary of all known dashboards for the service.
+ # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+ def all_dashboard_paths(_project)
+ raise NotImplementedError
+ end
+
+ private
+
+ # Returns a new dashboard Hash, supplemented with DB info
+ def process_dashboard
+ Gitlab::Metrics::Dashboard::Processor
+ .new(project, params[:environment], raw_dashboard)
+ .process(insert_project_metrics: insert_project_metrics?)
+ end
+
+ # @return [String] Relative filepath of the dashboard yml
+ def dashboard_path
+ params[:dashboard_path]
+ end
+
+ # Returns an un-processed dashboard from the cache.
+ def raw_dashboard
+ Rails.cache.fetch(cache_key) { get_raw_dashboard }
+ end
+
+ # @return [Hash] an unmodified dashboard
+ def get_raw_dashboard
+ raise NotImplementedError
+ end
+
+ # @return [String]
+ def cache_key
+ raise NotImplementedError
+ end
+
+ # Determines whether custom metrics should be included
+ # in the processed output.
+ def insert_project_metrics?
+ false
+ end
+
+ # Checks if dashboard path exists or should be rejected
+ # as a result of file-changes to the project repository.
+ # @return [Boolean]
+ def path_available?
+ available_paths = Gitlab::Metrics::Dashboard::Finder.find_all_paths(project)
+
+ available_paths.any? do |path_params|
+ path_params[:path] == dashboard_path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
new file mode 100644
index 00000000000..4a41590f000
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Returns DB-supplmented dashboard info for determining
+# the layout of UI. Intended entry-point for the Metrics::Dashboard
+# module.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class Finder
+ class << self
+ # Returns a formatted dashboard packed with DB info.
+ # @return [Hash]
+ def find(project, user, environment, dashboard_path = nil)
+ service = system_dashboard?(dashboard_path) ? system_service : project_service
+
+ service
+ .new(project, user, environment: environment, dashboard_path: dashboard_path)
+ .get_dashboard
+ end
+
+ # Summary of all known dashboards.
+ # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+ def find_all_paths(project)
+ project.repository.metrics_dashboard_paths
+ end
+
+ # Summary of all known dashboards. Used to populate repo cache.
+ # Prefer #find_all_paths.
+ def find_all_paths_from_source(project)
+ system_service.all_dashboard_paths(project)
+ .+ project_service.all_dashboard_paths(project)
+ end
+
+ private
+
+ def system_service
+ Gitlab::Metrics::Dashboard::SystemDashboardService
+ end
+
+ def project_service
+ Gitlab::Metrics::Dashboard::ProjectDashboardService
+ end
+
+ def system_dashboard?(filepath)
+ !filepath || system_service.system_dashboard?(filepath)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
new file mode 100644
index 00000000000..dd986020693
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ # Responsible for processesing a dashboard hash, inserting
+ # relevant DB records & sorting for proper rendering in
+ # the UI. These includes shared metric info, custom metrics
+ # info, and alerts (only in EE).
+ class Processor
+ SYSTEM_SEQUENCE = [
+ Stages::CommonMetricsInserter,
+ Stages::ProjectMetricsInserter,
+ Stages::Sorter
+ ].freeze
+
+ PROJECT_SEQUENCE = [
+ Stages::CommonMetricsInserter,
+ Stages::Sorter
+ ].freeze
+
+ def initialize(project, environment, dashboard)
+ @project = project
+ @environment = environment
+ @dashboard = dashboard
+ end
+
+ # Returns a new dashboard hash with the results of
+ # running transforms on the dashboard.
+ def process(insert_project_metrics:)
+ @dashboard.deep_symbolize_keys.tap do |dashboard|
+ sequence(insert_project_metrics).each do |stage|
+ stage.new(@project, @environment, dashboard).transform!
+ end
+ end
+ end
+
+ private
+
+ def sequence(insert_project_metrics)
+ insert_project_metrics ? SYSTEM_SEQUENCE : PROJECT_SEQUENCE
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/project_dashboard_service.rb b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
new file mode 100644
index 00000000000..fdffd067c93
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class ProjectDashboardService < Gitlab::Metrics::Dashboard::BaseService
+ DASHBOARD_ROOT = ".gitlab/dashboards"
+
+ class << self
+ def all_dashboard_paths(project)
+ file_finder(project)
+ .list_files_for(DASHBOARD_ROOT)
+ .map do |filepath|
+ Rails.cache.delete(cache_key(project.id, filepath))
+
+ { path: filepath, default: false }
+ end
+ end
+
+ def file_finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, DASHBOARD_ROOT, '.yml')
+ end
+
+ def cache_key(id, dashboard_path)
+ "project_#{id}_metrics_dashboard_#{dashboard_path}"
+ end
+ end
+
+ private
+
+ # Searches the project repo for a custom-defined dashboard.
+ def get_raw_dashboard
+ yml = self.class.file_finder(project).read(dashboard_path)
+
+ YAML.safe_load(yml)
+ end
+
+ def cache_key
+ self.class.cache_key(project.id, dashboard_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/base_stage.rb b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
new file mode 100644
index 00000000000..a6d1f974556
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class BaseStage
+ DashboardLayoutError = Class.new(StandardError)
+
+ DEFAULT_PANEL_TYPE = 'area-chart'
+
+ attr_reader :project, :environment, :dashboard
+
+ def initialize(project, environment, dashboard)
+ @project = project
+ @environment = environment
+ @dashboard = dashboard
+ end
+
+ # Entry-point to the stage
+ def transform!
+ raise NotImplementedError
+ end
+
+ protected
+
+ def missing_panel_groups!
+ raise DashboardLayoutError.new('Top-level key :panel_groups must be an array')
+ end
+
+ def missing_panels!
+ raise DashboardLayoutError.new('Each "panel_group" must define an array :panels')
+ end
+
+ def missing_metrics!
+ raise DashboardLayoutError.new('Each "panel" must define an array :metrics')
+ end
+
+ def for_metrics
+ missing_panel_groups! unless dashboard[:panel_groups].is_a?(Array)
+
+ dashboard[:panel_groups].each do |panel_group|
+ missing_panels! unless panel_group[:panels].is_a?(Array)
+
+ panel_group[:panels].each do |panel|
+ missing_metrics! unless panel[:metrics].is_a?(Array)
+
+ panel[:metrics].each do |metric|
+ yield metric
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
new file mode 100644
index 00000000000..188912bedb4
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class CommonMetricsInserter < BaseStage
+ # For each metric in the dashboard config, attempts to
+ # find a corresponding database record. If found,
+ # includes the record's id in the dashboard config.
+ def transform!
+ common_metrics = ::PrometheusMetric.common
+
+ for_metrics do |metric|
+ metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
+ metric[:metric_id] = metric_record.id if metric_record
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
new file mode 100644
index 00000000000..221610a14d1
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class ProjectMetricsInserter < BaseStage
+ # Inserts project-specific metrics into the dashboard
+ # config. If there are no project-specific metrics,
+ # this will have no effect.
+ def transform!
+ project.prometheus_metrics.each do |project_metric|
+ group = find_or_create_panel_group(dashboard[:panel_groups], project_metric)
+ panel = find_or_create_panel(group[:panels], project_metric)
+ find_or_create_metric(panel[:metrics], project_metric)
+ end
+ end
+
+ private
+
+ # Looks for a panel_group corresponding to the
+ # provided metric object. If unavailable, inserts one.
+ # @param panel_groups [Array<Hash>]
+ # @param metric [PrometheusMetric]
+ def find_or_create_panel_group(panel_groups, metric)
+ panel_group = find_panel_group(panel_groups, metric)
+ return panel_group if panel_group
+
+ panel_group = new_panel_group(metric)
+ panel_groups << panel_group
+
+ panel_group
+ end
+
+ # Looks for a panel corresponding to the provided
+ # metric object. If unavailable, inserts one.
+ # @param panels [Array<Hash>]
+ # @param metric [PrometheusMetric]
+ def find_or_create_panel(panels, metric)
+ panel = find_panel(panels, metric)
+ return panel if panel
+
+ panel = new_panel(metric)
+ panels << panel
+
+ panel
+ end
+
+ # Looks for a metric corresponding to the provided
+ # metric object. If unavailable, inserts one.
+ # @param metrics [Array<Hash>]
+ # @param metric [PrometheusMetric]
+ def find_or_create_metric(metrics, metric)
+ target_metric = find_metric(metrics, metric)
+ return target_metric if target_metric
+
+ target_metric = new_metric(metric)
+ metrics << target_metric
+
+ target_metric
+ end
+
+ def find_panel_group(panel_groups, metric)
+ return unless panel_groups
+
+ panel_groups.find { |group| group[:group] == metric.group_title }
+ end
+
+ def find_panel(panels, metric)
+ return unless panels
+
+ panel_identifiers = [DEFAULT_PANEL_TYPE, metric.title, metric.y_label]
+ panels.find { |panel| panel.values_at(:type, :title, :y_label) == panel_identifiers }
+ end
+
+ def find_metric(metrics, metric)
+ return unless metrics
+
+ metrics.find { |m| m[:id] == metric.identifier }
+ end
+
+ def new_panel_group(metric)
+ {
+ group: metric.group_title,
+ priority: metric.priority,
+ panels: []
+ }
+ end
+
+ def new_panel(metric)
+ {
+ type: DEFAULT_PANEL_TYPE,
+ title: metric.title,
+ y_label: metric.y_label,
+ metrics: []
+ }
+ end
+
+ def new_metric(metric)
+ metric.queries.first.merge(metric_id: metric.id)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/sorter.rb b/lib/gitlab/metrics/dashboard/stages/sorter.rb
new file mode 100644
index 00000000000..ba5aa78059c
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/sorter.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class Sorter < BaseStage
+ def transform!
+ missing_panel_groups! unless dashboard[:panel_groups].is_a? Array
+
+ sort_groups!
+ sort_panels!
+ end
+
+ private
+
+ # Sorts the groups in the dashboard by the :priority key
+ def sort_groups!
+ dashboard[:panel_groups] = dashboard[:panel_groups].sort_by { |group| -group[:priority].to_i }
+ end
+
+ # Sorts the panels in the dashboard by the :weight key
+ def sort_panels!
+ dashboard[:panel_groups].each do |group|
+ missing_panels! unless group[:panels].is_a? Array
+
+ group[:panels] = group[:panels].sort_by { |panel| -panel[:weight].to_i }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/system_dashboard_service.rb b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
new file mode 100644
index 00000000000..67509ed4230
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Fetches the system metrics dashboard and formats the output.
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class SystemDashboardService < Gitlab::Metrics::Dashboard::BaseService
+ SYSTEM_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
+
+ class << self
+ def all_dashboard_paths(_project)
+ [{
+ path: SYSTEM_DASHBOARD_PATH,
+ default: true
+ }]
+ end
+
+ def system_dashboard?(filepath)
+ filepath == SYSTEM_DASHBOARD_PATH
+ end
+ end
+
+ private
+
+ def dashboard_path
+ SYSTEM_DASHBOARD_PATH
+ end
+
+ # Returns the base metrics shipped with every GitLab service.
+ def get_raw_dashboard
+ yml = File.read(Rails.root.join(dashboard_path))
+
+ YAML.safe_load(yml)
+ end
+
+ def cache_key
+ "metrics_dashboard_#{dashboard_path}"
+ end
+
+ def insert_project_metrics?
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 28ed587f5c7..890228e5e78 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -73,7 +73,7 @@ module Gitlab
result = with_custom_logger(logger) do
with_user(user) do
- RubyProf.profile { app.public_send(verb, url, post_data, headers) } # rubocop:disable GitlabSecurity/PublicSend
+ RubyProf.profile { app.public_send(verb, url, params: post_data, headers: headers) } # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/lib/gitlab/prometheus/query_variables.rb b/lib/gitlab/prometheus/query_variables.rb
index 1cc85d4b4a6..dca09aef47d 100644
--- a/lib/gitlab/prometheus/query_variables.rb
+++ b/lib/gitlab/prometheus/query_variables.rb
@@ -4,9 +4,13 @@ module Gitlab
module Prometheus
module QueryVariables
def self.call(environment)
+ deployment_platform = environment.deployment_platform
+ namespace = deployment_platform&.namespace_for(environment.project) ||
+ deployment_platform&.actual_namespace || ''
+
{
ci_environment_slug: environment.slug,
- kube_namespace: environment.deployment_platform&.actual_namespace || '',
+ kube_namespace: namespace,
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
}
end
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index fb303e3fb0c..c102fa14cfc 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -7,7 +7,7 @@ module Gitlab
module SidekiqConfig
QUEUE_CONFIG_PATHS = %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml].freeze
- # This method is called by `bin/sidekiq-cluster` in EE, which runs outside
+ # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside
# of bundler/Rails context, so we cannot use any gem or Rails methods.
def self.worker_queues(rails_path = Rails.root.to_s)
@worker_queues ||= {}
@@ -19,7 +19,7 @@ module Gitlab
end
end
- # This method is called by `bin/sidekiq-cluster` in EE, which runs outside
+ # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside
# of bundler/Rails context, so we cannot use any gem or Rails methods.
def self.expand_queues(queues, all_queues = self.worker_queues)
return [] if queues.empty?
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index f86d599e4cb..169ce8ab026 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -30,6 +30,8 @@ module Gitlab
snippet_url(object)
when Milestone
milestone_url(object)
+ when ::Ci::Build
+ project_job_url(object.project, object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 06f2f848925..8ae350779d4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -138,6 +138,9 @@ msgstr ""
msgid "%{label_for_message} unavailable"
msgstr ""
+msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
+msgstr ""
+
msgid "%{level_name} is not allowed in a %{group_level_name} group."
msgstr ""
@@ -245,6 +248,9 @@ msgstr ""
msgid "- show less"
msgstr ""
+msgid "0 for unlimited"
+msgstr ""
+
msgid "1 %{type} addition"
msgid_plural "%{count} %{type} additions"
msgstr[0] ""
@@ -366,6 +372,9 @@ 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 Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates."
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -801,6 +810,9 @@ msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
+msgid "An error occurred while fetching label colors."
+msgstr ""
+
msgid "An error occurred while fetching markdown preview"
msgstr ""
@@ -810,6 +822,9 @@ msgstr ""
msgid "An error occurred while fetching stages."
msgstr ""
+msgid "An error occurred while fetching the board lists. Please try again."
+msgstr ""
+
msgid "An error occurred while fetching the job log."
msgstr ""
@@ -846,6 +861,9 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
+msgid "An error occurred while parsing recent searches"
+msgstr ""
+
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -933,6 +951,9 @@ msgstr ""
msgid "Application settings saved successfully"
msgstr ""
+msgid "Application uninstalled but failed to destroy: %{error_message}"
+msgstr ""
+
msgid "Application was successfully destroyed."
msgstr ""
@@ -951,6 +972,15 @@ msgstr ""
msgid "Apply suggestion"
msgstr ""
+msgid "Applying command"
+msgstr ""
+
+msgid "Applying command to %{commandDescription}"
+msgstr ""
+
+msgid "Applying multiple commands"
+msgstr ""
+
msgid "Apr"
msgstr ""
@@ -981,6 +1011,9 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
+msgid "Are you sure you want to delete this list?"
+msgstr ""
+
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
@@ -1173,6 +1206,9 @@ msgstr ""
msgid "Automatically resolved"
msgstr ""
+msgid "Autosave|Note"
+msgstr ""
+
msgid "Available"
msgstr ""
@@ -1284,6 +1320,9 @@ msgstr ""
msgid "Badges|e.g. %{exampleUrl}"
msgstr ""
+msgid "Balsamiq file could not be loaded."
+msgstr ""
+
msgid "BambooService|A continuous integration and build server"
msgstr ""
@@ -1578,6 +1617,9 @@ msgstr ""
msgid "Can't find HEAD commit for this branch"
msgstr ""
+msgid "Can't find variable: ZiteReader"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -1602,6 +1644,9 @@ msgstr ""
msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded."
msgstr ""
+msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above."
+msgstr ""
+
msgid "Cannot skip two factor authentication setup"
msgstr ""
@@ -1788,6 +1833,9 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "CiVariables|Cannot use Masked Variable with current value"
+msgstr ""
+
msgid "CiVariables|Input variable key"
msgstr ""
@@ -1797,9 +1845,6 @@ msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
-msgid "CiVariables|This variable will not be masked"
-msgstr ""
-
msgid "CiVariable|* (All environments)"
msgstr ""
@@ -1890,6 +1935,9 @@ msgstr ""
msgid "Close milestone"
msgstr ""
+msgid "Close sidebar"
+msgstr ""
+
msgid "Closed"
msgstr ""
@@ -2388,9 +2436,15 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "ComboSearch is not defined"
+msgstr ""
+
msgid "Command line instructions"
msgstr ""
+msgid "Commands applied"
+msgstr ""
+
msgid "Comment"
msgstr ""
@@ -2522,6 +2576,9 @@ msgstr ""
msgid "Configure Gitaly timeouts."
msgstr ""
+msgid "Configure Let's Encrypt"
+msgstr ""
+
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr ""
@@ -2648,6 +2705,9 @@ msgstr ""
msgid "ConvDev Index"
msgstr ""
+msgid "Copied"
+msgstr ""
+
msgid "Copy %{http_label} clone URL"
msgstr ""
@@ -2696,6 +2756,9 @@ msgstr ""
msgid "Copy secret to clipboard"
msgstr ""
+msgid "Copy source to clipboard"
+msgstr ""
+
msgid "Copy to clipboard"
msgstr ""
@@ -3259,6 +3322,9 @@ msgstr ""
msgid "Discuss a specific suggestion or question that needs to be resolved"
msgstr ""
+msgid "Discussion"
+msgstr ""
+
msgid "Dismiss"
msgstr ""
@@ -3271,9 +3337,15 @@ msgstr ""
msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?"
msgstr ""
+msgid "Dockerfile"
+msgstr ""
+
msgid "Domain"
msgstr ""
+msgid "Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -3685,6 +3757,9 @@ msgstr ""
msgid "Error loading branches."
msgstr ""
+msgid "Error loading file viewer."
+msgstr ""
+
msgid "Error loading last commit."
msgstr ""
@@ -3703,6 +3778,9 @@ msgstr ""
msgid "Error loading template."
msgstr ""
+msgid "Error loading viewer"
+msgstr ""
+
msgid "Error occurred when toggling the notification subscription"
msgstr ""
@@ -3736,9 +3814,15 @@ msgstr ""
msgid "Error uploading file"
msgstr ""
+msgid "Error uploading file: %{stripped}"
+msgstr ""
+
msgid "Error while loading the merge request. Please try again."
msgstr ""
+msgid "Error while loading the project data. Please try again."
+msgstr ""
+
msgid "Error while migrating %{upload_id}: %{error_message}"
msgstr ""
@@ -3937,6 +4021,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
+msgid "Failed to connect to the prometheus server"
+msgstr ""
+
msgid "Failed to create repository via gitlab-shell"
msgstr ""
@@ -3985,6 +4072,9 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failed to update tag!"
+msgstr ""
+
msgid "Failed to update."
msgstr ""
@@ -4616,6 +4706,9 @@ msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
+msgid "I have read and agree to the Let's Encrypt Terms of Service"
+msgstr ""
+
msgid "ID"
msgstr ""
@@ -5165,6 +5258,9 @@ msgstr ""
msgid "LFSStatus|Enabled"
msgstr ""
+msgid "LICENSE"
+msgstr ""
+
msgid "Label"
msgstr ""
@@ -5305,6 +5401,9 @@ msgstr ""
msgid "Leave the \"File type\" and \"Delivery method\" options on their default values."
msgstr ""
+msgid "Let's Encrypt does not accept emails on example.com"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
@@ -5936,12 +6035,18 @@ msgstr ""
msgid "Newly registered users will by default be external"
msgstr ""
+msgid "Next"
+msgstr ""
+
msgid "No"
msgstr ""
msgid "No %{providerTitle} repositories available to import"
msgstr ""
+msgid "No Milestone"
+msgstr ""
+
msgid "No Tag"
msgstr ""
@@ -6008,6 +6113,9 @@ msgstr ""
msgid "No other labels with such name or description"
msgstr ""
+msgid "No parent group"
+msgstr ""
+
msgid "No preview for this file type"
msgstr ""
@@ -6083,6 +6191,9 @@ msgstr ""
msgid "Notes|Show history only"
msgstr ""
+msgid "Nothing to preview."
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -6238,6 +6349,12 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operation failed. Check pod logs for %{pod_name} for more details."
+msgstr ""
+
+msgid "Operation timed out. Check pod logs for %{pod_name} for more details."
+msgstr ""
+
msgid "Operations"
msgstr ""
@@ -6589,6 +6706,9 @@ msgstr ""
msgid "Please accept the Terms of Service before continuing."
msgstr ""
+msgid "Please add a list to your board first"
+msgstr ""
+
msgid "Please choose a group URL with no special characters."
msgstr ""
@@ -6613,6 +6733,9 @@ msgstr ""
msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access."
msgstr ""
+msgid "Please select a file"
+msgstr ""
+
msgid "Please select a group."
msgstr ""
@@ -6649,12 +6772,18 @@ msgstr ""
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr ""
+msgid "Press %{key}-C to copy"
+msgstr ""
+
msgid "Press Enter or click to search"
msgstr ""
msgid "Preview"
msgstr ""
+msgid "Preview Markdown"
+msgstr ""
+
msgid "Preview changes"
msgstr ""
@@ -7150,6 +7279,57 @@ msgstr ""
msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
msgstr ""
+msgid "ProjectTemplates|.NET Core"
+msgstr ""
+
+msgid "ProjectTemplates|Android"
+msgstr ""
+
+msgid "ProjectTemplates|Go Micro"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/GitBook"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Hexo"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Hugo"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Jekyll"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Plain HTML"
+msgstr ""
+
+msgid "ProjectTemplates|NodeJS Express"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/GitBook"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Hexo"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Hugo"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Jekyll"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Plain HTML"
+msgstr ""
+
+msgid "ProjectTemplates|Ruby on Rails"
+msgstr ""
+
+msgid "ProjectTemplates|Spring"
+msgstr ""
+
+msgid "ProjectTemplates|iOS (Swift)"
+msgstr ""
+
msgid "Projects"
msgstr ""
@@ -7273,6 +7453,9 @@ msgstr ""
msgid "Protected"
msgstr ""
+msgid "Protected Tag"
+msgstr ""
+
msgid "Protip:"
msgstr ""
@@ -7378,6 +7561,9 @@ msgstr ""
msgid "Recent Project Activity"
msgstr ""
+msgid "Recent Searches Service is unavailable"
+msgstr ""
+
msgid "Recent searches"
msgstr ""
@@ -7617,6 +7803,9 @@ msgstr ""
msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr ""
+msgid "Require users to prove ownership of custom domains"
+msgstr ""
+
msgid "Resend invite"
msgstr ""
@@ -7793,6 +7982,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
+msgid "Save anyway"
+msgstr ""
+
msgid "Save application"
msgstr ""
@@ -8296,6 +8488,9 @@ msgstr ""
msgid "Something went wrong when toggling the button"
msgstr ""
+msgid "Something went wrong while adding your award. Please try again."
+msgstr ""
+
msgid "Something went wrong while applying the suggestion. Please try again."
msgstr ""
@@ -8308,6 +8503,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching latest comments."
+msgstr ""
+
msgid "Something went wrong while fetching related merge requests."
msgstr ""
@@ -8671,6 +8869,12 @@ msgstr ""
msgid "Switch branch/tag"
msgstr ""
+msgid "Switch to GitLab Next"
+msgstr ""
+
+msgid "Switch to the source to copy it to the clipboard"
+msgstr ""
+
msgid "System Hooks"
msgstr ""
@@ -8695,6 +8899,9 @@ msgstr ""
msgid "System metrics (Kubernetes)"
msgstr ""
+msgid "Table of Contents"
+msgstr ""
+
msgid "Tag"
msgstr ""
@@ -8818,6 +9025,9 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "Test settings and save changes"
+msgstr ""
+
msgid "TestHooks|Ensure one of your projects has merge requests."
msgstr ""
@@ -9093,6 +9303,9 @@ msgstr ""
msgid "There is already a repository with that name on disk"
msgstr ""
+msgid "There was a problem communicating with your device."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -9117,6 +9330,9 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
+msgid "There was an error while fetching cycle analytics data."
+msgstr ""
+
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
@@ -9168,6 +9384,12 @@ msgstr ""
msgid "This container registry has been scheduled for deletion."
msgstr ""
+msgid "This device has already been registered with us."
+msgstr ""
+
+msgid "This device has not been registered with us."
+msgstr ""
+
msgid "This diff is collapsed."
msgstr ""
@@ -9754,12 +9976,18 @@ msgstr ""
msgid "Type"
msgstr ""
+msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
+msgstr ""
+
msgid "Unable to connect to server: %{error}"
msgstr ""
msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
+msgid "Unable to regenerate public ssh key."
+msgstr ""
+
msgid "Unable to schedule a pipeline to run immediately"
msgstr ""
@@ -9784,6 +10012,9 @@ msgstr ""
msgid "Unknown format"
msgstr ""
+msgid "Unknown response text"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -10180,6 +10411,9 @@ msgstr ""
msgid "VisibilityLevel|Unknown"
msgstr ""
+msgid "Wait for the source to load to copy it to the clipboard"
+msgstr ""
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
@@ -10210,6 +10444,9 @@ msgstr ""
msgid "Webhooks Help"
msgstr ""
+msgid "Welcome to your Issue Board!"
+msgstr ""
+
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
@@ -10404,6 +10641,9 @@ msgstr ""
msgid "Yes"
msgstr ""
+msgid "Yes or No"
+msgstr ""
+
msgid "Yes, add it"
msgstr ""
@@ -10452,6 +10692,12 @@ msgstr ""
msgid "You can also create a project from the command line."
msgstr ""
+msgid "You can also press &#8984;-Enter"
+msgstr ""
+
+msgid "You can also press Ctrl-Enter"
+msgstr ""
+
msgid "You can also star a label to make it a priority label."
msgstr ""
@@ -10909,6 +11155,9 @@ msgstr ""
msgid "it is too large"
msgstr ""
+msgid "jigsaw is not defined"
+msgstr ""
+
msgid "latest"
msgstr ""
@@ -11226,7 +11475,7 @@ msgstr[1] ""
msgid "score"
msgstr ""
-msgid "should be higher than %{access} inherited membership from group %{group_name}"
+msgid "should be greater than or equal to %{access} inherited membership from group %{group_name}"
msgstr ""
msgid "show less"
diff --git a/package.json b/package.json
index e04470109be..3410bcaa9f5 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,7 @@
"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",
+ "jest-debug": "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",
@@ -34,7 +33,7 @@
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.59.0",
- "@gitlab/ui": "^3.4.0",
+ "@gitlab/ui": "^3.7.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-upload-client": "^10.0.0",
@@ -90,7 +89,7 @@
"jszip-utils": "^0.0.2",
"katex": "^0.10.0",
"marked": "^0.3.12",
- "mermaid": "^8.0.0-rc.8",
+ "mermaid": "^8.0.0",
"monaco-editor": "^0.15.6",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
index de5c535c757..10bba98f704 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -2,7 +2,8 @@
module QA
context 'Create' do
- describe 'File templates' do
+ # Issue: https://gitlab.com/gitlab-org/quality/nightly/issues/97
+ describe 'File templates', :quarantine do
include Runtime::Fixtures
def login
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index daeee665c93..c2c2b6da90a 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/62
+ context 'Create', :quarantine do
describe 'Create, list, and delete branches via web' do
master_branch = 'master'
second_branch = 'second-branch'
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
index 309ae6cd986..e689ba4c69c 100644
--- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
@@ -3,11 +3,6 @@
module QA
context 'Create' do
describe 'Wiki management' do
- def validate_content(content)
- expect(page).to have_content('Wiki was successfully updated')
- expect(page).to have_content(/#{content}/)
- end
-
it 'user creates, edits, clones, and pushes to the wiki' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
@@ -38,6 +33,11 @@ module QA
expect(page).to have_content('My Third Wiki Content')
end
+
+ def validate_content(content)
+ expect(page).to have_content('Wiki was successfully updated')
+ expect(page).to have_content(/#{content}/)
+ end
end
end
end
diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb
index 52cb05fcd13..ca0ce32e74f 100644
--- a/qa/qa/specs/helpers/quarantine.rb
+++ b/qa/qa/specs/helpers/quarantine.rb
@@ -20,6 +20,14 @@ module QA::Specs::Helpers
end
end
+ # Skip the entire context if a context is quarantined. This avoids running
+ # before blocks unnecessarily.
+ def skip_or_run_quarantined_contexts(filters, example)
+ return unless example.metadata.key?(:quarantine)
+
+ skip_or_run_quarantined_tests_or_contexts(filters, example)
+ end
+
# Skip tests in quarantine unless we explicitly focus on them.
def skip_or_run_quarantined_tests_or_contexts(filters, example)
if filters.key?(:quarantine)
@@ -39,14 +47,6 @@ module QA::Specs::Helpers
end
end
- # Skip the entire context if a context is quarantined. This avoids running
- # before blocks unnecessarily.
- def skip_or_run_quarantined_contexts(filters, example)
- return unless example.metadata.key?(:quarantine)
-
- skip_or_run_quarantined_tests_or_contexts(filters, example)
- end
-
def filters_other_than_quarantine(filter)
filter.reject { |key, _| key == :quarantine }
end
diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb
index 6d8f9aa7c12..120ba6e6c06 100644
--- a/qa/qa/vendor/github/page/login.rb
+++ b/qa/qa/vendor/github/page/login.rb
@@ -12,9 +12,7 @@ module QA
fill_in 'password', with: QA::Runtime::Env.github_password
click_on 'Sign in'
- unless has_no_text?("Authorize GitLab-OAuth")
- click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa')
- end
+ click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa')
end
end
end
diff --git a/rubocop/cop/include_action_view_context.rb b/rubocop/cop/include_action_view_context.rb
new file mode 100644
index 00000000000..14662a33e95
--- /dev/null
+++ b/rubocop/cop/include_action_view_context.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative '../spec_helpers'
+
+module RuboCop
+ module Cop
+ # Cop that makes sure workers include `::Gitlab::ActionViewOutput::Context`, not `ActionView::Context`.
+ class IncludeActionViewContext < RuboCop::Cop::Cop
+ include SpecHelpers
+
+ MSG = 'Include `::Gitlab::ActionViewOutput::Context`, not `ActionView::Context`, for Rails 5.'.freeze
+
+ def_node_matcher :includes_action_view_context?, <<~PATTERN
+ (send nil? :include (const (const nil? :ActionView) :Context))
+ PATTERN
+
+ def on_send(node)
+ return if in_spec?(node)
+ return unless includes_action_view_context?(node)
+
+ add_offense(node.arguments.first, location: :expression)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.replace(node.source_range, '::Gitlab::ActionViewOutput::Context')
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 50eab6f9270..ce6bdbf292c 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -4,6 +4,7 @@ require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/finder_with_find_by'
require_relative 'cop/gitlab/union'
+require_relative 'cop/include_action_view_context'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/safe_params'
require_relative 'cop/active_record_association_reload'
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index bc73225c1bf..f39c64339fe 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -35,7 +35,7 @@ fi
# Do not use 'README.md', instead use 'index.md'
# Number of 'README.md's as of 2018-03-26
-NUMBER_READMES_CE=43
+NUMBER_READMES_CE=44
NUMBER_READMES_EE=46
FIND_READMES=$(find doc/ -name "README.md" | wc -l)
echo '=> Checking for new README.md files...'
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index b55ce1af55e..9455e462617 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -1,26 +1,6 @@
[[ "$TRACE" ]] && set -x
export TILLER_NAMESPACE="$KUBE_NAMESPACE"
-function echoerr() {
- local header="${2}"
-
- if [ -n "${header}" ]; then
- printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2;
- else
- printf "\033[0;31m%s\n\033[0m" "${1}" >&2;
- fi
-}
-
-function echoinfo() {
- local header="${2}"
-
- if [ -n "${header}" ]; then
- printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2;
- else
- printf "\033[0;33m%s\n\033[0m" "${1}" >&2;
- fi
-}
-
function deployExists() {
local namespace="${1}"
local deploy="${2}"
@@ -236,6 +216,7 @@ HELM_CMD=$(cat << EOF
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=tls-cert \
--set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10"
+ --set nginx-ingress.controller.service.enableHttp=false \
--set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \
--set nginx-ingress.controller.resources.requests.memory=440M \
--set nginx-ingress.controller.replicaCount=2 \
@@ -328,80 +309,3 @@ function add_license() {
puts "License added";
'
}
-
-function get_job_id() {
- local job_name="${1}"
- local query_string="${2:+&${2}}"
-
- local max_page=3
- local page=1
-
- while true; do
- local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}"
- echoinfo "GET ${url}"
-
- local job_id
- job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
- [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
-
- let "page++"
- done
-
- if [[ "${job_id}" == "" ]]; then
- echoerr "The '${job_name}' job ID couldn't be retrieved!"
- else
- echoinfo "The '${job_name}' job ID is ${job_id}"
- echo "${job_id}"
- fi
-}
-
-function play_job() {
- local job_name="${1}"
- local job_id
- job_id=$(get_job_id "${job_name}" "scope=manual");
- if [ -z "${job_id}" ]; then return; fi
-
- local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play"
- echoinfo "POST ${url}"
-
- local job_url
- job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url")
- echoinfo "Manual job '${job_name}' started at: ${job_url}"
-}
-
-function wait_for_job_to_be_done() {
- local job_name="${1}"
- local query_string="${2}"
- local job_id
- job_id=$(get_job_id "${job_name}" "${query_string}")
- if [ -z "${job_id}" ]; then return; fi
-
- echoinfo "Waiting for the '${job_name}' job to finish..."
-
- local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}"
- echoinfo "GET ${url}"
-
- # In case the job hasn't finished yet. Keep trying until the job times out.
- local interval=30
- local elapsed_seconds=0
- while true; do
- local job_status
- job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g)
- [[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break
-
- printf "."
- let "elapsed_seconds+=interval"
- sleep ${interval}
- done
-
- local elapsed_minutes=$((elapsed_seconds / 60))
- echoinfo "Waited '${job_name}' for ${elapsed_minutes} minutes."
-
- if [[ "${job_status}" == "failed" ]]; then
- echoerr "The '${job_name}' failed."
- elif [[ "${job_status}" == "manual" ]]; then
- echoinfo "The '${job_name}' is manual."
- else
- echoinfo "The '${job_name}' passed."
- fi
-}
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 2d2ba115563..4a6567b8a62 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -1,4 +1,4 @@
-retry() {
+function retry() {
if eval "$@"; then
return 0
fi
@@ -13,15 +13,15 @@ retry() {
return 1
}
-setup_db_user_only() {
+function setup_db_user_only() {
if [ "$GITLAB_DATABASE" = "postgresql" ]; then
- . scripts/create_postgres_user.sh
+ source scripts/create_postgres_user.sh
else
- . scripts/create_mysql_user.sh
+ source scripts/create_mysql_user.sh
fi
}
-setup_db() {
+function setup_db() {
setup_db_user_only
bundle exec rake db:drop db:create db:schema:load db:migrate
@@ -30,3 +30,129 @@ setup_db() {
bundle exec rake add_limits_mysql
fi
}
+
+function install_api_client_dependencies_with_apk() {
+ apk add --update openssl curl jq
+}
+
+function install_api_client_dependencies_with_apt() {
+ apt update && apt install jq -y
+}
+
+function install_gitlab_gem() {
+ gem install gitlab --no-document
+}
+
+function echoerr() {
+ local header="${2}"
+
+ if [ -n "${header}" ]; then
+ printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2;
+ else
+ printf "\033[0;31m%s\n\033[0m" "${1}" >&2;
+ fi
+}
+
+function echoinfo() {
+ local header="${2}"
+
+ if [ -n "${header}" ]; then
+ printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2;
+ else
+ printf "\033[0;33m%s\n\033[0m" "${1}" >&2;
+ fi
+}
+
+function get_job_id() {
+ local job_name="${1}"
+ local query_string="${2:+&${2}}"
+ local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}"
+ if [ -z "${api_token}" ]; then
+ echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN."
+ return
+ fi
+
+ local max_page=3
+ local page=1
+
+ while true; do
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}"
+ echoinfo "GET ${url}"
+
+ local job_id
+ job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
+ [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
+
+ let "page++"
+ done
+
+ if [[ "${job_id}" == "" ]]; then
+ echoerr "The '${job_name}' job ID couldn't be retrieved!"
+ else
+ echoinfo "The '${job_name}' job ID is ${job_id}"
+ echo "${job_id}"
+ fi
+}
+
+function play_job() {
+ local job_name="${1}"
+ local job_id
+ job_id=$(get_job_id "${job_name}" "scope=manual");
+ if [ -z "${job_id}" ]; then return; fi
+
+ local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}"
+ if [ -z "${api_token}" ]; then
+ echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN."
+ return
+ fi
+
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play"
+ echoinfo "POST ${url}"
+
+ local job_url
+ job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq ".web_url")
+ echoinfo "Manual job '${job_name}' started at: ${job_url}"
+}
+
+function wait_for_job_to_be_done() {
+ local job_name="${1}"
+ local query_string="${2}"
+ local job_id
+ job_id=$(get_job_id "${job_name}" "${query_string}")
+ if [ -z "${job_id}" ]; then return; fi
+
+ local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}"
+ if [ -z "${api_token}" ]; then
+ echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN."
+ return
+ fi
+
+ echoinfo "Waiting for the '${job_name}' job to finish..."
+
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}"
+ echoinfo "GET ${url}"
+
+ # In case the job hasn't finished yet. Keep trying until the job times out.
+ local interval=30
+ local elapsed_seconds=0
+ while true; do
+ local job_status
+ job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq ".status" | sed -e s/\"//g)
+ [[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break
+
+ printf "."
+ let "elapsed_seconds+=interval"
+ sleep ${interval}
+ done
+
+ local elapsed_minutes=$((elapsed_seconds / 60))
+ echoinfo "Waited '${job_name}' for ${elapsed_minutes} minutes."
+
+ if [[ "${job_status}" == "failed" ]]; then
+ echoerr "The '${job_name}' failed."
+ elif [[ "${job_status}" == "manual" ]]; then
+ echoinfo "The '${job_name}' is manual."
+ else
+ echoinfo "The '${job_name}' passed."
+ fi
+}
diff --git a/security.txt b/security.txt
new file mode 100644
index 00000000000..f7adb43fda6
--- /dev/null
+++ b/security.txt
@@ -0,0 +1,6 @@
+Contact: security@gitlab.com
+Acknowledgments: https://about.gitlab.com/security/vulnerability-acknowledgements/
+Preferred-Languages: en
+Canonical: https://about.gitlab.com/security/disclosure/
+Policy: https://hackerone.com/gitlab
+Hiring: https://about.gitlab.com/jobs/
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index cd1a01f8acc..70b34f071c8 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -145,4 +145,66 @@ describe Projects::Clusters::ApplicationsController do
it_behaves_like 'a secure endpoint'
end
end
+
+ describe 'DELETE destroy' do
+ subject do
+ delete :destroy, params: params.merge(namespace_id: project.namespace, project_id: project)
+ end
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id } }
+ let(:worker_class) { Clusters::Applications::UninstallWorker }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(worker_class).to receive(:perform_async).with(application.name, application.id).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_prometheus).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(worker_class).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 75158f2e8e0..cf23d937037 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -342,11 +342,9 @@ describe Projects::EnvironmentsController do
end
context 'when environment has no metrics' do
- before do
- expect(environment).to receive(:metrics).and_return(nil)
- end
-
it 'returns a metrics page' do
+ expect(environment).not_to receive(:metrics)
+
get :metrics, params: environment_params
expect(response).to be_ok
@@ -354,6 +352,8 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
+ expect(environment).to receive(:metrics).and_return(nil)
+
get :metrics, params: environment_params(format: :json)
expect(response).to have_gitlab_http_status(204)
@@ -461,6 +461,120 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'metrics_dashboard' do
+ context 'when prometheus endpoint is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_use_prometheus_endpoint: false)
+ end
+
+ it 'responds with status code 403' do
+ get :metrics_dashboard, params: environment_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ shared_examples_for '200 response' do |contains_all_dashboards: false|
+ let(:expected_keys) { %w(dashboard status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns a json representation of the environment dashboard' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ expect(json_response['dashboard']).to be_an_instance_of(Hash)
+ end
+ end
+
+ shared_examples_for 'error response' do |status_code, contains_all_dashboards: false|
+ let(:expected_keys) { %w(message status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns an error response' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(status_code)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ end
+ end
+
+ shared_examples_for 'has all dashboards' do
+ it 'includes an index of all available dashboards' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(json_response.keys).to include('all_dashboards')
+ expect(json_response['all_dashboards']).to be_an_instance_of(Array)
+ expect(json_response['all_dashboards']).to all( include('path', 'default') )
+ end
+ end
+
+ context 'when multiple dashboards is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
+ end
+
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response'
+
+ context 'when the dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } }
+
+ it_behaves_like '200 response'
+ end
+ end
+
+ context 'when multiple dashboards is enabled' do
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+
+ context 'when a dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } }
+
+ context 'when the dashboard is available' do
+ let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } }
+ let(:project) { create(:project, :custom_repo, files: dashboard_file) }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'error response', :not_found, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+ end
+ end
+ end
+
describe 'GET #search' do
before do
create(:environment, name: 'staging', project: project)
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 02a392f23c2..aa9cd41ed19 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -11,15 +11,118 @@ describe Projects::Settings::OperationsController do
project.add_maintainer(user)
end
- context 'error tracking' do
- describe 'GET #show' do
- it 'renders show template' do
+ shared_examples 'PATCHable' do
+ let(:operations_update_service) { instance_double(::Projects::Operations::UpdateService) }
+ let(:operations_url) { project_settings_operations_url(project) }
+
+ let(:permitted_params) do
+ ActionController::Parameters.new(params).permit!
+ end
+
+ context 'format json' do
+ context 'when update succeeds' do
+ it 'returns success status' do
+ stub_operations_update_service_returning(status: :success)
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('status' => 'success')
+ expect(flash[:notice]).to eq('Your changes have been saved')
+ end
+ end
+
+ context 'when update fails' do
+ it 'returns error' do
+ stub_operations_update_service_returning(
+ status: :error,
+ message: 'error message'
+ )
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+ end
+
+ private
+
+ def stub_operations_update_service_returning(return_value = {})
+ expect(::Projects::Operations::UpdateService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(operations_update_service)
+ expect(operations_update_service).to receive(:execute)
+ .and_return(return_value)
+ end
+ end
+
+ describe 'GET #show' do
+ it 'renders show template' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
get :show, params: project_params(project)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ patch :update, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
+ patch :update, params: project_params(project)
+
+ expect(response).to redirect_to(new_user_session_path)
end
+ end
+ end
+ context 'error tracking' do
+ describe 'GET #show' do
context 'with existing setting' do
let!(:error_tracking_setting) do
create(:project_error_tracking_setting, project: project)
@@ -40,37 +143,10 @@ describe Projects::Settings::OperationsController do
expect(controller.helpers.error_tracking_setting).to be_new_record
end
end
-
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
-
- it 'renders 404' do
- get :show, params: project_params(project)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- get :show, params: project_params(project)
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
end
describe 'PATCH #update' do
- let(:operations_update_service) { spy(:operations_update_service) }
- let(:operations_url) { project_settings_operations_url(project) }
-
- let(:error_tracking_params) do
+ let(:params) do
{
error_tracking_setting_attributes: {
enabled: '1',
@@ -86,79 +162,21 @@ describe Projects::Settings::OperationsController do
}
end
- let(:error_tracking_permitted) do
- ActionController::Parameters.new(error_tracking_params).permit!
- end
-
- context 'format json' do
- context 'when update succeeds' do
- before do
- stub_operations_update_service_returning(status: :success)
- end
-
- it 'returns success status' do
- patch :update,
- params: project_params(project, error_tracking_params),
- format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq('status' => 'success')
- expect(flash[:notice]).to eq('Your changes have been saved')
- end
- end
-
- context 'when update fails' do
- before do
- stub_operations_update_service_returning(
- status: :error,
- message: 'error message'
- )
- end
-
- it 'returns error' do
- patch :update,
- params: project_params(project, error_tracking_params),
- format: :json
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).not_to be_nil
- end
- end
- end
-
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
-
- it 'renders 404' do
- patch :update, params: project_params(project)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- patch :update, params: project_params(project)
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
+ it_behaves_like 'PATCHable'
end
+ end
- private
+ context 'metrics dashboard setting' do
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'https://gitlab.com'
+ }
+ }
+ end
- def stub_operations_update_service_returning(return_value = {})
- expect(::Projects::Operations::UpdateService)
- .to receive(:new).with(project, user, error_tracking_permitted)
- .and_return(operations_update_service)
- expect(operations_update_service).to receive(:execute)
- .and_return(return_value)
+ it_behaves_like 'PATCHable'
end
end
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index e0a6fc52ee9..f2e0b5e5c1d 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -19,6 +19,18 @@ describe Projects::WikisController do
destroy_page(wiki_title)
end
+ describe 'GET #pages' do
+ subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
+
+ it 'does not load the pages content' do
+ expect(controller).to receive(:load_wiki).and_return(project_wiki)
+
+ expect(project_wiki).to receive(:list_pages).twice.and_call_original
+
+ subject
+ end
+ end
+
describe 'GET #show' do
render_views
@@ -28,9 +40,9 @@ describe Projects::WikisController do
expect(controller).to receive(:load_wiki).and_return(project_wiki)
# empty? call
- expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
+ expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original
# Sidebar entries
- expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
+ expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
subject
@@ -104,7 +116,7 @@ describe Projects::WikisController do
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.pages.first))
+ expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
end
end
@@ -138,7 +150,7 @@ describe Projects::WikisController do
allow(controller).to receive(:valid_encoding?).and_return(false)
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.pages.first))
+ expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
end
end
@@ -148,7 +160,7 @@ describe Projects::WikisController do
it 'updates the page' do
subject
- wiki_page = project_wiki.pages.first
+ wiki_page = project_wiki.list_pages(load_content: true).first
expect(wiki_page.title).to eq new_title
expect(wiki_page.content).to eq new_content
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 9c60f0fcd4d..4634d1d4bb3 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -11,6 +11,30 @@ describe SearchController do
sign_in(user)
end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ set(:project) { create(:project, :public, :repository, :wiki_repo) }
+
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
+ end
+
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+
+ expect(subject).to render_template("search/results/#{partial}")
+ end
+ end
+ end
+
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index fe56ac5b71d..d78f01828d7 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -6,6 +6,11 @@ FactoryBot.define do
status(-2)
end
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
trait :installable do
status 0
end
@@ -30,17 +35,21 @@ FactoryBot.define do
status 5
end
- trait :errored do
- status(-1)
+ trait :update_errored do
+ status(6)
status_reason 'something went wrong'
end
- trait :update_errored do
- status(6)
+ trait :uninstalling do
+ status 7
+ end
+
+ trait :uninstall_errored do
+ status(8)
status_reason 'something went wrong'
end
- trait :timeouted do
+ trait :timed_out do
installing
updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago }
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 011c98599a3..db438ad32d3 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :deployment, class: Deployment do
- sha '97de212e80737a608d939f648d959671fb0a0142'
+ sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
ref 'master'
tag false
user nil
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index b74f72f2bd3..db8384877b0 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -45,6 +45,10 @@ nNp/xedE1YxutQ==
remove_at { 1.day.from_now }
end
+ trait :should_be_removed do
+ remove_at { 1.day.ago }
+ end
+
trait :unverified do
verified_at nil
end
diff --git a/spec/factories/project_metrics_settings.rb b/spec/factories/project_metrics_settings.rb
new file mode 100644
index 00000000000..234753f9b87
--- /dev/null
+++ b/spec/factories/project_metrics_settings.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_metrics_setting, class: ProjectMetricsSetting do
+ project
+ external_dashboard_url 'https://grafana.com'
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 04f39b807d7..f9950b5b03f 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -230,6 +230,13 @@ describe 'Admin updates settings' do
expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel'
end
+
+ it 'defaults Deployment events to false for chat notification template settings' do
+ first(:link, 'Service Templates').click
+ click_link 'Slack notifications'
+
+ expect(find_field('Deployment')).not_to be_checked
+ end
end
context 'CI/CD page' do
@@ -368,15 +375,50 @@ describe 'Admin updates settings' do
expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy
expect(page).to have_content "Application settings saved successfully"
end
+
+ context 'When pages_auto_ssl is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Change Pages Let's Encrypt settings" do
+ page.within('.as-pages') do
+ fill_in 'Email', with: 'my@test.example.com'
+ check "I have read and agree to the Let's Encrypt Terms of Service"
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.lets_encrypt_notification_email).to eq 'my@test.example.com'
+ expect(Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted).to eq true
+ end
+ end
+
+ context 'When pages_auto_ssl is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Doesn't show Let's Encrypt options" do
+ page.within('.as-pages') do
+ expect(page).not_to have_content('Email')
+ end
+ end
+ end
end
def check_all_events
page.check('Active')
page.check('Push')
- page.check('Tag push')
- page.check('Note')
page.check('Issue')
+ page.check('Confidential issue')
page.check('Merge request')
+ page.check('Note')
+ page.check('Confidential note')
+ page.check('Tag push')
page.check('Pipeline')
+ page.check('Wiki page')
+ page.check('Deployment')
end
end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 8eaccfc0949..cc04798248c 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -1,6 +1,6 @@
require "rails_helper"
-describe "Jira", :js do
+describe "Jira", :js, :quarantine do
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 224375daf71..9cf04fe13b4 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -936,8 +936,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
find('.js-cancel-job').click
end
- it 'loads the page and shows all needed controls' do
- expect(page).to have_content 'Retry'
+ it 'loads the page and shows no controls' do
+ expect(page).not_to have_content 'Retry'
end
end
end
@@ -946,7 +946,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Job from project", :js do
before do
job.run!
- job.cancel!
+ job.drop!(:script_failure)
visit project_job_path(project, job)
wait_for_requests
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cf334e1e4da..3825468cf71 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -115,11 +115,11 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the running build and shows retry button' do
+ it 'cancels the running build and does not show retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
- expect(page).to have_css('.js-icon-retry')
+ expect(page).not_to have_css('.js-icon-retry')
end
end
end
@@ -133,11 +133,11 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the preparing build and shows retry button' do
+ it 'cancels the preparing build and does not show retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
- expect(page).to have_css('.js-icon-retry')
+ expect(page).not_to have_css('.js-icon-retry')
end
end
end
@@ -331,11 +331,9 @@ describe 'Pipeline', :js do
merge_request.all_pipelines.last
end
- before do
+ it 'shows the pipeline information' do
visit_pipeline
- end
- it 'shows the pipeline information' do
within '.pipeline-info' do
expect(page).to have_content("#{pipeline.statuses.count} jobs " \
"for !#{merge_request.iid} " \
@@ -347,6 +345,21 @@ describe 'Pipeline', :js do
end
end
+ context 'when source branch does not exist' do
+ before do
+ project.repository.rm_branch(user, merge_request.source_branch)
+ end
+
+ it 'does not link to the source branch commit path' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_link(merge_request.source_branch)
+ expect(page).to have_content(merge_request.source_branch)
+ end
+ end
+ end
+
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
@@ -386,11 +399,11 @@ describe 'Pipeline', :js do
before do
pipeline.update(user: user)
-
- visit_pipeline
end
it 'shows the pipeline information' do
+ visit_pipeline
+
within '.pipeline-info' do
expect(page).to have_content("#{pipeline.statuses.count} jobs " \
"for !#{merge_request.iid} " \
@@ -405,6 +418,21 @@ describe 'Pipeline', :js do
end
end
+ context 'when target branch does not exist' do
+ before do
+ project.repository.rm_branch(user, merge_request.target_branch)
+ end
+
+ it 'does not link to the target branch commit path' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_link(merge_request.target_branch)
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
+ end
+
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 0aff916ec83..0dbff5a2701 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Branches', :js do
+ include ProtectedBranchHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
@@ -150,27 +152,11 @@ describe 'Protected Branches', :js do
end
describe "access control" do
- include_examples "protected branches > access control > CE"
- end
- end
-
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
-
- def set_defaults
- find(".js-allowed-to-merge").click
- within('.qa-allowed-to-merge-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
- find(".js-allowed-to-push").click
- within('.qa-allowed-to-push-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
+ include_examples "protected branches > access control > CE"
end
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c8e92cd1c07..652542b1719 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Tags', :js do
+ include ProtectedTagHelpers
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
@@ -8,13 +10,6 @@ describe 'Protected Tags', :js do
sign_in(user)
end
- def set_protected_tag_name(tag_name)
- find(".js-protected-tag-select").click
- find(".dropdown-input-field").set(tag_name)
- click_on("Create wildcard #{tag_name}")
- find('.protected-tags-dropdown .dropdown-menu', visible: false)
- end
-
describe "explicit protected tags" do
it "allows creating explicit protected tags" do
visit project_protected_tags_path(project)
@@ -92,6 +87,10 @@ describe 'Protected Tags', :js do
end
describe "access control" do
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
+
include_examples "protected tags > access control > CE"
end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 7225ca65492..6d4facd0649 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -14,22 +14,36 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
- it 'finds a page' do
- find('.js-search-project-dropdown').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('.js-search-project-dropdown').click
- page.within('.project-filter') do
- click_link(project.full_name)
- end
+ page.within('.project-filter') do
+ click_link(project.full_name)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').click
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
- fill_in('dashboard_search', with: 'content')
- find('.btn-search').click
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ end
+ end
+ end
- page.within('.search-filter') do
- click_link('Wiki')
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
end
+ end
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'test_wiki' }
end
end
end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 9da07a0b253..695175689b9 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -36,7 +36,8 @@
"external_hostname": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] },
- "update_available": { "type": ["boolean", "null"] }
+ "update_available": { "type": ["boolean", "null"] },
+ "can_uninstall": { "type": "boolean" }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
new file mode 100644
index 00000000000..638ecbcc11f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
@@ -0,0 +1,36 @@
+dashboard: 'Test Dashboard'
+priority: 1
+panel_groups:
+- group: Group A
+ priority: 1
+ panels:
+ - title: "Super Chart A1"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_a1
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
+ - title: "Super Chart A2"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 2
+ metrics:
+ - id: metric_a2
+ query_range: 'query'
+ label: Legend Label
+ unit: unit
+- group: Group B
+ priority: 10
+ panels:
+ - title: "Super Chart B"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_b
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
new file mode 100644
index 00000000000..1ee1205e29a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": ["dashboard", "priority", "panel_groups"],
+ "properties": {
+ "dashboard": { "type": "string" },
+ "priority": { "type": "number" },
+ "panel_groups": {
+ "type": "array",
+ "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
new file mode 100644
index 00000000000..2d0af57ec2c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "unit",
+ "label"
+ ],
+ "oneOf": [
+ { "required": ["query"] },
+ { "required": ["query_range"] }
+ ],
+ "properties": {
+ "id": { "type": "string" },
+ "query_range": { "type": "string" },
+ "query": { "type": "string" },
+ "unit": { "type": "string" },
+ "label": { "type": "string" },
+ "track": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
new file mode 100644
index 00000000000..d7a390adcdc
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "required": [
+ "group",
+ "priority",
+ "panels"
+ ],
+ "properties": {
+ "group": { "type": "string" },
+ "priority": { "type": "number" },
+ "panels": {
+ "type": "array",
+ "items": { "$ref": "panels.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
new file mode 100644
index 00000000000..1548daacd64
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "title",
+ "y_label",
+ "weight",
+ "metrics"
+ ],
+ "properties": {
+ "title": { "type": "string" },
+ "type": { "type": "string" },
+ "y_label": { "type": "string" },
+ "weight": { "type": "number" },
+ "metrics": {
+ "type": "array",
+ "items": { "$ref": "metrics.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 33a35069004..a61103397eb 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,16 +1,13 @@
import Clusters from '~/clusters/clusters_bundle';
-import {
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- APPLICATION_STATUS,
- INGRESS_DOMAIN_SUFFIX,
-} from '~/clusters/constants';
+import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX } from '~/clusters/constants';
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';
+const { INSTALLING, INSTALLABLE, INSTALLED } = APPLICATION_STATUS;
+
describe('Clusters', () => {
setTestTimeout(1000);
@@ -93,7 +90,7 @@ describe('Clusters', () => {
it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' },
+ helm: { status: INSTALLABLE, title: 'Helm Tiller' },
});
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
@@ -105,11 +102,11 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
},
);
@@ -125,13 +122,13 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: INSTALLABLE, title: 'Ingress' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: INSTALLED, title: 'Ingress' },
},
);
@@ -218,11 +215,11 @@ describe('Clusters', () => {
it('tries to install helm', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications.helm.status = INSTALLABLE;
cluster.installApplication({ id: 'helm' });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
});
@@ -230,11 +227,11 @@ describe('Clusters', () => {
it('tries to install ingress', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
+ cluster.store.state.applications.ingress.status = INSTALLABLE;
cluster.installApplication({ id: 'ingress' });
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.ingress.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
});
@@ -242,11 +239,11 @@ describe('Clusters', () => {
it('tries to install runner', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
+ cluster.store.state.applications.runner.status = INSTALLABLE;
cluster.installApplication({ id: 'runner' });
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.runner.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
});
@@ -254,13 +251,12 @@ describe('Clusters', () => {
it('tries to install jupyter', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
cluster.installApplication({
id: 'jupyter',
params: { hostname: cluster.store.state.applications.jupyter.hostname },
});
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED);
+ cluster.store.state.applications.jupyter.status = INSTALLABLE;
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', {
hostname: cluster.store.state.applications.jupyter.hostname,
@@ -272,16 +268,18 @@ describe('Clusters', () => {
.spyOn(cluster.service, 'installApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications.helm.status = INSTALLABLE;
const promise = cluster.installApplication({ id: 'helm' });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalled();
return promise.then(() => {
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
+ expect(cluster.store.state.applications.helm.installFailed).toBe(true);
+
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
@@ -315,18 +313,16 @@ describe('Clusters', () => {
});
describe('toggleIngressDomainHelpText', () => {
- const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
let ingressPreviousState;
let ingressNewState;
beforeEach(() => {
- ingressPreviousState = { status: INSTALLABLE };
- ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' };
+ ingressPreviousState = { externalIp: null };
+ ingressNewState = { externalIp: '127.0.0.1' };
});
- describe(`when ingress application new status is ${INSTALLED}`, () => {
+ describe(`when ingress have an external ip assigned`, () => {
beforeEach(() => {
- ingressNewState.status = INSTALLED;
cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
});
@@ -341,31 +337,11 @@ describe('Clusters', () => {
});
});
- describe(`when ingress application new status is different from ${INSTALLED}`, () => {
+ describe(`when ingress does not have an external ip assigned`, () => {
it('hides custom domain help text', () => {
- ingressNewState.status = NOT_INSTALLABLE;
- cluster.ingressDomainHelpText.classList.remove('hide');
-
- cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
-
- expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
- });
- });
-
- describe('when ingress application new status and old status are the same', () => {
- it('does not display custom domain help text', () => {
- ingressPreviousState.status = INSTALLED;
- ingressNewState.status = ingressPreviousState.status;
-
- cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
-
- 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', () => {
+ ingressPreviousState.externalIp = '127.0.0.1';
ingressNewState.externalIp = null;
+ cluster.ingressDomainHelpText.classList.remove('hide');
cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 038d2be9e98..17273b7d5b1 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import eventHub from '~/clusters/event_hub';
-import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants';
+import { APPLICATION_STATUS } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
@@ -80,17 +80,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(false);
});
- it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.SCHEDULED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -102,18 +91,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has loading "Installing" when REQUEST_SUBMITTED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_SUBMITTED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has disabled "Installed" when application is installed and not uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -139,10 +116,11 @@ describe('Application Row', () => {
expect(installBtn).toBe(null);
});
- it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
+ it('has enabled "Install" when install fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -154,7 +132,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -246,15 +223,15 @@ describe('Application Row', () => {
expect(upgradeBtn.innerHTML).toContain('Upgrade');
});
- it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => {
+ it('has enabled "Retry update" when update process fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
- expect(vm.upgradeFailed).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Retry update');
});
@@ -274,7 +251,8 @@ describe('Application Row', () => {
jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ upgradeAvailable: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
@@ -303,7 +281,8 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const failureMessage = vm.$el.querySelector(
'.js-cluster-application-upgrade-failure-message',
@@ -314,6 +293,21 @@ describe('Application Row', () => {
'Update failed. Please check the logs and try again.',
);
});
+
+ it('displays a success toast message if application upgrade was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ updateSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.updateSuccessful = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
+ });
+ });
});
describe('Version', () => {
@@ -321,7 +315,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -337,7 +332,8 @@ describe('Application Row', () => {
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
chartRepo,
version,
});
@@ -351,7 +347,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -367,7 +364,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: null,
- requestStatus: null,
});
const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message',
@@ -376,12 +372,13 @@ describe('Application Row', () => {
expect(generalErrorMessage).toBeNull();
});
- it('shows status reason when APPLICATION_STATUS.ERROR', () => {
+ it('shows status reason when install fails', () => {
const statusReason = 'We broke it 0.0';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.ERROR,
statusReason,
+ installFailed: true,
});
const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message',
@@ -402,7 +399,7 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
+ installFailed: true,
requestReason,
});
const generalErrorMessage = vm.$el.querySelector(
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
new file mode 100644
index 00000000000..e74b7910572
--- /dev/null
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -0,0 +1,134 @@
+import transitionApplicationState from '~/clusters/services/application_state_machine';
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '~/clusters/constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+} = APPLICATION_STATUS;
+
+const NO_EFFECTS = 'no effects';
+
+describe('applicationStateMachine', () => {
+ const noEffectsToEmptyObject = effects => (typeof effects === 'string' ? {} : effects);
+
+ describe(`current state is ${NO_STATUS}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NO_STATUS,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${NOT_INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NOT_INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLED}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLED,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UPDATING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true, updateAcknowledged: false }}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UPDATING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index b4d1bb710e0..1e896af1c7d 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
- requestStatus: null,
requestReason: null,
};
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index c0e8b737ea2..a20e0439555 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -32,15 +32,6 @@ describe('Clusters Store', () => {
});
describe('updateAppProperty', () => {
- it('should store new request status', () => {
- expect(store.state.applications.helm.requestStatus).toEqual(null);
-
- const newStatus = APPLICATION_STATUS.INSTALLING;
- store.updateAppProperty('helm', 'requestStatus', newStatus);
-
- expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
- });
-
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
@@ -68,80 +59,90 @@ describe('Clusters Store', () => {
title: 'Helm Tiller',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
- requestStatus: null,
requestReason: null,
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
ingress: {
title: 'Ingress',
- status: mockResponseData.applications[1].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
- requestStatus: null,
requestReason: null,
externalIp: null,
externalHostname: null,
installed: false,
+ installFailed: true,
+ uninstallable: false,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
- requestStatus: null,
requestReason: null,
version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
installed: false,
+ installFailed: false,
+ updateAcknowledged: true,
+ updateFailed: false,
+ updateSuccessful: false,
+ uninstallable: false,
},
prometheus: {
title: 'Prometheus',
- status: mockResponseData.applications[3].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
- requestStatus: null,
requestReason: null,
installed: false,
+ installFailed: true,
+ uninstallable: false,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
- requestStatus: null,
requestReason: null,
hostname: '',
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
- requestStatus: null,
requestReason: null,
hostname: null,
isEditingHostName: false,
externalIp: null,
externalHostname: null,
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
cert_manager: {
title: 'Cert-Manager',
- status: mockResponseData.applications[6].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
- requestStatus: null,
requestReason: null,
email: mockResponseData.applications[6].email,
installed: false,
+ uninstallable: false,
},
},
});
});
- describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', () => {
+ describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
- mockResponseData.applications[runnerAppIndex].status = APPLICATION_STATUS.INSTALLED;
+ mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData);
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index fba7c41df94..3886853f3c1 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -209,6 +209,38 @@ describe('GfmAutoComplete', () => {
});
});
+ describe('DefaultOptions.highlighter', () => {
+ beforeEach(() => {
+ atwhoInstance = { setting: {} };
+ });
+
+ it('should return li if no query is given', () => {
+ const liTag = '<li></li>';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag);
+
+ expect(highlightedTag).toEqual(liTag);
+ });
+
+ it('should highlight search query in li element', () => {
+ const liTag = '<li><img src="" />string</li>';
+ const query = 's';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag, query);
+
+ expect(highlightedTag).toEqual('<li><img src="" /> <strong>s</strong>tring </li>');
+ });
+
+ it('should highlight search query with special char in li element', () => {
+ const liTag = '<li><img src="" />te.st</li>';
+ const query = '.';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag, query);
+
+ expect(highlightedTag).toEqual('<li><img src="" /> te<strong>.</strong>st </li>');
+ });
+ });
+
describe('isLoading', () => {
it('should be true with loading data object item', () => {
expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..17a998d0174
--- /dev/null
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,185 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
+import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportProjectsTable', () => {
+ let vm;
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const importedProject = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'started',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchJobs: jest.fn(),
+ fetchRepos: jest.fn(actions.requestRepos),
+ fetchImport: jest.fn(actions.requestImport),
+ });
+
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+
+ const component = mount(importProjectsTable, {
+ localVue,
+ store,
+ propsData: {
+ providerTitle,
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a loading icon whilst repos are loading', () =>
+ vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
+ }));
+
+ it('renders a table with imported projects and provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).not.toBeNull();
+ expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
+ `From ${providerTitle}`,
+ );
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+ });
+ });
+
+ it('renders an empty state if there are no imported projects or provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).toBeNull();
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ });
+ });
+
+ it('shows loading spinner when bulk import button is clicked', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$el.querySelector('.js-import-all').click();
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-import-all .js-loading-button-icon')).not.toBeNull();
+ });
+ });
+
+ it('imports provider repos if bulk import button is clicked', () => {
+ mountComponent();
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$store.dispatch('receiveImportSuccess', { importedProject, repoId: providerRepo.id });
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
+ });
+ });
+
+ it('polls to update the status of imported projects', () => {
+ const updatedProjects = [
+ {
+ id: importedProject.id,
+ importStatus: 'finished',
+ },
+ ];
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ const statusObject = STATUS_MAP[importedProject.importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+
+ vm.$store.dispatch('receiveJobsSuccess', updatedProjects);
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
index 7dac7e9ccc1..f95acc1edd7 100644
--- a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
+++ b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
@@ -1,5 +1,6 @@
-import Vue from 'vue';
+import Vuex from 'vuex';
import createStore from '~/import_projects/store';
+import { createLocalVue, mount } from '@vue/test-utils';
import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import STATUS_MAP from '~/import_projects/constants';
@@ -13,27 +14,33 @@ describe('ImportedProjectTableRow', () => {
importSource: 'importSource',
};
- function createComponent() {
- const ImportedProjectTableRow = Vue.extend(importedProjectTableRow);
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
- const store = createStore();
- return new ImportedProjectTableRow({
- store,
+ const component = mount(importedProjectTableRow, {
+ localVue,
+ store: createStore(),
propsData: {
project: {
...project,
},
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
afterEach(() => {
vm.$destroy();
});
it('renders an imported project table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[project.importStatus];
diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index 4d2bacd2ad0..02c786d8d0b 100644
--- a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,14 +1,15 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('ProviderRepoTableRow', () => {
- let store;
let vm;
+ const fetchImport = jest.fn((context, data) => actions.requestImport(context, data));
+ const importPath = '/import-path';
+ const defaultTargetNamespace = 'user';
+ const ciCdOnly = true;
const repo = {
id: 10,
sanitizedName: 'sanitizedName',
@@ -16,21 +17,42 @@ describe('ProviderRepoTableRow', () => {
providerLink: 'providerLink',
};
- function createComponent() {
- const ProviderRepoTableRow = Vue.extend(providerRepoTableRow);
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchImport,
+ });
- return new ProviderRepoTableRow({
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+ store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+
+ const component = mount(providerRepoTableRow, {
+ localVue,
store,
propsData: {
- repo: {
- ...repo,
- },
+ repo,
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
beforeEach(() => {
- store = createStore();
+ vm = mountComponent();
});
afterEach(() => {
@@ -38,8 +60,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a provider repo table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[STATUSES.NONE];
@@ -55,8 +75,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a select2 namespace select', () => {
- vm = createComponent();
-
const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
expect(dropdownTrigger).not.toBeNull();
@@ -67,30 +85,20 @@ describe('ProviderRepoTableRow', () => {
expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
});
- it('imports repo when clicking import button', done => {
- const importPath = '/import-path';
- const defaultTargetNamespace = 'user';
- const ciCdOnly = true;
- const mock = new MockAdapter(axios);
-
- store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
- mock.onPost(importPath).replyOnce(200);
- spyOn(store, 'dispatch').and.returnValue(new Promise(() => {}));
-
- vm = createComponent();
-
+ it('imports repo when clicking import button', () => {
vm.$el.querySelector('.js-import-button').click();
- setTimeoutPromise()
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchImport', {
- repo,
- newName: repo.sanitizedName,
- targetNamespace: defaultTargetNamespace,
- });
- })
- .then(() => mock.restore())
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick().then(() => {
+ const { calls } = fetchImport.mock;
+
+ // Not using .toBeCalledWith because it expects
+ // an unmatchable and undefined 3rd argument.
+ expect(calls.length).toBe(1);
+ expect(calls[0][1]).toEqual({
+ repo,
+ newName: repo.sanitizedName,
+ targetNamespace: defaultTargetNamespace,
+ });
+ });
});
});
diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 77850ee3283..6a7b90788dd 100644
--- a/spec/javascripts/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -27,8 +27,8 @@ import {
stopJobsPolling,
} from '~/import_projects/store/actions';
import state from '~/import_projects/store/state';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
describe('import_projects store actions', () => {
let localState;
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
new file mode 100644
index 00000000000..392c1b6533e
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -0,0 +1,139 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import '~/behaviors/markdown/render_gfm';
+import { SYSTEM_NOTE } from '~/notes/constants';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import NoteableNote from '~/notes/components/noteable_note.vue';
+import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import createStore from '~/notes/stores';
+import {
+ noteableDataMock,
+ discussionMock,
+ notesDataMock,
+} from '../../../javascripts/notes/mock_data';
+
+const localVue = createLocalVue();
+
+describe('DiscussionNotes', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ const store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ wrapper = mount(DiscussionNotes, {
+ localVue,
+ store,
+ propsData: {
+ discussion: discussionMock,
+ isExpanded: false,
+ shouldGroupReplies: false,
+ ...props,
+ },
+ scopedSlots: {
+ footer: '<p slot-scope="{ showReplies }">showReplies:{{showReplies}}</p>',
+ },
+ slots: {
+ 'avatar-badge': '<span class="avatar-badge-slot-content" />',
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('renders an element for each note in the discussion', () => {
+ createComponent();
+ const notesCount = discussionMock.notes.length;
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(notesCount);
+ });
+
+ it('renders one element if replies groupping is enabled', () => {
+ createComponent({ shouldGroupReplies: true });
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(1);
+ });
+
+ it('uses proper component to render each note type', () => {
+ const discussion = { ...discussionMock };
+ const notesData = [
+ // PlaceholderSystemNote
+ {
+ id: 1,
+ isPlaceholderNote: true,
+ placeholderType: SYSTEM_NOTE,
+ notes: [{ body: 'PlaceholderSystemNote' }],
+ },
+ // PlaceholderNote
+ {
+ id: 2,
+ isPlaceholderNote: true,
+ notes: [{ body: 'PlaceholderNote' }],
+ },
+ // SystemNote
+ {
+ id: 3,
+ system: true,
+ note: 'SystemNote',
+ },
+ // NoteableNote
+ discussion.notes[0],
+ ];
+ discussion.notes = notesData;
+ createComponent({ discussion });
+ const notes = wrapper.findAll('.notes > li');
+
+ expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true);
+ expect(notes.at(1).is(PlaceholderNote)).toBe(true);
+ expect(notes.at(2).is(SystemNote)).toBe(true);
+ expect(notes.at(3).is(NoteableNote)).toBe(true);
+ });
+
+ it('renders footer scoped slot with showReplies === true when expanded', () => {
+ createComponent({ isExpanded: true });
+ expect(wrapper.text()).toMatch('showReplies:true');
+ });
+
+ it('renders footer scoped slot with showReplies === false when collapsed', () => {
+ createComponent({ isExpanded: false });
+ expect(wrapper.text()).toMatch('showReplies:false');
+ });
+
+ it('passes down avatar-badge slot content', () => {
+ createComponent();
+ expect(wrapper.find('.avatar-badge-slot-content').exists()).toBe(true);
+ });
+ });
+
+ describe('componentData', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should return first note object for placeholder note', () => {
+ const data = {
+ isPlaceholderNote: true,
+ notes: [{ body: 'hello world!' }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data.notes[0]);
+ });
+
+ it('should return given note for nonplaceholder notes', () => {
+ const data = {
+ notes: [{ id: 12 }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 9eac75fac96..d1de98f4a15 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockAssigneesList } from 'spec/boards/mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockAssigneesList } from '../../../../javascripts/boards/mock_data';
const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
const Component = Vue.extend(IssueAssignees);
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..2e93ec412b9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import { mockMilestone } from '../../../../javascripts/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mount(Component, {
+ propsData: {
+ milestone,
+ },
+ sync: false,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let wrapper;
+ let vm;
+
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(false);
+ });
+
+ it('should return `true` when milestone start date is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(true);
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(false);
+ });
+
+ it('should return `true` when milestone due is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(true);
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
+ });
+
+ it('returns empty string when milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toBe('');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
index aa7d6ea2e34..4a8de5fc4f1 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
@@ -19,7 +19,9 @@ describe('Issue Warning Component', () => {
isLocked: true,
});
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
'This issue is locked. Only project members can comment.',
);
@@ -32,7 +34,9 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
'This is a confidential issue. Your comment will not be visible to the public.',
);
diff --git a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index 42198e92eea..e43d5301a50 100644
--- a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -1,7 +1,11 @@
import Vue from 'vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
import { mount, createLocalVue } from '@vue/test-utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import { defaultMilestone, defaultAssignees } from './related_issuable_mock_data';
+import {
+ defaultAssignees,
+ defaultMilestone,
+} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data';
describe('RelatedIssuableItem', () => {
let wrapper;
@@ -85,11 +89,11 @@ describe('RelatedIssuableItem', () => {
it('renders state title', () => {
const stateTitle = tokenState.attributes('data-original-title');
+ const formatedCreateDate = formatDate(props.createdAt);
expect(stateTitle).toContain('<span class="bold">Opened</span>');
- expect(stateTitle).toContain(
- '<span class="text-tertiary">Dec 1, 2018 12:00am GMT+0000</span>',
- );
+
+ expect(stateTitle).toContain(`<span class="text-tertiary">${formatedCreateDate}</span>`);
});
it('renders aria label', () => {
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 45f131194ca..eafff7f681e 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import createStore from '~/notes/stores';
-import { userDataMock } from '../../../notes/mock_data';
+import { userDataMock } from '../../../../javascripts/notes/mock_data';
describe('issue placeholder system note component', () => {
let store;
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
index 6013e85811a..976e38c15ee 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index adcb1c858aa..dc66150ab8d 100644
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import createStore from '~/notes/stores';
+import initMRPopovers from '~/mr_popover/index';
+
+jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
@@ -56,4 +59,8 @@ describe('system note component', () => {
it('removes wrapping paragraph from note HTML', () => {
expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
});
+
+ it('should initMRPopovers onMount', () => {
+ expect(initMRPopovers).toHaveBeenCalled();
+ });
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
index 6bff1521695..691ebe43d6b 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('collapsedCalendarIcon', () => {
let vm;
@@ -26,7 +26,7 @@ describe('collapsedCalendarIcon', () => {
});
it('should emit click event when container is clicked', () => {
- const click = jasmine.createSpy();
+ const click = jest.fn();
vm.$on('click', click);
vm.$el.click();
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index c507a97d37e..062ebfa01c9 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('collapsedGroupedDatePicker', () => {
let vm;
@@ -13,7 +13,7 @@ describe('collapsedGroupedDatePicker', () => {
describe('toggleCollapse events', () => {
beforeEach(done => {
- spyOn(vm, 'toggleSidebar');
+ jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {});
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
index 805ba7b9947..5e2bca6efc9 100644
--- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('sidebarDatePicker', () => {
let vm;
@@ -13,7 +13,7 @@ describe('sidebarDatePicker', () => {
});
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- const toggleCollapse = jasmine.createSpy();
+ const toggleCollapse = jest.fn();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
@@ -90,7 +90,7 @@ describe('sidebarDatePicker', () => {
});
it('should emit saveDate when remove button is clicked', () => {
- const saveDate = jasmine.createSpy();
+ const saveDate = jest.fn();
vm.$on('saveDate', saveDate);
vm.$el.querySelector('.value-content .btn-blank').click();
@@ -110,7 +110,7 @@ describe('sidebarDatePicker', () => {
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- const toggleCollapse = jasmine.createSpy();
+ const toggleCollapse = jest.fn();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.title .gutter-toggle').click();
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index c44b04009ca..6aee616c324 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -3,25 +3,35 @@ import Vue from 'vue';
import LabelsSelect from '~/labels_select';
import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
+import { mount } from '@vue/test-utils';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (config = mockConfig) => {
const Component = Vue.extend(baseComponent);
- return mountComponent(Component, config);
+ return mount(Component, {
+ propsData: config,
+ sync: false,
+ });
};
describe('BaseComponent', () => {
+ let wrapper;
let vm;
- beforeEach(() => {
- vm = createComponent();
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('computed', () => {
@@ -31,11 +41,9 @@ describe('BaseComponent', () => {
});
it('returns correct string when showCreate prop is `false`', () => {
- const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false });
- const vmNonEditable = createComponent(mockConfigNonEditable);
+ wrapper.setProps({ showCreate: false });
- expect(vmNonEditable.hiddenInputName).toBe('label_id[]');
- vmNonEditable.$destroy();
+ expect(vm.hiddenInputName).toBe('label_id[]');
});
});
@@ -45,11 +53,9 @@ describe('BaseComponent', () => {
});
it('return `Create group label` when `isProject` prop is false', () => {
- const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false });
- const vmGroup = createComponent(mockConfigGroup);
+ wrapper.setProps({ isProject: false });
- expect(vmGroup.createLabelTitle).toBe('Create group label');
- vmGroup.$destroy();
+ expect(vm.createLabelTitle).toBe('Create group label');
});
});
@@ -59,11 +65,9 @@ describe('BaseComponent', () => {
});
it('return `Manage group labels` when `isProject` prop is false', () => {
- const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false });
- const vmGroup = createComponent(mockConfigGroup);
+ wrapper.setProps({ isProject: false });
- expect(vmGroup.manageLabelsTitle).toBe('Manage group labels');
- vmGroup.$destroy();
+ expect(vm.manageLabelsTitle).toBe('Manage group labels');
});
});
});
@@ -71,7 +75,7 @@ describe('BaseComponent', () => {
describe('methods', () => {
describe('handleClick', () => {
it('emits onLabelClick event with label and list of labels as params', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleClick(mockLabels[0]);
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
@@ -80,7 +84,7 @@ describe('BaseComponent', () => {
describe('handleCollapsedValueClick', () => {
it('emits toggleCollapse event on component', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleCollapsedValueClick();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
@@ -89,7 +93,7 @@ describe('BaseComponent', () => {
describe('handleDropdownHidden', () => {
it('emits onDropdownClose event on component', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleDropdownHidden();
expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index 0689fc1cf1f..bb33dc6ea0f 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -2,9 +2,11 @@ import Vue from 'vue';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const componentConfig = Object.assign({}, mockConfig, {
fieldName: 'label_id[]',
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index b8f32f96332..1c25d42682c 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -2,9 +2,8 @@ import Vue from 'vue';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockSuggestedColors } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = headerTitle => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 3711e9dac8c..989901a0012 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -2,9 +2,8 @@ import Vue from 'vue';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
index 115e21e4f9f..c36a82e1a35 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownHeaderComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
index c30e619e76b..2fffb2e495e 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownSearchInputComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
index 6c84d2e167c..1616e657c81 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const createComponent = (canEdit = true) => {
const Component = Vue.extend(dropdownTitleComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 4d3de5e474d..517f2c01c46 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -2,9 +2,8 @@ import Vue from 'vue';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockLabels } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
@@ -72,7 +71,7 @@ describe('DropdownValueCollapsedComponent', () => {
describe('methods', () => {
describe('handleClick', () => {
it('emits onValueClick event on component', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleClick();
expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 35a9c300953..ec143fec5d9 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -3,9 +3,11 @@ import $ from 'jquery';
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (
labels = mockLabels,
diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
index c911a129173..5cf25ca6f81 100644
--- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('toggleSidebar', () => {
let vm;
@@ -23,7 +23,7 @@ describe('toggleSidebar', () => {
});
it('should emit toggle event when button clicked', () => {
- const toggle = jasmine.createSpy();
+ const toggle = jest.fn();
vm.$on('toggle', toggle);
vm.$el.click();
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 5f9c180cbb7..399a33dae75 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -4,104 +4,119 @@ describe Resolvers::IssuesResolver do
include GraphqlHelpers
let(:current_user) { create(:user) }
- set(:project) { create(:project) }
- set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
- set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
- set(:label1) { create(:label, project: project) }
- set(:label2) { create(:label, project: project) }
-
- before do
- project.add_developer(current_user)
- create(:label_link, label: label1, target: issue1)
- create(:label_link, label: label1, target: issue2)
- create(:label_link, label: label2, target: issue2)
- end
-
- describe '#resolve' do
- it 'finds all issues' do
- expect(resolve_issues).to contain_exactly(issue1, issue2)
- end
- it 'filters by state' do
- expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
- expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
+ context "with a project" do
+ set(:project) { create(:project) }
+ set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
+ set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
+ set(:label1) { create(:label, project: project) }
+ set(:label2) { create(:label, project: project) }
+
+ before do
+ project.add_developer(current_user)
+ create(:label_link, label: label1, target: issue1)
+ create(:label_link, label: label1, target: issue2)
+ create(:label_link, label: label2, target: issue2)
end
- it 'filters by labels' do
- expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
- expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
- end
+ describe '#resolve' do
+ it 'finds all issues' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
- describe 'filters by created_at' do
- it 'filters by created_before' do
- expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ it 'filters by state' do
+ expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
+ expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
end
- it 'filters by created_after' do
- expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ it 'filters by labels' do
+ expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
+ expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
end
- end
- describe 'filters by updated_at' do
- it 'filters by updated_before' do
- expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ describe 'filters by created_at' do
+ it 'filters by created_before' do
+ expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by created_after' do
+ expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- it 'filters by updated_after' do
- expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ describe 'filters by updated_at' do
+ it 'filters by updated_before' do
+ expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by updated_after' do
+ expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- end
- describe 'filters by closed_at' do
- let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+ describe 'filters by closed_at' do
+ let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
- it 'filters by closed_before' do
- expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ it 'filters by closed_before' do
+ expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ end
+
+ it 'filters by closed_after' do
+ expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- it 'filters by closed_after' do
- expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ it 'searches issues' do
+ expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
end
- end
- it 'searches issues' do
- expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
- end
+ it 'sort issues' do
+ expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
+ end
- it 'sort issues' do
- expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
- end
+ it 'returns issues user can see' do
+ project.add_guest(current_user)
- it 'returns issues user can see' do
- project.add_guest(current_user)
+ create(:issue, confidential: true)
- create(:issue, confidential: true)
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
- expect(resolve_issues).to contain_exactly(issue1, issue2)
- end
+ it 'finds a specific issue with iid' do
+ expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'finds a specific issue with iid' do
- expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
- end
+ it 'finds a specific issue with iids' do
+ expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'finds a specific issue with iids' do
- expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
- end
+ it 'finds multiple issues with iids' do
+ expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
+ .to contain_exactly(issue1, issue2)
+ end
- it 'finds multiple issues with iids' do
- expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
- .to contain_exactly(issue1, issue2)
- end
+ it 'finds only the issues within the project we are looking at' do
+ another_project = create(:project)
+ iids = [issue1, issue2].map(&:iid)
+
+ iids.each do |iid|
+ create(:issue, project: another_project, iid: iid)
+ end
- it 'finds only the issues within the project we are looking at' do
- another_project = create(:project)
- iids = [issue1, issue2].map(&:iid)
+ expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
+ end
+ end
+ end
- iids.each do |iid|
- create(:issue, project: another_project, iid: iid)
+ context "when passing a non existent, batch loaded project" do
+ let(:project) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
end
+ end
- expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
+ it "returns nil without breaking" do
+ expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
new file mode 100644
index 00000000000..3dd5b602aa2
--- /dev/null
+++ b/spec/graphql/types/group_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Group'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
+
+ it { expect(described_class.graphql_name).to eq('Group') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_group) }
+end
diff --git a/spec/graphql/types/namespace_type.rb b/spec/graphql/types/namespace_type.rb
new file mode 100644
index 00000000000..7cd6a79ae5d
--- /dev/null
+++ b/spec/graphql/types/namespace_type.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Namespace'] do
+ it { expect(described_class.graphql_name).to eq('Namespace') }
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 69e3ea8a4a9..b4626955816 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :echo, :metadata) }
+ it { is_expected.to have_graphql_fields(:project, :group, :echo, :metadata) }
describe 'project field' do
subject { described_class.fields['project'] }
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index e537e0e8afc..494b3b934a8 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -4,7 +4,7 @@ import Api from '~/api';
describe('Api', () => {
const dummyApiVersion = 'v3000';
- const dummyUrlRoot = 'http://host.invalid';
+ const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
@@ -32,6 +32,18 @@ describe('Api', () => {
expect(builtUrl).toEqual(expectedOutput);
});
+
+ [null, '', '/'].forEach(root => {
+ it(`works when relative_url_root is ${root}`, () => {
+ window.gon.relative_url_root = root;
+ const input = '/api/:version/foo/bar';
+ const expectedOutput = `/api/${dummyApiVersion}/foo/bar`;
+
+ const builtUrl = Api.buildUrl(input);
+
+ expect(builtUrl).toEqual(expectedOutput);
+ });
+ });
});
describe('group', () => {
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
index 68b4f261617..33210794ba1 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -1,5 +1,7 @@
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
-import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const bmprPath = `${FIXTURES_PATH}/blob/balsamiq/test.bmpr`;
describe('Balsamiq integration spec', () => {
let container;
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index acf87580777..6fa3890483c 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
import renderPDF from '~/blob/pdf';
-import testPDF from '../../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('PDF renderer', () => {
let viewer;
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
index 0bc9da5ad0f..f7f0ab83c21 100644
--- a/spec/javascripts/diffs/components/diff_discussions_spec.js
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -1,90 +1,103 @@
-import Vue from 'vue';
+import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import { createStore } from '~/mr_notes/stores';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
+const localVue = createLocalVue();
+
describe('DiffDiscussions', () => {
- let vm;
+ let store;
+ let wrapper;
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- function createComponent(props = {}) {
- const store = createStore();
-
- vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
- discussions: getDiscussionsMockData(),
- ...props,
- }).$mount();
- }
+ const createComponent = props => {
+ store = createStore();
+ wrapper = mount(localVue.extend(DiffDiscussions), {
+ store,
+ propsData: {
+ discussions: getDiscussionsMockData(),
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('template', () => {
it('should have notes list', () => {
createComponent();
- expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ expect(wrapper.find(NoteableDiscussion).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe(
+ discussionsMockData.notes.length,
+ );
});
});
describe('image commenting', () => {
+ const findDiffNotesToggle = () => wrapper.find('.js-diff-notes-toggle');
+
it('renders collapsible discussion button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain(
- 'diff-notes-collapse',
- );
+ expect(diffNotesToggle.exists()).toBe(true);
+ expect(diffNotesToggle.find(Icon).exists()).toBe(true);
+ expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true);
});
it('dispatches toggleDiscussion when clicking collapse button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ spyOn(wrapper.vm.$store, 'dispatch').and.stub();
+ const diffNotesToggle = findDiffNotesToggle();
+ diffNotesToggle.trigger('click');
- spyOn(vm.$store, 'dispatch').and.stub();
-
- vm.$el.querySelector('.js-diff-notes-toggle').click();
-
- expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
- discussionId: vm.discussions[0].id,
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ discussionId: discussionsMockData.id,
});
});
- it('renders expand button when discussion is collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
-
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1');
- expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain(
- 'btn-transparent badge badge-pill',
- );
+ it('renders expand button when discussion is collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- done();
- });
+ expect(diffNotesToggle.text().trim()).toBe('1');
+ expect(diffNotesToggle.classes()).toEqual(
+ jasmine.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
+ );
});
- it('hides discussion when collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
+ it('hides discussion when collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
-
- done();
- });
+ expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
- createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
-
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null);
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1');
+ createComponent({ renderAvatarBadge: true });
+ const noteableDiscussion = wrapper.find(NoteableDiscussion);
+
+ expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
+ expect(
+ noteableDiscussion
+ .find('.badge-pill')
+ .text()
+ .trim(),
+ ).toBe('1');
});
});
});
diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/static/images/green_box.png
index cd1ff9f9ade..cd1ff9f9ade 100644
--- a/spec/javascripts/fixtures/images/green_box.png
+++ b/spec/javascripts/fixtures/static/images/green_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/one_white_pixel.png b/spec/javascripts/fixtures/static/images/one_white_pixel.png
index 073fcf40a18..073fcf40a18 100644
--- a/spec/javascripts/fixtures/one_white_pixel.png
+++ b/spec/javascripts/fixtures/static/images/one_white_pixel.png
Binary files differ
diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/static/images/red_box.png
index 73b2927da0f..73b2927da0f 100644
--- a/spec/javascripts/fixtures/images/red_box.png
+++ b/spec/javascripts/fixtures/static/images/red_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/static/projects.json
index 68a150f602a..68a150f602a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/static/projects.json
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 57e31d933ca..8c7820ddb52 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -6,7 +6,7 @@ import '~/lib/utils/common_utils';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html');
- loadJSONFixtures('projects.json');
+ loadJSONFixtures('static/projects.json');
const NON_SELECTABLE_CLASSES =
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
@@ -67,7 +67,7 @@ describe('glDropdown', function describeDropdown() {
loadFixtures('static/gl_dropdown.html');
this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
+ this.projectsData = getJSONFixture('static/projects.json');
});
afterEach(() => {
diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js
deleted file mode 100644
index ab8642bf0dd..00000000000
--- a/spec/javascripts/import_projects/components/import_projects_table_spec.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
-import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import STATUS_MAP from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-
-describe('ImportProjectsTable', () => {
- let vm;
- let mock;
- let store;
- const reposPath = '/repos-path';
- const jobsPath = '/jobs-path';
- const providerTitle = 'THE PROVIDER';
- const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
- const importedProject = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'started',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
-
- function createComponent() {
- const ImportProjectsTable = Vue.extend(importProjectsTable);
-
- const component = new ImportProjectsTable({
- store,
- propsData: {
- providerTitle,
- },
- }).$mount();
-
- store.dispatch('stopJobsPolling');
-
- return component;
- }
-
- beforeEach(() => {
- store = createStore();
- store.dispatch('setInitialData', { reposPath });
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- it('renders a loading icon whilst repos are loading', done => {
- mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders a table with imported projects and provider repos', done => {
- const response = {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).not.toBeNull();
- expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
- `From ${providerTitle}`,
- );
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders an empty state if there are no imported projects or provider repos', done => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- namespaces: [],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('imports provider repos if bulk import button is clicked', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
-
- mock.onGet(reposPath).replyOnce(200, response);
- mock.onPost(importPath).replyOnce(200, importedProject);
-
- store.dispatch('setInitialData', { importPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$el.querySelector('.js-import-all').click();
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('polls to update the status of imported projects', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [importedProject],
- providerRepos: [],
- namespaces: [{ path: 'path' }],
- };
- const updatedProjects = [
- {
- id: importedProject.id,
- importStatus: 'finished',
- },
- ];
-
- mock.onGet(reposPath).replyOnce(200, response);
-
- store.dispatch('setInitialData', { importPath, jobsPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- const statusObject = STATUS_MAP[importedProject.importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
-
- mock.onGet(jobsPath).replyOnce(200, updatedProjects);
- return store.dispatch('restartJobsPolling');
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 11ab6c38a55..966aee72abb 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -113,7 +113,6 @@ describe('Issue', function() {
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
- mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index ce2c6c43c0f..16dc0084a10 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -175,7 +175,7 @@ describe('Dashboard', () => {
setTimeout(() => {
const dropdownItems = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item[active="true"]',
+ '.js-environments-dropdown .dropdown-item.is-active',
);
expect(dropdownItems.length).toEqual(1);
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 3304c79cdb7..efa864e7d00 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -130,29 +130,6 @@ describe('noteable_discussion component', () => {
});
});
- describe('componentData', () => {
- it('should return first note object for placeholder note', () => {
- const data = {
- isPlaceholderNote: true,
- notes: [{ body: 'hello world!' }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data.notes[0]);
- });
-
- it('should return given note for nonplaceholder notes', () => {
- const data = {
- notes: [{ id: 12 }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data);
- });
- });
-
describe('action text', () => {
const commitId = 'razupaltuff';
const truncatedCommitId = commitId.substr(0, 8);
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
index 6df4ded92f0..7191b65b4cd 100644
--- a/spec/javascripts/pdf/index_spec.js
+++ b/spec/javascripts/pdf/index_spec.js
@@ -3,7 +3,9 @@ import { GlobalWorkerOptions } from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
import PDFLab from '~/pdf/index.vue';
-import pdf from '../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
GlobalWorkerOptions.workerSrc = workerSrc;
const Component = Vue.extend(PDFLab);
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
index b2e9fa42a63..f899b5b3a0d 100644
--- a/spec/javascripts/pdf/page_spec.js
+++ b/spec/javascripts/pdf/page_spec.js
@@ -4,7 +4,9 @@ import workerSrc from 'vendor/pdf.worker.min';
import PageComponent from '~/pdf/page/index.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import testPDF from 'spec/fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('Page component', () => {
const Component = Vue.extend(PageComponent);
diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js
index a503a54029f..6b9fe923624 100644
--- a/spec/javascripts/raven/index_spec.js
+++ b/spec/javascripts/raven/index_spec.js
@@ -5,19 +5,19 @@ describe('RavenConfig options', () => {
const sentryDsn = 'sentryDsn';
const currentUserId = 'currentUserId';
const gitlabUrl = 'gitlabUrl';
- const isProduction = 'isProduction';
+ const environment = 'test';
const revision = 'revision';
let indexReturnValue;
beforeEach(() => {
window.gon = {
sentry_dsn: sentryDsn,
+ sentry_environment: environment,
current_user_id: currentUserId,
gitlab_url: gitlabUrl,
revision,
};
- process.env.NODE_ENV = isProduction;
process.env.HEAD_COMMIT_SHA = revision;
spyOn(RavenConfig, 'init');
@@ -25,12 +25,12 @@ describe('RavenConfig options', () => {
indexReturnValue = index();
});
- it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => {
+ it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => {
expect(RavenConfig.init).toHaveBeenCalledWith({
sentryDsn,
currentUserId,
- whitelistUrls: [gitlabUrl],
- isProduction,
+ whitelistUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
release: revision,
tags: {
revision,
diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
index 5cc59cc28d3..af634a0c196 100644
--- a/spec/javascripts/raven/raven_config_spec.js
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -69,8 +69,8 @@ describe('RavenConfig', () => {
let ravenConfig;
const options = {
sentryDsn: '//sentryDsn',
- whitelistUrls: ['//gitlabUrl'],
- isProduction: true,
+ whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ environment: 'test',
release: 'revision',
tags: {
revision: 'revision',
@@ -95,7 +95,7 @@ describe('RavenConfig', () => {
release: options.release,
tags: options.tags,
whitelistUrls: options.whitelistUrls,
- environment: 'production',
+ environment: 'test',
ignoreErrors: ravenConfig.IGNORE_ERRORS,
ignoreUrls: ravenConfig.IGNORE_URLS,
shouldSendCallback: jasmine.any(Function),
@@ -106,8 +106,8 @@ describe('RavenConfig', () => {
expect(raven.install).toHaveBeenCalled();
});
- it('should set .environment to development if isProduction is false', () => {
- ravenConfig.options.isProduction = false;
+ it('should set environment from options', () => {
+ ravenConfig.options.environment = 'development';
RavenConfig.configure.call(ravenConfig);
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index a820dd2d09c..24b5512b053 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -1,7 +1,7 @@
export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
export const TEST_HOST = 'http://test.host';
-export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
+export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
-export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`;
-export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`;
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 690fcd3e224..a0628fdcebe 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -228,6 +228,7 @@ describe('mrWidgetOptions', () => {
describe('showTargetBranchAdvancedError', () => {
describe(`when the pipeline's target_sha property doesn't exist`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', undefined);
Vue.set(vm.mr, 'targetBranchSha', 'abcd');
vm.$nextTick(done);
@@ -240,6 +241,7 @@ describe('mrWidgetOptions', () => {
describe(`when the pipeline's target_sha matches the target branch's sha`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
Vue.set(vm.mr, 'targetBranchSha', 'abcd');
vm.$nextTick(done);
@@ -250,8 +252,22 @@ describe('mrWidgetOptions', () => {
});
});
+ describe(`when the merge request is not open`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', false);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
describe(`when the pipeline's target_sha does not match the target branch's sha`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
Vue.set(vm.mr, 'targetBranchSha', 'bcde');
vm.$nextTick(done);
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
deleted file mode 100644
index 8fca2637326..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import Vue from 'vue';
-
-import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockMilestone } from 'spec/boards/mock_data';
-
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return mountComponent(Component, {
- milestone,
- });
-};
-
-describe('IssueMilestoneComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', done => {
- const vmStartUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStartUndefined.isMilestoneStarted).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmStartUndefined.$destroy();
- });
-
- it('should return `true` when milestone start date is past current date', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.isMilestoneStarted).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
- });
-
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.isMilestonePastDue).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('should return `true` when milestone due is past current date', done => {
- const vmPastDue = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmPastDue.isMilestonePastDue).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmPastDue.$destroy();
- });
- });
-
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
-
- it('returns string containing absolute milestone start date when due date is not present', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('returns empty string when both milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
-
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', done => {
- const vmFuture = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: `${new Date().getFullYear() + 10}-01-01`,
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
- })
- .then(done)
- .catch(done.fail);
-
- vmFuture.$destroy();
- });
-
- it('returns string containing milestone start date when date has already started and due date is not present', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.milestoneDatesHuman).toContain('Started');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
- const vmStarts = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarts.milestoneDatesHuman).toContain('Starts');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarts.$destroy();
- });
-
- it('returns empty string when milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
- });
- });
-});
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
index b95183747bb..268ced38f40 100644
--- 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
@@ -9,8 +9,8 @@ describe('ProjectListItem component', () => {
let wrapper;
let vm;
let options;
- loadJSONFixtures('projects.json');
- const project = getJSONFixture('projects.json')[0];
+ loadJSONFixtures('static/projects.json');
+ const project = getJSONFixture('static/projects.json')[0];
beforeEach(() => {
options = {
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
index ba9ec8f2f19..34c0cd435cd 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -8,8 +8,8 @@ import { trimText } from 'spec/helpers/vue_component_helper';
describe('ProjectSelector component', () => {
let wrapper;
let vm;
- loadJSONFixtures('projects.json');
- const allProjects = getJSONFixture('projects.json');
+ loadJSONFixtures('static/projects.json');
+ const allProjects = getJSONFixture('static/projects.json');
const searchResults = allProjects.slice(0, 5);
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 08165f147bb..00916f80784 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -137,18 +137,6 @@ describe API::Helpers do
it_behaves_like 'user namespace finder'
end
- describe '#user_namespace' do
- let(:namespace_finder) do
- subject.user_namespace
- end
-
- before do
- allow(subject).to receive(:params).and_return({ id: namespace.id })
- end
-
- it_behaves_like 'user namespace finder'
- end
-
describe '#send_git_blob' do
let(:repository) { double }
let(:blob) { double(name: 'foobar') }
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index b6231510b91..025439f1b6e 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -163,11 +163,11 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Build::Canceled, Gitlab::Ci::Status::Build::Retryable]
+ .to eq [Gitlab::Ci::Status::Build::Canceled]
end
- it 'fabricates a retryable build status' do
- expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ it 'does not fabricate a retryable build status' do
+ expect(status).not_to be_a Gitlab::Ci::Status::Build::Retryable
end
it 'fabricates status with correct details' do
@@ -177,7 +177,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.illustration).to include(:image, :size, :title)
expect(status.label).to eq 'canceled'
expect(status).to have_details
- expect(status).to have_action
+ expect(status).not_to have_action
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index 3ff2fe18c15..613814df23f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -137,19 +137,5 @@ describe Gitlab::Ci::Variables::Collection::Item do
.to eq(key: 'VAR', value: 'value', public: true, file: true, masked: false)
end
end
-
- context 'when variable masking is disabled' do
- before do
- stub_feature_flags(variable_masking: false)
- end
-
- it 'does not expose the masked field to the runner' do
- runner_variable = described_class
- .new(key: 'VAR', value: 'value', masked: true)
- .to_runner_variable
-
- expect(runner_variable).to eq(key: 'VAR', value: 'value', public: true)
- end
- end
end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 66cd8171c12..32b90041c64 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -191,9 +191,8 @@ describe Gitlab::Danger::Helper do
expect(helper.changes_by_category).to eq(
backend: %w[foo.rb],
database: %w[db/foo],
- docs: %w[foo.md],
frontend: %w[foo.js],
- none: %w[ee/changelogs/foo.yml],
+ none: %w[ee/changelogs/foo.yml foo.md],
qa: %w[qa/foo],
unknown: %w[foo]
)
@@ -202,13 +201,13 @@ describe Gitlab::Danger::Helper do
describe '#category_for_file' do
where(:path, :expected_category) do
- 'doc/foo' | :docs
- 'CONTRIBUTING.md' | :docs
- 'LICENSE' | :docs
- 'MAINTENANCE.md' | :docs
- 'PHILOSOPHY.md' | :docs
- 'PROCESS.md' | :docs
- 'README.md' | :docs
+ 'doc/foo' | :none
+ 'CONTRIBUTING.md' | :none
+ 'LICENSE' | :none
+ 'MAINTENANCE.md' | :none
+ 'PHILOSOPHY.md' | :none
+ 'PROCESS.md' | :none
+ 'README.md' | :none
'ee/doc/foo' | :unknown
'ee/README' | :unknown
@@ -272,8 +271,8 @@ describe Gitlab::Danger::Helper do
'foo/bar.rb' | :backend
'foo/bar.js' | :frontend
- 'foo/bar.txt' | :docs
- 'foo/bar.md' | :docs
+ 'foo/bar.txt' | :none
+ 'foo/bar.md' | :none
end
with_them do
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
new file mode 100644
index 00000000000..b89a44e178b
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Deployment do
+ describe '.build' do
+ it 'returns the object kind for a deployment' do
+ deployment = build(:deployment)
+
+ data = described_class.build(deployment)
+
+ expect(data[:object_kind]).to eq('deployment')
+ end
+
+ it 'returns data for the given build' do
+ environment = create(:environment, name: "somewhere")
+ project = create(:project, :repository, name: 'myproj')
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
+ deployable = deployment.deployable
+ expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable)
+ expected_commit_url = Gitlab::UrlBuilder.build(commit)
+
+ data = described_class.build(deployment)
+
+ expect(data[:status]).to eq('failed')
+ expect(data[:deployable_id]).to eq(deployable.id)
+ expect(data[:deployable_url]).to eq(expected_deployable_url)
+ expect(data[:environment]).to eq("somewhere")
+ expect(data[:project]).to eq(project.hook_attrs)
+ expect(data[:short_sha]).to eq(deployment.short_sha)
+ expect(data[:user]).to eq(deployment.user.hook_attrs)
+ expect(data[:commit_url]).to eq(expected_commit_url)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index 0c4decc6518..46ad674a1eb 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -23,9 +23,12 @@ describe Gitlab::DataBuilder::Push do
describe '.build' do
let(:data) do
- described_class.build(project, user, Gitlab::Git::BLANK_SHA,
- '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
- 'refs/tags/v1.1.0')
+ described_class.build(
+ project: project,
+ user: user,
+ oldrev: Gitlab::Git::BLANK_SHA,
+ newrev: '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
+ ref: 'refs/tags/v1.1.0')
end
it { expect(data).to be_a(Hash) }
@@ -47,7 +50,7 @@ describe Gitlab::DataBuilder::Push do
include_examples 'deprecated repository hook data'
it 'does not raise an error when given nil commits' do
- expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) }
+ expect { described_class.build(project: spy, user: spy, ref: 'refs/tags/v1.1.0', commits: nil) }
.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 10bc82e24d1..1c24244c3a6 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -341,7 +341,7 @@ describe Gitlab::Git::Blob, :seed_helper do
it { expect(blob.mode).to eq("100755") }
end
- context 'file with Chinese text' do
+ context 'file with Japanese text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
it { expect(blob.name).to eq("テスト.txt") }
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
index 0d5069568e1..6511c2b61bf 100644
--- a/spec/lib/gitlab/git/object_pool_spec.rb
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -3,8 +3,12 @@
require 'spec_helper'
describe Gitlab::Git::ObjectPool do
+ include RepoHelpers
+
let(:pool_repository) { create(:pool_repository) }
let(:source_repository) { pool_repository.source_project.repository }
+ let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) }
+ let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) }
subject { pool_repository.object_pool }
@@ -76,4 +80,41 @@ describe Gitlab::Git::ObjectPool do
end
end
end
+
+ describe '#fetch' do
+ let(:commit_count) { source_repository.commit_count }
+
+ context "when the object's pool repository exists" do
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
+ end
+
+ context "when the object's pool repository does not exist" do
+ before do
+ subject.delete
+ end
+
+ it "re-creates the object pool's repository" do
+ subject.fetch
+
+ expect(subject.repository.exists?).to be(true)
+ end
+
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
+
+ it 'fetches objects from the source repository' do
+ new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid
+
+ expect(subject.repository.exists?).to be false
+
+ subject.fetch
+
+ expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count)
+ expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 45fe5d72937..0f6aac9b6de 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Git::Repository, :seed_helper do
include Gitlab::EncodingHelper
+ include RepoHelpers
using RSpec::Parameterized::TableSyntax
shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method|
@@ -95,6 +96,12 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#create_repository' do
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :create_repository do
+ subject { repository.create_repository }
+ end
+ end
+
describe '#branch_names' do
subject { repository.branch_names }
@@ -2203,83 +2210,6 @@ describe Gitlab::Git::Repository, :seed_helper do
repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
end
- # Build the options hash that's passed to Rugged::Commit#create
- def commit_options(repo, index, target, ref, message)
- options = {}
- options[:tree] = index.write_tree(repo)
- options[:author] = {
- email: "test@example.com",
- name: "Test Author",
- time: Time.gm(2014, "mar", 3, 20, 15, 1)
- }
- options[:committer] = {
- email: "test@example.com",
- name: "Test Author",
- time: Time.gm(2014, "mar", 3, 20, 15, 1)
- }
- options[:message] ||= message
- options[:parents] = repo.empty? ? [] : [target].compact
- options[:update_ref] = ref
-
- options
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of CHANGELOG with a single new line of text.
- def new_commit_edit_old_file(repo)
- oid = repo.write("I replaced the changelog with this text", :blob)
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
-
- options = commit_options(
- repo,
- index,
- repo.head.target,
- "HEAD",
- "Edit CHANGELOG in its original location"
- )
-
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of the specified file_path with new text.
- def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
- oid = repo.write(text, :blob)
- index = repo.index
- index.read_tree(branch.target.tree)
- index.add(path: file_path, oid: oid, mode: 0100644)
- options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of encoding/CHANGELOG with new text.
- def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
- branch = repo.branches[branch_name]
- new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
- # CHANGELOG file to the encoding/ directory.
- def new_commit_move_file(repo)
- blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
- file_content = repo.lookup(blob_oid).content
- oid = repo.write(file_content, :blob)
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
- index.remove("CHANGELOG")
-
- options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
-
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
def refs(dir)
IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
line.split("\t").last
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index ded5d7576df..1e577392949 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -21,13 +21,13 @@ describe Gitlab::Git::Wiki do
end
it 'returns all the pages' do
- expect(subject.pages.count).to eq(2)
- expect(subject.pages.first.title).to eq 'page1'
- expect(subject.pages.last.title).to eq 'page2'
+ expect(subject.list_pages.count).to eq(2)
+ expect(subject.list_pages.first.title).to eq 'page1'
+ expect(subject.list_pages.last.title).to eq 'page2'
end
it 'returns only one page' do
- pages = subject.pages(limit: 1)
+ pages = subject.list_pages(limit: 1)
expect(pages.count).to eq(1)
expect(pages.first.title).to eq 'page1'
@@ -62,8 +62,8 @@ describe Gitlab::Git::Wiki do
subject.delete_page('*', commit_details('whatever'))
- expect(subject.pages.count).to eq 1
- expect(subject.pages.first.title).to eq 'page1'
+ expect(subject.list_pages.count).to eq 1
+ expect(subject.list_pages.first.title).to eq 'page1'
end
end
@@ -87,7 +87,7 @@ describe Gitlab::Git::Wiki do
with_them do
subject { wiki.preview_slug(title, format) }
- let(:gitaly_slug) { wiki.pages.first }
+ let(:gitaly_slug) { wiki.list_pages.first }
it { is_expected.to eq(expected_slug) }
@@ -96,7 +96,7 @@ describe Gitlab::Git::Wiki do
create_page(title, 'content', format: format)
- gitaly_slug = wiki.pages.first.url_path
+ gitaly_slug = wiki.list_pages.first.url_path
is_expected.to eq(gitaly_slug)
end
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
index 149b7ec5bb0..0e0c3d329b5 100644
--- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -43,4 +43,24 @@ describe Gitlab::GitalyClient::ObjectPoolService do
end
end
end
+
+ describe '#fetch' do
+ before do
+ subject.delete
+ end
+
+ it 'restores the pool repository objects' do
+ subject.fetch(project.repository)
+
+ expect(object_pool.repository.exists?).to be(true)
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ subject.delete
+
+ expect { subject.fetch(project.repository) }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
index d82c9c28da0..4fa8e97aca0 100644
--- a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::GitalyClient::WikiService do
end
end
- describe '#get_all_pages' do
+ describe '#load_all_pages' do
let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } }
let(:response) do
[
@@ -63,7 +63,7 @@ describe Gitlab::GitalyClient::WikiService do
let(:wiki_page_2) { subject[1].first }
let(:wiki_page_2_version) { subject[1].last }
- subject { client.get_all_pages }
+ subject { client.load_all_pages }
it 'sends a wiki_get_all_pages message' do
expect_any_instance_of(Gitaly::WikiService::Stub)
@@ -99,7 +99,7 @@ describe Gitlab::GitalyClient::WikiService do
end
context 'with limits' do
- subject { client.get_all_pages(limit: 1) }
+ subject { client.load_all_pages(limit: 1) }
it 'sends a request with the limit' do
expect_any_instance_of(Gitaly::WikiService::Stub)
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index f1acb1d9bc4..da1eb0c2618 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -142,6 +142,48 @@ describe Gitlab::GitalyClient do
end
end
+ describe '.request_kwargs' do
+ context 'when catfile-cache feature is enabled' do
+ before do
+ stub_feature_flags('gitaly_catfile-cache': true)
+ end
+
+ it 'sets the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).to include('gitaly-session-id')
+ end
+
+ context 'when RequestStore is not enabled' do
+ it 'sets a different gitaly-session-id per request' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ it 'sets the same gitaly-session-id on every outgoing request metadata' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ 3.times do
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ end
+ end
+ end
+ end
+
+ context 'when catfile-cache feature is disabled' do
+ before do
+ stub_feature_flags({ 'gitaly_catfile-cache': false })
+ end
+
+ it 'does not set the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).not_to include('gitaly-session-id')
+ end
+ end
+ end
+
describe 'enforce_gitaly_request_limits?' do
def call_gitaly(count = 1)
(1..count).each do
diff --git a/spec/lib/gitlab/graphql/generic_tracing_spec.rb b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
new file mode 100644
index 00000000000..ae92dcc40af
--- /dev/null
+++ b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::GenericTracing 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
+
+ context "when labkit tracing is enabled" do
+ before do
+ expect(Labkit::Tracing).to receive(:enabled?).and_return(true)
+ end
+
+ it 'yields with labkit tracing' do
+ expected_tags = {
+ 'component' => 'web',
+ 'span.kind' => 'server',
+ 'platform_key' => 'pkey',
+ 'key' => 'key'
+ }
+
+ expect(Labkit::Tracing)
+ .to receive(:with_tracing)
+ .with(operation_name: "pkey.key", tags: expected_tags)
+ .and_yield
+
+ expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
+ end
+ end
+
+ context "when labkit tracing is disabled" do
+ before do
+ expect(Labkit::Tracing).to receive(:enabled?).and_return(false)
+ end
+
+ it 'yields without measurement' do
+ expect(Labkit::Tracing).not_to receive(:with_tracing)
+
+ expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
+ end
+ 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/graphql/tracing_spec.rb b/spec/lib/gitlab/graphql/tracing_spec.rb
deleted file mode 100644
index 7300a9a572e..00000000000
--- a/spec/lib/gitlab/graphql/tracing_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 54369ff75f4..482e9c05da8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -322,6 +322,7 @@ project:
- pool_repository
- kubernetes_namespaces
- error_tracking_setting
+- metrics_setting
award_emoji:
- awardable
- user
@@ -360,3 +361,5 @@ error_tracking_setting:
- project
suggestions:
- note
+metrics_setting:
+- project
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index ebb62124cb1..9093d21647a 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -423,6 +423,7 @@ Service:
- wiki_page_events
- confidential_issues_events
- confidential_note_events
+- deployment_events
ProjectHook:
- id
- url
@@ -606,7 +607,6 @@ ResourceLabelEvent:
- user_id
- created_at
ErrorTracking::ProjectErrorTrackingSetting:
-- id
- api_url
- project_id
- project_name
@@ -626,3 +626,8 @@ MergeRequestAssignee:
- id
- user_id
- merge_request_id
+ProjectMetricsSetting:
+- project_id
+- external_dashboard_url
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 8433d40b2ea..24ce397ec3d 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -33,6 +33,28 @@ describe Gitlab::Kubernetes::Helm::Api do
end
end
+ describe '#uninstall' do
+ before do
+ allow(client).to receive(:create_pod).and_return(nil)
+ allow(client).to receive(:delete_pod).and_return(nil)
+ allow(namespace).to receive(:ensure_exists!).once
+ end
+
+ it 'ensures the namespace exists before creating the POD' do
+ expect(namespace).to receive(:ensure_exists!).once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.uninstall(command)
+ end
+
+ it 'removes an existing pod before installing' do
+ expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.uninstall(command)
+ end
+ end
+
describe '#install' do
before do
allow(client).to receive(:create_pod).and_return(nil)
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
new file mode 100644
index 00000000000..e88eb140b35
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+ let(:system_dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
+
+ describe '.find' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_call) { described_class.find(project, nil, environment, dashboard_path) }
+
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the system dashboard is specified' do
+ let(:dashboard_path) { system_dashboard_path }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when no dashboard is specified' do
+ let(:service_call) { described_class.find(project, nil, environment) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+
+ describe '.find_all_paths' do
+ let(:all_dashboard_paths) { described_class.find_all_paths(project) }
+ let(:system_dashboard) { { path: system_dashboard_path, default: true } }
+
+ it 'includes only the system dashboard by default' do
+ expect(all_dashboard_paths).to eq([system_dashboard])
+ end
+
+ context 'when the project contains dashboards' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it 'includes system and project dashboards' do
+ project_dashboard = { path: dashboard_path, default: false }
+
+ expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
new file mode 100644
index 00000000000..be3c1095bd7
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Processor do
+ let(:project) { build(:project) }
+ let(:environment) { build(:environment, project: project) }
+ let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+
+ describe 'process' do
+ let(:process_params) { [project, environment, dashboard_yml] }
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: true) }
+
+ context 'when dashboard config corresponds to common metrics' do
+ let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
+
+ it 'inserts metric ids into the config' do
+ target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' }
+
+ expect(target_metric).to include(:metric_id)
+ expect(target_metric[:metric_id]).to eq(common_metric.id)
+ end
+ end
+
+ context 'when the project has associated metrics' do
+ let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) }
+ let!(:project_system_metric) { create(:prometheus_metric, project: project, group: :system) }
+ let!(:project_business_metric) { create(:prometheus_metric, project: project, group: :business) }
+
+ it 'includes project-specific metrics' do
+ expect(all_metrics).to include get_metric_details(project_system_metric)
+ expect(all_metrics).to include get_metric_details(project_response_metric)
+ expect(all_metrics).to include get_metric_details(project_business_metric)
+ end
+
+ it 'orders groups by priority and panels by weight' do
+ expected_metrics_order = [
+ 'metric_b', # group priority 10, panel weight 1
+ 'metric_a2', # group priority 1, panel weight 2
+ 'metric_a1', # group priority 1, panel weight 1
+ project_business_metric.id, # group priority 0, panel weight nil (0)
+ project_response_metric.id, # group priority -5, panel weight nil (0)
+ project_system_metric.id, # group priority -10, panel weight nil (0)
+ ]
+ actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] }
+
+ expect(actual_metrics_order).to eq expected_metrics_order
+ end
+
+ context 'when the dashboard should not include project metrics' do
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: false) }
+
+ it 'includes only dashboard metrics' do
+ metrics = all_metrics.map { |m| m[:id] }
+
+ expect(metrics.length).to be(3)
+ expect(metrics).to eq %w(metric_b metric_a2 metric_a1)
+ end
+ end
+ end
+
+ shared_examples_for 'errors with message' do |expected_message|
+ it 'raises a DashboardLayoutError' do
+ error_class = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ expect { dashboard }.to raise_error(error_class, expected_message)
+ end
+ end
+
+ context 'when the dashboard is missing panel_groups' do
+ let(:dashboard_yml) { {} }
+
+ it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array'
+ end
+
+ context 'when the dashboard contains a panel_group which is missing panels' do
+ let(:dashboard_yml) { { panel_groups: [{}] } }
+
+ it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels'
+ end
+
+ context 'when the dashboard contains a panel which is missing metrics' do
+ let(:dashboard_yml) { { panel_groups: [{ panels: [{}] }] } }
+
+ it_behaves_like 'errors with message', 'Each "panel" must define an array :metrics'
+ end
+ end
+
+ private
+
+ def all_metrics
+ dashboard[:panel_groups].map do |group|
+ group[:panels].map { |panel| panel[:metrics] }
+ end.flatten
+ end
+
+ def get_metric_details(metric)
+ {
+ query_range: metric.query,
+ unit: metric.unit,
+ label: metric.legend,
+ metric_id: metric.id
+ }
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
new file mode 100644
index 00000000000..162beb0268a
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:user) { build(:user) }
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+ end
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'and the dashboard is then deleted' do
+ it 'does not return the previously cached dashboard' do
+ described_class.new(*service_params).get_dashboard
+
+ delete_project_dashboard(project, user, dashboard_path)
+
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ end
+ end
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
new file mode 100644
index 00000000000..e71ce2481a3
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { described_class::SYSTEM_DASHBOARD_PATH }
+ let(:service_params) { [project, nil, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect(YAML).to receive(:safe_load).once.and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'when called with a non-system dashboard' do
+ let(:dashboard_path) { 'garbage/dashboard/path' }
+
+ # We want to alwaus return the system dashboard.
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 9f2214f7ce7..5af52db7a1f 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -27,13 +27,13 @@ describe Gitlab::Profiler do
it 'sends a POST request when data is passed' do
post_data = '{"a":1}'
- expect(app).to receive(:post).with(anything, post_data, anything)
+ expect(app).to receive(:post).with(anything, params: post_data, headers: anything)
described_class.profile('/', post_data: post_data)
end
it 'uses the private_token for auth if given' do
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', private_token: private_token)
@@ -51,7 +51,7 @@ describe Gitlab::Profiler do
user = double(:user)
expect(described_class).to receive(:with_user).with(nil).and_call_original
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', user: user, private_token: private_token)
diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb
index 78c74266c61..048f4af6020 100644
--- a/spec/lib/gitlab/prometheus/query_variables_spec.rb
+++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Prometheus::QueryVariables do
describe '.call' do
+ let(:project) { environment.project }
let(:environment) { create(:environment) }
let(:slug) { environment.slug }
@@ -21,13 +22,32 @@ describe Gitlab::Prometheus::QueryVariables do
end
context 'with deployment platform' do
- let(:kube_namespace) { environment.deployment_platform.actual_namespace }
+ context 'with project cluster' do
+ let(:kube_namespace) { environment.deployment_platform.actual_namespace }
- before do
- create(:cluster, :provided_by_user, projects: [environment.project])
+ before do
+ create(:cluster, :project, :provided_by_user, projects: [project])
+ end
+
+ it { is_expected.to include(kube_namespace: kube_namespace) }
end
- it { is_expected.to include(kube_namespace: kube_namespace) }
+ context 'with group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) }
+ let(:group) { create(:group) }
+ let(:project2) { create(:project) }
+ let(:kube_namespace) { k8s_ns.namespace }
+
+ let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) }
+ let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) }
+
+ before do
+ group.projects << project
+ group.projects << project2
+ end
+
+ it { is_expected.to include(kube_namespace: kube_namespace) }
+ end
end
end
end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index fd25132ed3a..cc90a998d3f 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -11,6 +11,25 @@ describe ApplicationRecord do
end
end
+ describe '.safe_ensure_unique' do
+ let(:model) { build(:suggestion) }
+ let(:klass) { model.class }
+
+ before do
+ allow(model).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'returns false when ActiveRecord::RecordNotUnique is raised' do
+ expect(model).to receive(:save).once
+ expect(klass.safe_ensure_unique { model.save }).to be_falsey
+ end
+
+ it 'retries based on retry count specified' do
+ expect(model).to receive(:save).exactly(3).times
+ expect(klass.safe_ensure_unique(retries: 2) { model.save }).to be_falsey
+ end
+ end
+
describe '.safe_find_or_create_by' do
it 'creates the user avoiding race conditions' do
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c7d7dbac736..f8dc1541dd3 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -31,6 +31,20 @@ describe ApplicationSetting do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
+ it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value(nil).for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("myemail@example.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value("myemail@test.example.com").for(:lets_encrypt_notification_email) }
+
+ context "when user accepted let's encrypt terms of service" do
+ before do
+ setting.update(lets_encrypt_terms_of_service_accepted: true)
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
+ end
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index d0e1688cce3..8364293b908 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -43,6 +43,21 @@ describe Blob do
changelog.id
contributing.id
end
+
+ it 'does not include blobs from previous requests in later requests' do
+ changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
+
+ # Access property so the values are loaded
+ changelog.id
+ contributing.id
+
+ readme = described_class.lazy(project, commit_id, 'README.md')
+
+ expect(project.repository).to receive(:blobs_at).with([[commit_id, 'README.md']]).once.and_call_original
+
+ readme.id
+ end
end
describe '#data' do
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 44b5af5e5aa..eb32198265b 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -10,6 +10,8 @@ describe Ci::Bridge do
create(:ci_bridge, pipeline: pipeline)
end
+ it { is_expected.to include_module(Ci::PipelineDelegator) }
+
describe '#tags' do
it 'only has a bridge tag' do
expect(bridge.tags).to eq [:bridge]
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3a7d20a58c8..8c73f37bd32 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -28,6 +28,7 @@ describe Ci::Build do
it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
+ it { is_expected.to include_module(Ci::PipelineDelegator) }
it { is_expected.to be_a(ArtifactMigratable) }
@@ -856,6 +857,10 @@ describe Ci::Build do
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
+ before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
+ end
+
it 'has deployments record with created status' do
expect(deployment).to be_created
expect(environment.name).to eq('review/master')
@@ -1429,7 +1434,7 @@ describe Ci::Build do
build.cancel!
end
- it { is_expected.to be_retryable }
+ it { is_expected.not_to be_retryable }
end
end
@@ -1959,7 +1964,7 @@ describe Ci::Build do
context 'when build has been canceled' do
subject { build_stubbed(:ci_build, :manual, status: :canceled) }
- it { is_expected.to be_playable }
+ it { is_expected.not_to be_playable }
end
context 'when build is successful' do
@@ -2269,6 +2274,19 @@ describe Ci::Build do
it { user_variables.each { |v| is_expected.to include(v) } }
end
+ context 'when build belongs to a pipeline for merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_branch: 'improve/awesome') }
+ let(:pipeline) { merge_request.all_pipelines.first }
+ let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
+
+ it 'returns values based on source ref' do
+ is_expected.to include(
+ { key: 'CI_COMMIT_REF_NAME', value: 'improve/awesome', public: true, masked: false },
+ { key: 'CI_COMMIT_REF_SLUG', value: 'improve-awesome', public: true, masked: false }
+ )
+ end
+ end
+
context 'when build has an environment' do
let(:environment_variables) do
[
@@ -2660,6 +2678,8 @@ describe Ci::Build do
)
end
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+
it 'returns static predefined variables' do
expect(build.variables.size).to be >= 28
expect(build.variables)
@@ -2709,6 +2729,8 @@ describe Ci::Build do
)
end
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3c823b78be7..9d0cd654f13 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -382,6 +382,54 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#source_ref' do
+ subject { pipeline.source_ref }
+
+ let(:pipeline) { create(:ci_pipeline, ref: 'feature') }
+
+ it 'returns source ref' do
+ is_expected.to eq('feature')
+ end
+
+ context 'when the pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
+ end
+
+ it 'returns source ref' do
+ is_expected.to eq(merge_request.source_branch)
+ end
+ end
+ end
+
+ describe '#source_ref_slug' do
+ subject { pipeline.source_ref_slug }
+
+ let(:pipeline) { create(:ci_pipeline, ref: 'feature') }
+
+ it 'slugifies with the source ref' do
+ expect(Gitlab::Utils).to receive(:slugify).with('feature')
+
+ subject
+ end
+
+ context 'when the pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
+ end
+
+ it 'slugifies with the source ref of the merge request' do
+ expect(Gitlab::Utils).to receive(:slugify).with(merge_request.source_branch)
+
+ subject
+ end
+ end
+ end
+
describe '.triggered_for_branch' do
subject { described_class.triggered_for_branch(ref) }
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index 5cd80edb3a1..8d853a04e33 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -10,6 +10,12 @@ describe Clusters::Applications::CertManager do
include_examples 'cluster application version specs', :clusters_applications_cert_managers
include_examples 'cluster application initial status specs'
+ describe '#can_uninstall?' do
+ subject { cert_manager.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#install_command' do
let(:cert_email) { 'admin@example.com' }
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index f177d493a2e..6ea6c110d62 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -18,6 +18,14 @@ describe Clusters::Applications::Helm do
it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
end
+ describe '#can_uninstall?' do
+ let(:helm) { create(:clusters_applications_helm) }
+
+ subject { helm.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#issue_client_cert' do
let(:application) { create(:clusters_applications_helm) }
subject { application.issue_client_cert }
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 113d29b5551..292ddabd2d8 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -18,6 +18,12 @@ describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
+ describe '#can_uninstall?' do
+ subject { ingress.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#make_installed!' do
before do
application.make_installed!
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 1a7363b64f9..fc9ebed863e 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -10,6 +10,15 @@ describe Clusters::Applications::Jupyter do
it { is_expected.to belong_to(:oauth_application) }
+ describe '#can_uninstall?' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ subject { jupyter.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#set_initial_status' do
before do
jupyter.set_initial_status
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index 405b5ad691c..d5974f47190 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -39,6 +39,12 @@ describe Clusters::Applications::Knative do
end
end
+ describe '#can_uninstall?' do
+ subject { knative.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#schedule_status_update with external_ip' do
let(:application) { create(:clusters_applications_knative, :installed) }
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index e8ba9737c23..26267c64112 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -11,6 +11,21 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs'
+ describe 'after_destroy' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:prometheus_service) { project.create_prometheus_service(active: true) }
+
+ it 'deactivates prometheus_service after destroy' do
+ expect do
+ application.destroy!
+
+ prometheus_service.reload
+ end.to change(prometheus_service, :active).from(true).to(false)
+ end
+ end
+
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
@@ -23,12 +38,20 @@ describe Clusters::Applications::Prometheus do
end
it 'ensures Prometheus service is activated' do
- expect(prometheus_service).to receive(:update).with(active: true)
+ expect(prometheus_service).to receive(:update!).with(active: true)
subject.make_installed
end
end
+ describe '#can_uninstall?' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.can_uninstall? }
+
+ it { is_expected.to be_truthy }
+ end
+
describe '#prometheus_client' do
context 'cluster is nil' do
it 'returns nil' do
@@ -134,6 +157,34 @@ describe Clusters::Applications::Prometheus do
end
end
+ describe '#uninstall_command' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.uninstall_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
+
+ it 'has the application name' do
+ expect(subject.name).to eq('prometheus')
+ end
+
+ it 'has files' do
+ expect(subject.files).to eq(prometheus.files)
+ end
+
+ it 'is rbac' do
+ expect(subject).to be_rbac
+ end
+
+ context 'on a non rbac enabled cluster' do
+ before do
+ prometheus.cluster.platform_kubernetes.abac!
+ end
+
+ it { is_expected.not_to be_rbac }
+ end
+ end
+
describe '#upgrade_command' do
let(:prometheus) { build(:clusters_applications_prometheus) }
let(:values) { prometheus.values }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index de406211a5b..bdc0cb8ed86 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -13,6 +13,14 @@ describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) }
+ describe '#can_uninstall?' do
+ let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
+
+ subject { gitlab_runner.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#install_command' do
let(:kubeclient) { double('kubernetes client') }
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
@@ -24,7 +32,7 @@ describe Clusters::Applications::Runner 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.4.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
expect(subject).to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -42,7 +50,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.4.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index d9170d5fa07..f51322e1404 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -102,6 +102,13 @@ describe Deployment do
deployment.succeed!
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.succeed!
+ end
end
context 'when deployment failed' do
@@ -115,6 +122,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.drop!
+ end
end
context 'when deployment was canceled' do
@@ -128,6 +142,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.cancel!
+ end
end
end
@@ -379,6 +400,12 @@ describe Deployment do
it { is_expected.to be_nil }
end
+ context 'project uses the kubernetes service for deployments' do
+ let!(:service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to be_nil }
+ end
+
context 'project has a deployment platform' do
let!(:cluster) { create(:cluster, projects: [project]) }
let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index c68c3ce2abe..782a84f922b 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -70,6 +70,16 @@ describe Member do
expect(child_member).not_to be_valid
end
+ # Membership in a subgroup confers certain access rights, such as being
+ # able to merge or push code to protected branches.
+ it "is valid with an equal level" do
+ child_member.access_level = GroupMember::DEVELOPER
+
+ child_member.validate
+
+ expect(child_member).to be_valid
+ end
+
it "is valid with a higher level" do
child_member.access_level = GroupMember::MAINTAINER
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f61857ea5ff..7457efef013 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2123,7 +2123,7 @@ describe MergeRequest do
end
context 'when merges are not restricted to green builds' do
- subject { build(:merge_request, target_project: build(:project, only_allow_merge_if_pipeline_succeeds: false)) }
+ subject { build(:merge_request, target_project: create(:project, only_allow_merge_if_pipeline_succeeds: false)) }
context 'and a failed pipeline is associated' do
before do
@@ -2262,6 +2262,50 @@ describe MergeRequest do
end
end
+ describe "#environments" do
+ subject { merge_request.environments }
+
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ let(:project) { merge_request.project }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request, project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+
+ it 'returns environments' do
+ is_expected.to eq(pipeline.environments)
+ expect(subject.count).to be(1)
+ end
+
+ context 'when pipeline is not associated with environments' do
+ let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is not a pipeline for merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ it 'returns empty relation' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe "#reload_diff" do
it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
user = create(:user)
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 3710f2be287..1b1ede6b14c 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -9,11 +9,43 @@ describe NotificationRecipient do
subject(:recipient) { described_class.new(user, :watch, target: target, project: project) }
- it 'denies access to a target when cross project access is denied' do
- allow(Ability).to receive(:allowed?).and_call_original
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false)
+ describe '#has_access?' do
+ before do
+ allow(user).to receive(:can?).and_call_original
+ end
+
+ context 'user cannot read cross project' do
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_cross_project).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'user cannot read build' do
+ let(:target) { build(:ci_pipeline) }
+
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_build, target).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
- expect(recipient.has_access?).to be_falsy
+ context 'user cannot read commit' do
+ let(:target) { build(:commit) }
+
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_commit, target).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'target has no policy' do
+ let(:target) { double.as_null_object }
+
+ it 'returns true' do
+ expect(recipient.has_access?).to eq true
+ end
+ end
end
context '#notification_setting' do
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 142ddebbbf8..ec4d4517f82 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -344,4 +344,32 @@ describe PagesDomain do
end
end
end
+
+ describe '.for_removal' do
+ subject { described_class.for_removal }
+
+ context 'when domain is not schedule for removal' do
+ let!(:domain) { create :pages_domain }
+
+ it 'does not return domain' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when domain is scheduled for removal yesterday' do
+ let!(:domain) { create :pages_domain, remove_at: 1.day.ago }
+
+ it 'returns domain' do
+ is_expected.to eq([domain])
+ end
+ end
+
+ context 'when domain is scheduled for removal tomorrow' do
+ let!(:domain) { create :pages_domain, remove_at: 1.day.from_now }
+
+ it 'does not return domain' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/project_metrics_setting_spec.rb b/spec/models/project_metrics_setting_spec.rb
new file mode 100644
index 00000000000..7df01625ba1
--- /dev/null
+++ b/spec/models/project_metrics_setting_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectMetricsSetting do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ context 'when external_dashboard_url is over 255 chars' do
+ before do
+ subject.external_dashboard_url = 'https://' + 'a' * 250
+ end
+
+ it 'fails validation' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:external_dashboard_url])
+ .to include('is too long (maximum is 255 characters)')
+ end
+ end
+
+ context 'with unsafe url' do
+ before do
+ subject.external_dashboard_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>}
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'non ascii chars in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://gitlab.com/api/0/projects/project1/something€'
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'internal url in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://192.168.1.1'
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'external_dashboard_url is blank' do
+ before do
+ subject.external_dashboard_url = ''
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/project_services/chat_message/deployment_message_spec.rb
new file mode 100644
index 00000000000..86565ce8b01
--- /dev/null
+++ b/spec/models/project_services/chat_message/deployment_message_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ChatMessage::DeploymentMessage do
+ describe '#pretext' do
+ it 'returns a message with the data returned by the deployment data builder' do
+ environment = create(:environment, name: "myenvironment")
+ project = create(:project, :repository)
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq("Deploy to myenvironment succeeded")
+ end
+
+ it 'returns a message for a successful deployment' do
+ data = {
+ status: 'success',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production succeeded')
+ end
+
+ it 'returns a message for a failed deployment' do
+ data = {
+ status: 'failed',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production failed')
+ end
+
+ it 'returns a message for a canceled deployment' do
+ data = {
+ status: 'canceled',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production canceled')
+ end
+
+ it 'returns a message for a deployment to another environment' do
+ data = {
+ status: 'success',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging succeeded')
+ end
+
+ it 'returns a message for a deployment with any other status' do
+ data = {
+ status: 'unknown',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging unknown')
+ end
+ end
+
+ describe '#attachments' do
+ def deployment_data(params)
+ {
+ object_kind: "deployment",
+ status: "success",
+ deployable_id: 3,
+ deployable_url: "deployable_url",
+ environment: "sandbox",
+ project: {
+ name: "greatproject",
+ web_url: "project_web_url",
+ path_with_namespace: "project_path_with_namespace"
+ },
+ user: {
+ name: "Jane Person",
+ username: "jane"
+ },
+ short_sha: "12345678",
+ commit_url: "commit_url"
+ }.merge(params)
+ end
+
+ it 'returns attachments with the data returned by the deployment data builder' do
+ user = create(:user, name: "John Smith", username: "smith")
+ namespace = create(:namespace, name: "myspace")
+ project = create(:project, :repository, namespace: namespace, name: "myproject")
+ commit = project.commit('HEAD')
+ environment = create(:environment, name: "myenvironment", project: project)
+ ci_build = create(:ci_build, project: project)
+ deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
+ job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
+ commit_url = Gitlab::UrlBuilder.build(deployment.commit)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[myspace/myproject](#{project.web_url})\n[Job ##{ci_build.id}](#{job_url}), SHA [#{deployment.short_sha}](#{commit_url}), by John Smith (smith)",
+ color: "good"
+ }])
+ end
+
+ it 'returns attachments for a failed deployment' do
+ data = deployment_data(status: 'failed')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
+ color: "danger"
+ }])
+ end
+
+ it 'returns attachments for a canceled deployment' do
+ data = deployment_data(status: 'canceled')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
+ color: "warning"
+ }])
+ end
+
+ it 'uses a neutral color for a deployment with any other status' do
+ data = deployment_data(status: 'some-new-status-we-make-in-the-future')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
+ color: "#334455"
+ }])
+ end
+ end
+end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index fd9e33c1781..a04b984c1f6 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -98,12 +98,11 @@ describe HipchatService do
context 'tag_push events' do
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build(
- project,
- user,
- Gitlab::Git::BLANK_SHA,
- '1' * 40,
- 'refs/tags/test',
- [])
+ project: project,
+ user: user,
+ oldrev: Gitlab::Git::BLANK_SHA,
+ newrev: '1' * 40,
+ ref: 'refs/tags/test')
end
it "calls Hipchat API for tag push events" do
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 521d5265753..c025d7c882e 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -30,6 +30,12 @@ describe MicrosoftTeamsService do
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
set(:project) { create(:project, :repository, :wiki_repo) }
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 2525a6aebe0..d12dd97bb9e 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -109,8 +109,7 @@ describe ProjectWiki do
subject { super().empty? }
it { is_expected.to be_falsey }
- # Re-enable this when https://gitlab.com/gitlab-org/gitaly/issues/1204 is fixed
- xit 'only instantiates a Wiki page once' do
+ it 'only instantiates a Wiki page once' do
expect(WikiPage).to receive(:new).once.and_call_original
subject
@@ -119,22 +118,65 @@ describe ProjectWiki do
end
end
- describe "#pages" do
+ describe "#list_pages" do
+ let(:wiki_pages) { subject.list_pages }
+
before do
- create_page("index", "This is an awesome new Gollum Wiki")
- @pages = subject.pages
+ create_page("index", "This is an index")
+ create_page("index2", "This is an index2")
+ create_page("an index3", "This is an index3")
end
after do
- destroy_page(@pages.first.page)
+ wiki_pages.each do |wiki_page|
+ destroy_page(wiki_page.page)
+ end
end
it "returns an array of WikiPage instances" do
- expect(@pages.first).to be_a WikiPage
+ expect(wiki_pages.first).to be_a WikiPage
+ end
+
+ it 'does not load WikiPage content by default' do
+ wiki_pages.each do |page|
+ expect(page.content).to be_empty
+ end
+ end
+
+ it 'returns all pages by default' do
+ expect(wiki_pages.count).to eq(3)
+ end
+
+ context "with limit option" do
+ it 'returns limited set of pages' do
+ expect(subject.list_pages(limit: 1).count).to eq(1)
+ end
end
- it "returns the correct number of pages" do
- expect(@pages.count).to eq(1)
+ context "with sorting options" do
+ it 'returns pages sorted by title by default' do
+ pages = ['an index3', 'index', 'index2']
+
+ expect(subject.list_pages.map(&:title)).to eq(pages)
+ expect(subject.list_pages(direction: "desc").map(&:title)).to eq(pages.reverse)
+ end
+
+ it 'returns pages sorted by created_at' do
+ pages = ['index', 'index2', 'an index3']
+
+ expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
+ expect(subject.list_pages(sort: 'created_at', direction: "desc").map(&:title)).to eq(pages.reverse)
+ end
+ end
+
+ context "with load_content option" do
+ let(:pages) { subject.list_pages(load_content: true) }
+
+ it 'loads WikiPage content' do
+ expect(pages.first.content).to eq("This is an index3")
+ expect(pages.second.content).to eq("This is an index")
+ expect(pages.third.content).to eq("This is an index2")
+ end
end
end
@@ -144,7 +186,7 @@ describe ProjectWiki do
end
after do
- subject.pages.each { |page| destroy_page(page.page) }
+ subject.list_pages.each { |page| destroy_page(page.page) }
end
it "returns the latest version of the page if it exists" do
@@ -195,7 +237,7 @@ describe ProjectWiki do
end
after do
- subject.pages.each { |page| destroy_page(page.page) }
+ subject.list_pages.each { |page| destroy_page(page.page) }
end
it 'finds the page defined as _sidebar' do
@@ -242,12 +284,12 @@ describe ProjectWiki do
describe "#create_page" do
after do
- destroy_page(subject.pages.first.page)
+ destroy_page(subject.list_pages.first.page)
end
it "creates a new wiki page" do
expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.pages.count).to eq(1)
+ expect(subject.list_pages.count).to eq(1)
end
it "returns false when a duplicate page exists" do
@@ -262,7 +304,7 @@ describe ProjectWiki do
it "sets the correct commit message" do
subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.pages.first.page.version.message).to eq("commit message")
+ expect(subject.list_pages.first.page.version.message).to eq("commit message")
end
it 'sets the correct commit email' do
@@ -293,7 +335,7 @@ describe ProjectWiki do
format: :markdown,
message: "updated page"
)
- @page = subject.pages.first.page
+ @page = subject.list_pages(load_content: true).first.page
end
after do
@@ -337,7 +379,7 @@ describe ProjectWiki do
it "deletes the page" do
subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
+ expect(subject.list_pages.count).to eq(0)
end
it 'sets the correct commit email' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 3f5d285bc2c..a6c3d5756aa 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -217,6 +217,25 @@ describe Repository do
expect(result.size).to eq(0)
end
+
+ context 'with a commit with invalid UTF-8 path' do
+ def create_commit_with_invalid_utf8_path
+ rugged = rugged_repo(repository)
+ blob_id = Rugged::Blob.from_buffer(rugged, "some contents")
+ tree_builder = Rugged::Tree::Builder.new(rugged)
+ tree_builder.insert({ oid: blob_id, name: "hello\x80world", filemode: 0100644 })
+ tree_id = tree_builder.write
+ user = { email: "jcai@gitlab.com", time: Time.now, name: "John Cai" }
+
+ Rugged::Commit.create(rugged, message: 'some commit message', parents: [rugged.head.target.oid], tree: tree_id, committer: user, author: user)
+ end
+
+ it 'does not raise an error' do
+ commit = create_commit_with_invalid_utf8_path
+
+ expect { repository.list_last_commits_for_tree(commit, '.', offset: 0) }.not_to raise_error
+ end
+ end
end
describe '#last_commit_for_path' do
@@ -1618,6 +1637,7 @@ describe Repository do
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
+ :metrics_dashboard_paths,
:xcode_project?
])
@@ -2468,4 +2488,69 @@ describe Repository do
repository.merge_base('master', 'fix')
end
end
+
+ describe '#create_if_not_exists' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ it 'creates the repository if it did not exist' do
+ expect { repository.create_if_not_exists }.to change { repository.exists? }.from(false).to(true)
+ end
+
+ it 'calls out to the repository client to create a repo' do
+ expect(repository.raw.gitaly_repository_client).to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+
+ context 'it does nothing if the repository already existed' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does nothing if the repository already existed' do
+ expect(repository.raw.gitaly_repository_client).not_to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+ end
+
+ context 'when the repository exists but the cache is not up to date' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does not raise errors' do
+ allow(repository).to receive(:exists?).and_return(false)
+ expect(repository.raw).to receive(:create_repository).and_call_original
+
+ expect { repository.create_if_not_exists }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#blobs_metadata" do
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ def expect_metadata_blob(thing)
+ expect(thing).to be_a(Blob)
+ expect(thing.data).to be_empty
+ end
+
+ it "returns blob metadata in batch for HEAD" do
+ result = repository.blobs_metadata(["bar/branch-test.txt", "README.md", "does/not/exist"])
+
+ expect_metadata_blob(result.first)
+ expect_metadata_blob(result.second)
+ expect(result.size).to eq(2)
+ end
+
+ it "returns blob metadata for a specified ref" do
+ result = repository.blobs_metadata(["files/ruby/feature.rb"], "feature")
+
+ expect_metadata_blob(result.first)
+ end
+
+ it "performs a single gitaly call", :request_store do
+ expect { repository.blobs_metadata(["bar/branch-test.txt", "readme.txt", "does/not/exist"]) }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index d5c85c11195..520a06e138e 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -44,47 +44,49 @@ describe WikiPage do
WikiDirectory.new('dir_2', pages)
end
- context 'sort by title' do
- let(:grouped_entries) { described_class.group_by_directory(wiki.pages) }
- let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] }
-
- 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)
+ context "#list_pages" do
+ context 'sort by title' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) }
+ let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] }
+
+ 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
- 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, page_dir_2, dir_2, page_6] }
+ context 'sort by created_at' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) }
+ let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] }
- 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)
+ 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)
+ expect(slugs).to match_array(expected_slugs)
+ end
end
end
- end
- 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', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
+ 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', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
- grouped_entries = described_class.group_by_directory(wiki.pages)
+ grouped_entries = described_class.group_by_directory(wiki.list_pages)
- actual_order =
- grouped_entries.map do |page_or_dir|
- get_slugs(page_or_dir)
- end
- .flatten
- expect(actual_order).to eq(expected_order)
+ actual_order =
+ grouped_entries.map do |page_or_dir|
+ get_slugs(page_or_dir)
+ end
+ .flatten
+ expect(actual_order).to eq(expected_order)
+ end
end
end
end
@@ -386,7 +388,7 @@ describe WikiPage do
it "deletes the page" do
@page.delete
- expect(wiki.pages).to be_empty
+ expect(wiki.list_pages).to be_empty
end
it "returns true" do
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index e5f08aeb1fa..451dc88880c 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -439,6 +439,52 @@ describe MergeRequestPresenter do
end
end
+ describe '#source_branch_link' do
+ subject { presenter.source_branch_link }
+
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ context 'when source branch exists' do
+ it 'returns link' do
+ allow(resource).to receive(:source_branch_exists?) { true }
+
+ is_expected
+ .to eq("<a class=\"ref-name\" href=\"#{presenter.source_branch_commits_path}\">#{presenter.source_branch}</a>")
+ end
+ end
+
+ context 'when source branch does not exist' do
+ it 'returns text' do
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to eq("<span class=\"ref-name\">#{presenter.source_branch}</span>")
+ end
+ end
+ end
+
+ describe '#target_branch_link' do
+ subject { presenter.target_branch_link }
+
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ context 'when target branch exists' do
+ it 'returns link' do
+ allow(resource).to receive(:target_branch_exists?) { true }
+
+ is_expected
+ .to eq("<a class=\"ref-name\" href=\"#{presenter.target_branch_commits_path}\">#{presenter.target_branch}</a>")
+ end
+ end
+
+ context 'when target branch does not exist' do
+ it 'returns text' do
+ allow(resource).to receive(:target_branch_exists?) { false }
+
+ is_expected.to eq("<span class=\"ref-name\">#{presenter.target_branch}</span>")
+ end
+ end
+ end
+
describe '#source_branch_with_namespace_link' do
subject do
described_class.new(resource, current_user: user).source_branch_with_namespace_link
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 065b16c6221..018691e8099 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -164,139 +164,4 @@ describe API::Events do
expect(json_response['message']).to eq('404 User Not Found')
end
end
-
- describe 'GET /projects/:id/events' do
- context 'when unauthenticated ' do
- it 'returns 404 for private project' do
- get api("/projects/#{private_project.id}/events")
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 200 status for a public project' do
- public_project = create(:project, :public)
-
- get api("/projects/#{public_project.id}/events")
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context 'with inaccessible events' do
- let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
- let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
- let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
- let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
- let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
-
- it 'returns only accessible events' do
- get api("/projects/#{public_project.id}/events", non_member)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(1)
- end
-
- it 'returns all events when the user has access' do
- get api("/projects/#{public_project.id}/events", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(2)
- end
- end
-
- context 'pagination' do
- let(:public_project) { create(:project, :public) }
-
- before do
- create(:event,
- project: public_project,
- target: create(:issue, project: public_project, title: 'Issue 1'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-10'))
- create(:event,
- project: public_project,
- target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-11'))
- create(:event,
- project: public_project,
- target: create(:issue, project: public_project, title: 'Issue 2'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-12'))
- end
-
- it 'correctly returns the second page without inaccessible events' do
- get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 2 }
-
- titles = json_response.map { |event| event['target_title'] }
-
- expect(titles.first).to eq('Issue 1')
- expect(titles).not_to include('Confidential event')
- end
-
- it 'correctly returns the first page without inaccessible events' do
- get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 1 }
-
- titles = json_response.map { |event| event['target_title'] }
-
- expect(titles.first).to eq('Issue 2')
- expect(titles).not_to include('Confidential event')
- end
- end
-
- context 'when not permitted to read' do
- it 'returns 404' do
- get api("/projects/#{private_project.id}/events", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when authenticated' do
- it 'returns project events' do
- get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- end
-
- it 'returns 404 if project does not exist' do
- get api("/projects/1234/events", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when exists some events' do
- let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
-
- before do
- create_event(merge_request1)
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.count
-
- create_event(merge_request2)
-
- expect do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.not_to exceed_all_query_limit(control_count)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response.size).to eq(2)
- expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
- end
-
- def create_event(target)
- create(:event, project: private_project, author: user, target: target)
- end
- end
- end
end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
new file mode 100644
index 00000000000..8ff95cc9af2
--- /dev/null
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Based on spec/requests/api/groups_spec.rb
+# Should follow closely in order to ensure all situations are covered
+describe 'getting group information' do
+ include GraphqlHelpers
+ include UploadHelpers
+
+ let(:user1) { create(:user, can_create_group: false) }
+ let(:user2) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:public_group) { create(:group, :public) }
+ let(:private_group) { create(:group, :private) }
+
+ # similar to the API "GET /groups/:id"
+ describe "Query group(fullPath)" do
+ def group_query(group)
+ graphql_query_for('group', 'fullPath' => group.full_path)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(group_query(public_group))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns nil for a private group' do
+ post_graphql(group_query(private_group))
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it 'returns a public group' do
+ post_graphql(group_query(public_group))
+
+ expect(graphql_data['group']).not_to be_nil
+ end
+ end
+
+ context "when authenticated as user" do
+ let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+ let!(:group2) { create(:group, :private) }
+
+ before do
+ group1.add_owner(user1)
+ group2.add_owner(user2)
+ end
+
+ it "returns one of user1's groups" do
+ project = create(:project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
+ post_graphql(group_query(group1), current_user: user1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(graphql_data['group']['id']).to eq(group1.id.to_s)
+ expect(graphql_data['group']['name']).to eq(group1.name)
+ expect(graphql_data['group']['path']).to eq(group1.path)
+ expect(graphql_data['group']['description']).to eq(group1.description)
+ expect(graphql_data['group']['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level))
+ expect(graphql_data['group']['avatarUrl']).to eq(group1.avatar_url(only_path: false))
+ expect(graphql_data['group']['webUrl']).to eq(group1.web_url)
+ expect(graphql_data['group']['requestAccessEnabled']).to eq(group1.request_access_enabled)
+ expect(graphql_data['group']['fullName']).to eq(group1.full_name)
+ expect(graphql_data['group']['fullPath']).to eq(group1.full_path)
+ expect(graphql_data['group']['parentId']).to eq(group1.parent_id)
+ end
+
+ it "does not return a non existing group" do
+ query = graphql_query_for('group', 'fullPath' => '1328')
+
+ post_graphql(query, current_user: user1)
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it "does not return a group not attached to user1" do
+ private_group.add_owner(user2)
+
+ post_graphql(group_query(private_group), current_user: user1)
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it 'avoids N+1 queries' do
+ post_graphql(group_query(group1), current_user: admin)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(group_query(group1), current_user: admin)
+ end.count
+
+ create(:project, namespace: group1)
+
+ expect do
+ post_graphql(group_query(group1), current_user: admin)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns any existing group" do
+ post_graphql(group_query(private_group), current_user: admin)
+
+ expect(graphql_data['group']['name']).to eq(private_group.name)
+ end
+
+ it "does not return a non existing group" do
+ query = graphql_query_for('group', 'fullPath' => '1328')
+ post_graphql(query, current_user: admin)
+
+ expect(graphql_data['group']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index ed2ef4c730b..c14507de186 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -863,7 +863,7 @@ describe API::Jobs do
end
describe 'POST /projects/:id/jobs/:job_id/retry' do
- let(:job) { create(:ci_build, :canceled, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :failed, pipeline: pipeline) }
before do
post api("/projects/#{project.id}/jobs/#{job.id}/retry", api_user)
@@ -873,7 +873,7 @@ describe API::Jobs do
context 'user with :update_build permission' do
it 'retries non-running job' do
expect(response).to have_gitlab_http_status(201)
- expect(project.builds.first.status).to eq('canceled')
+ expect(project.builds.first.status).to eq('failed')
expect(json_response['status']).to eq('pending')
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 79edbb301f2..48869cab4da 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -236,7 +236,7 @@ describe API::Members do
params: { user_id: stranger.id, access_level: Member::REPORTER }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
+ expect(json_response['message']['access_level']).to eq(["should be greater than or equal to Developer inherited membership from group #{parent.name}"])
end
it 'creates the member if group level is lower', :nested_groups do
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
new file mode 100644
index 00000000000..43df9993eb9
--- /dev/null
+++ b/spec/requests/api/project_events_spec.rb
@@ -0,0 +1,156 @@
+require 'spec_helper'
+
+describe API::ProjectEvents do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
+ let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+
+ describe 'GET /projects/:id/events' do
+ context 'when unauthenticated ' do
+ it 'returns 404 for private project' do
+ get api("/projects/#{private_project.id}/events")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 200 status for a public project' do
+ public_project = create(:project, :public)
+
+ get api("/projects/#{public_project.id}/events")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'with inaccessible events' do
+ let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
+ let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
+ let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
+ let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
+ let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
+
+ it 'returns only accessible events' do
+ get api("/projects/#{public_project.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns all events when the user has access' do
+ get api("/projects/#{public_project.id}/events", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(2)
+ end
+ end
+
+ context 'pagination' do
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ create(:event,
+ project: public_project,
+ target: create(:issue, project: public_project, title: 'Issue 1'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-10'))
+ create(:event,
+ project: public_project,
+ target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-11'))
+ create(:event,
+ project: public_project,
+ target: create(:issue, project: public_project, title: 'Issue 2'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-12'))
+ end
+
+ it 'correctly returns the second page without inaccessible events' do
+ get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 2 }
+
+ titles = json_response.map { |event| event['target_title'] }
+
+ expect(titles.first).to eq('Issue 1')
+ expect(titles).not_to include('Confidential event')
+ end
+
+ it 'correctly returns the first page without inaccessible events' do
+ get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 1 }
+
+ titles = json_response.map { |event| event['target_title'] }
+
+ expect(titles.first).to eq('Issue 2')
+ expect(titles).not_to include('Confidential event')
+ end
+ end
+
+ context 'when not permitted to read' do
+ it 'returns 404' do
+ get api("/projects/#{private_project.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns project events' do
+ get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns 404 if project does not exist' do
+ get api("/projects/1234/events", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ context 'when the requesting token does not have "api" scope' do
+ let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
+
+ it 'returns a "403" response' do
+ get api("/projects/#{private_project.id}/events", personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when exists some events' do
+ let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+
+ before do
+ create_event(merge_request1)
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
+ end.count
+
+ create_event(merge_request2)
+
+ expect do
+ get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
+ end.not_to exceed_all_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
+ end
+
+ def create_event(target)
+ create(:event, project: private_project, author: user, target: target)
+ end
+ end
+ end
+end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 53271550e8b..00ca394a50b 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -133,5 +133,19 @@ describe "Groups", "routing" do
let(:resource) { create(:group, parent: parent, path: 'activity') }
end
end
+
+ describe 'subgroup "boards"' do
+ it 'shows group show page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq/boards', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/boards')).to route_to('groups#show', id: 'gitlabhq/boards')
+ end
+
+ it 'shows boards index page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/-/boards')).to route_to('groups/boards#index', group_id: 'gitlabhq')
+ end
+ end
end
end
diff --git a/spec/rubocop/cop/include_action_view_context_spec.rb b/spec/rubocop/cop/include_action_view_context_spec.rb
new file mode 100644
index 00000000000..c888555b54f
--- /dev/null
+++ b/spec/rubocop/cop/include_action_view_context_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/include_action_view_context'
+
+describe RuboCop::Cop::IncludeActionViewContext do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when `ActionView::Context` is included' do
+ let(:source) { 'include ActionView::Context' }
+ let(:correct_source) { 'include ::Gitlab::ActionViewOutput::Context' }
+
+ it 'registers an offense' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['ActionView::Context'])
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(source)
+
+ expect(autocorrected).to eq(correct_source)
+ end
+ end
+
+ context 'when `ActionView::Context` is not included' do
+ it 'registers no offense' do
+ inspect_source('include Context')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 7e151c3744e..f38a18fcf59 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -21,6 +21,10 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to be_nil
end
+ it 'has can_uninstall' do
+ expect(subject[:can_uninstall]).to be_falsey
+ end
+
context 'non-helm application' do
let(:application) { build(:clusters_applications_runner, version: '0.0.0') }
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 31b8d540356..890fa5bc009 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -105,6 +105,82 @@ describe Ci::StopEnvironmentsService do
end
end
+ describe '#execute_for_merge_request' do
+ subject { service.execute_for_merge_request(merge_request) }
+
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ let(:project) { merge_request.project }
+ let(:user) { create(:user) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+ let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) }
+
+ before do
+ review_job.deployment.success!
+ end
+
+ it 'has active environment at first' do
+ expect(pipeline.environments.first).to be_available
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'stops the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_stopped
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'does not stop the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_available
+ end
+ end
+
+ context 'when pipeline is not associated with environments' do
+ let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ it 'does not raise exception' do
+ expect { subject }.not_to raise_exception
+ end
+ end
+
+ context 'when pipeline is not a pipeline for merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ it 'does not stop the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_available
+ end
+ end
+ end
+
def expect_environment_stopped_on(branch)
expect_any_instance_of(Environment)
.to receive(:stop!)
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 8ad90aaf720..a54bd85a11a 100644
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -18,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context "when phase is #{a_phase}" do
- context 'when not timeouted' do
+ context 'when not timed_out' do
it 'reschedule a new check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod)
@@ -113,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timeouted, :updating) }
+ let(:application) { create(:clusters_applications_helm, :timed_out, :updating) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
@@ -174,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timeouted) }
+ let(:application) { create(:clusters_applications_helm, :timed_out) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
new file mode 100644
index 00000000000..9ab83d913f5
--- /dev/null
+++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::CheckUninstallProgressService do
+ RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
+
+ let(:application) { create(:clusters_applications_prometheus, :uninstalling) }
+ let(:service) { described_class.new(application) }
+ let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
+ let(:errors) { nil }
+ let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
+
+ before do
+ allow(service).to receive(:installation_errors).and_return(errors)
+ allow(service).to receive(:remove_installation_pod)
+ end
+
+ shared_examples 'a not yet terminated installation' do |a_phase|
+ let(:phase) { a_phase }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ context "when phase is #{a_phase}" do
+ context 'when not timed_out' do
+ it 'reschedule a new check' do
+ expect(worker_class).to receive(:perform_in).once
+ expect(service).not_to receive(:remove_installation_pod)
+
+ expect do
+ service.execute
+
+ application.reload
+ end.not_to change(application, :status)
+
+ expect(application.status_reason).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'when application is installing' do
+ RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
+
+ context 'when installation POD succeeded' do
+ let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+
+ it 'destroys the application' do
+ expect(worker_class).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_destroyed
+ end
+
+ context 'an error occurs while destroying' do
+ before do
+ expect(application).to receive(:destroy!).once.and_raise("destroy failed")
+ end
+
+ it 'still removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+
+ it 'makes the application uninstall_errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed')
+ end
+ end
+ end
+
+ context 'when installation POD failed' do
+ let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
+ let(:errors) { 'test installation failed' }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.')
+ end
+ end
+
+ context 'when timed out' do
+ let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'make the application errored' do
+ expect(worker_class).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.')
+ end
+ end
+
+ 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(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
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Kubernetes error: 401')
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/destroy_service_spec.rb b/spec/services/clusters/applications/destroy_service_spec.rb
new file mode 100644
index 00000000000..8d9dc6a0f11
--- /dev/null
+++ b/spec/services/clusters/applications/destroy_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::DestroyService, '#execute' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:user) { create(:user) }
+ let(:params) { { application: 'prometheus' } }
+ let(:service) { described_class.new(cluster, user, params) }
+ let(:test_request) { double }
+ let(:worker_class) { Clusters::Applications::UninstallWorker }
+
+ subject { service.execute(test_request) }
+
+ before do
+ allow(worker_class).to receive(:perform_async)
+ end
+
+ context 'application is not installed' do
+ it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
+ .and not_change { Clusters::Applications::Prometheus.count }
+ .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
+ end
+ end
+
+ context 'application is installed' do
+ context 'application is schedulable' do
+ let!(:application) do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'makes application scheduled!' do
+ subject
+
+ expect(application.reload).to be_scheduled
+ end
+
+ it 'schedules UninstallWorker' do
+ expect(worker_class).to receive(:perform_async).with(application.name, application.id)
+
+ subject
+ end
+ end
+
+ context 'application is not schedulable' do
+ let!(:application) do
+ create(:clusters_applications_prometheus, :updating, cluster: cluster)
+ end
+
+ it 'raises StateMachines::InvalidTransition' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { StateMachines::InvalidTransition }
+ .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb
new file mode 100644
index 00000000000..16497d752b2
--- /dev/null
+++ b/spec/services/clusters/applications/uninstall_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::UninstallService, '#execute' do
+ let(:application) { create(:clusters_applications_prometheus, :scheduled) }
+ let(:service) { described_class.new(application) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
+ let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
+
+ before do
+ allow(service).to receive(:helm_api).and_return(helm_client)
+ end
+
+ context 'when there are no errors' do
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand))
+ allow(worker_class).to receive(:perform_in).and_return(nil)
+ end
+
+ it 'make the application to be uninstalling' do
+ expect(application.cluster).not_to be_nil
+ service.execute
+
+ expect(application).to be_uninstalling
+ end
+
+ it 'schedule async installation status check' do
+ expect(worker_class).to receive(:perform_in).once
+
+ service.execute
+ end
+ end
+
+ context 'when k8s cluster communication fails' do
+ let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
+
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).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_uninstall_errored
+ expect(application.status_reason).to match('Kubernetes error: 500')
+ end
+ end
+
+ context 'a non kubernetes error happens' do
+ let(:application) { create(:clusters_applications_prometheus, :scheduled) }
+ let(:error) { StandardError.new('something bad happened') }
+
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).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
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Failed to uninstall.')
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index aa7dfda4950..ffa612cf315 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -74,6 +74,14 @@ describe MergeRequests::CloseService do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
+ it 'clean up environments for the merge request' do
+ expect_next_instance_of(Ci::StopEnvironmentsService) do |service|
+ expect(service).to receive(:execute_for_merge_request).with(merge_request)
+ end
+
+ described_class.new(project, user).execute(merge_request)
+ end
+
context 'current user is not authorized to close merge request' do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 7b87913ab8b..ffc86f68469 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -62,5 +62,13 @@ describe MergeRequests::PostMergeService do
expect(merge_request.reload).to be_merged
end
+
+ it 'clean up environments for the merge request' do
+ expect_next_instance_of(Ci::StopEnvironmentsService) do |service|
+ expect(service).to receive(:execute_for_merge_request).with(merge_request)
+ end
+
+ described_class.new(project, user).execute(merge_request)
+ end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 7f233a52f50..d9f9ede8ecd 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -5,15 +5,11 @@ require 'spec_helper'
describe Projects::ImportService do
let!(:project) { create(:project) }
let(:user) { project.creator }
- let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
subject { described_class.new(project, user) }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute)
- allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
end
describe '#async?' do
@@ -77,7 +73,6 @@ describe Projects::ImportService do
context 'when repository creation succeeds' do
it 'does not download lfs files' do
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -114,7 +109,6 @@ describe Projects::ImportService do
context 'when repository import scheduled' do
it 'does not download lfs objects' do
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -130,7 +124,7 @@ describe Projects::ImportService do
it 'succeeds if repository import is successful' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({})
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :success)
result = subject.execute
@@ -146,6 +140,19 @@ describe Projects::ImportService do
expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]"
end
+ context 'when lfs import fails' do
+ it 'logs the error' do
+ error_message = 'error message'
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
+ expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message)
+ expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}")
+
+ subject.execute
+ end
+ end
+
context 'when repository import scheduled' do
before do
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
@@ -155,10 +162,7 @@ describe Projects::ImportService do
it 'downloads lfs objects if lfs_enabled is enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(true)
- service = double
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
- expect(service).to receive(:execute).twice
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute)
subject.execute
end
@@ -166,7 +170,6 @@ describe Projects::ImportService do
it 'does not download lfs objects if lfs_enabled is not enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(false)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -208,7 +211,6 @@ describe Projects::ImportService do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -216,13 +218,22 @@ describe Projects::ImportService do
it 'does not have a custom repository importer downloads lfs objects' do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
- service = double
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
- expect(service).to receive(:execute).twice
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute)
subject.execute
end
+
+ context 'when lfs import fails' do
+ it 'logs the error' do
+ error_message = 'error message'
+
+ allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message)
+ expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}")
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index f1c0f5b9576..d8427d0bf78 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadLinkListService do
@@ -85,7 +84,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
end
describe '#get_download_links' do
- it 'raise errorif request fails' do
+ it 'raise error if request fails' do
allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request'))
expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index cde3f2d6155..f4470b50753 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadService do
diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
index 5c9ca99df7c..7ca20a6d751 100644
--- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
@@ -1,148 +1,63 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsImportService do
+ let(:project) { create(:project) }
+ let(:user) { project.creator }
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
- let(:group) { create(:group, lfs_enabled: true)}
- let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
- let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
- let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
- let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
- let(:all_oids) { existing_lfs_objects.merge(oids) }
- let(:remote_uri) { URI.parse(lfs_endpoint) }
-
- subject { described_class.new(project) }
-
- before do
- allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
- end
-
- describe '#execute' do
- context 'when no lfs pointer is linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
- end
-
- it 'retrieves all lfs pointers in the project repository' do
- expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
-
- subject.execute
- end
-
- it 'links existent lfs objects to the project' do
- expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
-
- subject.execute
- end
- it 'retrieves the download links of non existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+ subject { described_class.new(project, user) }
- subject.execute
- end
+ context 'when lfs is enabled for the project' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
end
- context 'when some lfs objects are linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- end
+ it 'downloads lfs objects' do
+ service = double
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
+ expect(service).to receive(:execute).twice
- it 'retrieves the download links of non existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+ result = subject.execute
- subject.execute
- end
+ expect(result[:status]).to eq :success
end
- context 'when all lfs objects are linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
- end
+ context 'when no downloadable lfs object links' do
+ it 'does not call LfsDownloadService' do
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return({})
+ expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
- it 'retrieves no download links' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+ result = subject.execute
- expect(subject.execute).to be_empty
+ expect(result[:status]).to eq :success
end
end
- context 'when lfsconfig file exists' do
- before do
- allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
- end
-
- context 'when url points to the same import url host' do
- let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
- let(:service) { double }
-
- before do
- allow(service).to receive(:execute)
- end
- it 'downloads lfs object using the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
-
- subject.execute
- end
-
- context 'when import url has credentials' do
- let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
-
- it 'adds the credentials to the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
- .and_return(service)
-
- subject.execute
- end
-
- context 'when url has its own credentials' do
- let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
+ context 'when an exception is raised' do
+ it 'returns error' do
+ error_message = "error message"
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_raise(StandardError, error_message)
- it 'does not add the import url credentials' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: remote_uri)
- .and_return(service)
+ result = subject.execute
- subject.execute
- end
- end
- end
- end
-
- context 'when url points to a third party service' do
- let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
-
- it 'disables lfs from the project' do
- expect(project.lfs_enabled?).to be_truthy
-
- subject.execute
-
- expect(project.lfs_enabled?).to be_falsey
- end
-
- it 'does not download anything' do
- expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
-
- subject.execute
- end
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq error_message
end
end
end
- describe '#default_endpoint_uri' do
- let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+ context 'when lfs is not enabled for the project' do
+ it 'does not download lfs objects' do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ expect(Projects::LfsPointers::LfsObjectDownloadListService).not_to receive(:new)
+ expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
+
+ result = subject.execute
- it 'adds suffix .git if the url does not have it' do
- expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ expect(result[:status]).to eq :success
end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index 5caa9de732e..849601c4a63 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsLinkService do
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
new file mode 100644
index 00000000000..9dac29765a2
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsObjectDownloadListService do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
+ let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
+ let(:group) { create(:group, lfs_enabled: true)}
+ let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
+ let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+ let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
+ let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
+ let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
+ let(:all_oids) { existing_lfs_objects.merge(oids) }
+ let(:remote_uri) { URI.parse(lfs_endpoint) }
+
+ subject { described_class.new(project) }
+
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
+ end
+
+ describe '#execute' do
+ context 'when no lfs pointer is linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
+ end
+
+ it 'retrieves all lfs pointers in the project repository' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'links existent lfs objects to the project' do
+ expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when some lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when all lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
+ end
+
+ it 'retrieves no download links' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+
+ expect(subject.execute).to be_empty
+ end
+ end
+
+ context 'when lfsconfig file exists' do
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
+ end
+
+ context 'when url points to the same import url host' do
+ let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
+ let(:service) { double }
+
+ before do
+ allow(service).to receive(:execute)
+ end
+
+ it 'downloads lfs object using the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
+
+ subject.execute
+ end
+
+ context 'when import url has credentials' do
+ let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
+
+ it 'adds the credentials to the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
+ .and_return(service)
+
+ subject.execute
+ end
+
+ context 'when url has its own credentials' do
+ let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
+
+ it 'does not add the import url credentials' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: remote_uri)
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when url points to a third party service' do
+ let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
+
+ it 'disables lfs from the project' do
+ expect(project.lfs_enabled?).to be_truthy
+
+ subject.execute
+
+ expect(project.lfs_enabled?).to be_falsey
+ end
+
+ it 'does not download anything' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ describe '#default_endpoint_uri' do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+
+ it 'adds suffix .git if the url does not have it' do
+ expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ end
+ end
+end
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 86b1ec83f50..7e765659b9d 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -11,6 +11,56 @@ describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) }
describe '#execute' do
+ context 'metrics dashboard setting' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'http://gitlab.com'
+ }
+ }
+ end
+
+ context 'without existing metrics dashboard setting' do
+ it 'creates a setting' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+ end
+
+ context 'with existing metrics dashboard setting' do
+ before do
+ create(:project_metrics_setting, project: project)
+ end
+
+ it 'updates the settings' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+
+ context 'with blank external_dashboard_url in params' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: ''
+ }
+ }
+ end
+
+ it 'destroys the metrics_setting entry in DB' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting).to be_nil
+ end
+ end
+ end
+ end
+
context 'error tracking' do
context 'with existing error tracking setting' do
let(:params) do
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 1447b9d4126..2a553e18807 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -75,6 +75,13 @@ describe Todos::Destroy::EntityLeaveService do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
+ it 'enqueues the PrivateFeaturesWorker' do
+ expect(TodosDestroyer::PrivateFeaturesWorker)
+ .to receive(:perform_async).with(project.id, user.id)
+
+ subject
+ end
+
context 'confidential issues' do
context 'when a user is not an author of confidential issue' do
it 'removes only confidential issues todos' do
@@ -246,6 +253,13 @@ describe Todos::Destroy::EntityLeaveService do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
+ it 'enqueues the PrivateFeaturesWorker' do
+ expect(TodosDestroyer::PrivateFeaturesWorker)
+ .to receive(:perform_async).with(project.id, user.id)
+
+ subject
+ end
+
context 'when user is not member' do
it 'removes only confidential issues todos' do
expect { subject }.to change { Todo.count }.from(5).to(4)
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb
index c664bac39fc..7dc52f6816a 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/update_deployment_service_spec.rb
@@ -22,6 +22,7 @@ describe UpdateDeploymentService do
subject(:service) { described_class.new(deployment) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment
end
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index e5c7b5bb9a7..f2b3b44d223 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -57,12 +57,12 @@ describe VerifyPagesDomainService do
expect(domain).not_to be_verified
end
- it 'disables domain and shedules it for removal' do
- Timecop.freeze do
- service.execute
- expect(domain).not_to be_enabled
- expect(domain.remove_at).to be_within(1.second).of(1.week.from_now)
- end
+ it 'disables domain and shedules it for removal in 1 week' do
+ service.execute
+
+ expect(domain).not_to be_enabled
+
+ expect(domain.remove_at).to be_like_time(7.days.from_now)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8ca4c172707..fbc5fcea7b9 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -53,6 +53,7 @@ RSpec.configure do |config|
config.display_try_failure_messages = true
config.infer_spec_type_from_file_location!
+ config.full_backtrace = true
config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
location = metadata[:location]
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 2f4e6e4c934..b49d743fb9a 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -61,7 +61,14 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = input.map { |name, value| [GraphqlHelpers.fieldnamerize(name), value] }.to_h
- { input_variable_name_for_mutation(name) => graphql_input }.to_json
+ result = { input_variable_name_for_mutation(name) => graphql_input }
+
+ # Avoid trying to serialize multipart data into JSON
+ if graphql_input.values.none? { |value| io_value?(value) }
+ result.to_json
+ else
+ result
+ end
end
def input_variable_name_for_mutation(mutation_name)
@@ -162,6 +169,10 @@ module GraphqlHelpers
field.arguments.values.any? { |argument| argument.type.non_null? }
end
+ def io_value?(value)
+ Array.wrap(value).any? { |v| v.respond_to?(:to_io) }
+ end
+
def field_type(field)
field_type = field.type
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
new file mode 100644
index 00000000000..1f36b0e217c
--- /dev/null
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module MetricsDashboardHelpers
+ def project_with_dashboard(dashboard_path, dashboard_yml = nil)
+ dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')
+
+ create(:project, :custom_repo, files: { dashboard_path => dashboard_yml })
+ end
+
+ def delete_project_dashboard(project, user, dashboard_path)
+ project.repository.delete_file(
+ user,
+ dashboard_path,
+ branch_name: 'master',
+ message: 'Delete dashboard'
+ )
+
+ project.repository.refresh_method_caches([:metrics_dashboard])
+ end
+
+ shared_examples_for 'misconfigured dashboard service response' do |status_code|
+ it 'returns an appropriate message and status code' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:message, :http_status, :status)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(status_code)
+ end
+ end
+
+ shared_examples_for 'valid dashboard service response' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+ it 'returns a json representation of the dashboard' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:dashboard, :status)
+ expect(result[:status]).to eq(:success)
+
+ expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+ end
+ end
+end
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 4af90f4af79..44d95a029af 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -11,6 +11,8 @@ module RepoHelpers
# blob.path # => 'files/js/commit.js.coffee'
# blob.data # => 'class Commit...'
#
+ # Build the options hash that's passed to Rugged::Commit#create
+
def sample_blob
OpenStruct.new(
oid: '5f53439ca4b009096571d3c8bc3d09d30e7431b3',
@@ -129,4 +131,80 @@ eos
file_content: content
).execute
end
+
+ def commit_options(repo, index, target, ref, message)
+ options = {}
+ options[:tree] = index.write_tree(repo)
+ options[:author] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:committer] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:message] ||= message
+ options[:parents] = repo.empty? ? [] : [target].compact
+ options[:update_ref] = ref
+
+ options
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of CHANGELOG with a single new line of text.
+ def new_commit_edit_old_file(repo)
+ oid = repo.write("I replaced the changelog with this text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(
+ repo,
+ index,
+ repo.head.target,
+ "HEAD",
+ "Edit CHANGELOG in its original location"
+ )
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of the specified file_path with new text.
+ def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
+ oid = repo.write(text, :blob)
+ index = repo.index
+ index.read_tree(branch.target.tree)
+ index.add(path: file_path, oid: oid, mode: 0100644)
+ options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of encoding/CHANGELOG with new text.
+ def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
+ branch = repo.branches[branch_name]
+ new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
+ # CHANGELOG file to the encoding/ directory.
+ def new_commit_move_file(repo)
+ blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
+ file_content = repo.lookup(blob_oid).content
+ oid = repo.write(file_content, :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+ index.remove("CHANGELOG")
+
+ options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
end
diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb
new file mode 100644
index 00000000000..ede16d1c1e2
--- /dev/null
+++ b/spec/support/protected_branch_helpers.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ProtectedBranchHelpers
+ def set_allowed_to(operation, option = 'Maintainers', form: '.js-new-protected-branch')
+ within form do
+ select_elem = find(".js-allowed-to-#{operation}")
+ select_elem.click
+
+ wait_for_requests
+
+ within('.dropdown-content') do
+ Array(option).each { |opt| click_on(opt) }
+ end
+
+ # Enhanced select is used in EE, therefore an extra click is needed.
+ select_elem.click if select_elem['aria-expanded'] == 'true'
+ end
+ end
+
+ def set_protected_branch_name(branch_name)
+ find('.js-protected-branch-select').click
+ find('.dropdown-input-field').set(branch_name)
+ click_on("Create wildcard #{branch_name}")
+ end
+
+ def set_defaults
+ set_allowed_to('merge')
+ set_allowed_to('push')
+ end
+end
diff --git a/spec/support/protected_tag_helpers.rb b/spec/support/protected_tag_helpers.rb
new file mode 100644
index 00000000000..fe9be856286
--- /dev/null
+++ b/spec/support/protected_tag_helpers.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative 'protected_branch_helpers'
+
+module ProtectedTagHelpers
+ include ::ProtectedBranchHelpers
+
+ def set_allowed_to(operation, option = 'Maintainers', form: '.new-protected-tag')
+ super
+ end
+
+ def set_protected_tag_name(tag_name)
+ find('.js-protected-tag-select').click
+ find('.dropdown-input-field').set(tag_name)
+ click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
+ end
+end
diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb
index e7ec24c5b7e..421303c97be 100644
--- a/spec/support/shared_examples/application_setting_examples.rb
+++ b/spec/support/shared_examples/application_setting_examples.rb
@@ -249,4 +249,43 @@ RSpec.shared_examples 'application settings examples' do
expect(setting.password_authentication_enabled_for_web?).to be_falsey
end
+
+ describe 'sentry settings' do
+ context 'when the sentry settings are not set in gitlab.yml' do
+ it 'fallbacks to the settings in the database' do
+ setting.sentry_enabled = true
+ setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ setting.clientside_sentry_enabled = true
+ setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil)
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(nil)
+
+ expect(setting.sentry_enabled).to eq true
+ expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ expect(setting.clientside_sentry_enabled).to eq true
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+ end
+ end
+
+ context 'when the sentry settings are set in gitlab.yml' do
+ it 'does not fallback to the settings in the database' do
+ setting.sentry_enabled = false
+ setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ setting.clientside_sentry_enabled = false
+ setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42')
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43')
+
+ expect(setting).not_to receive(:read_attribute)
+ expect(setting.sentry_enabled).to eq true
+ expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42'
+ expect(setting.clientside_sentry_enabled).to eq true
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43'
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/chat_service_spec.rb b/spec/support/shared_examples/models/chat_service_spec.rb
index cf1d52a9616..0a302e7d030 100644
--- a/spec/support/shared_examples/models/chat_service_spec.rb
+++ b/spec/support/shared_examples/models/chat_service_spec.rb
@@ -25,6 +25,12 @@ shared_examples_for "chat service" do |service_name|
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -64,7 +70,7 @@ shared_examples_for "chat service" do |service_name|
context "with not default branch" do
let(:sample_data) do
- Gitlab::DataBuilder::Push.build(project, user, nil, nil, "not-the-default-branch")
+ Gitlab::DataBuilder::Push.build(project: project, user: user, ref: "not-the-default-branch")
end
context "when notify_only_default_branch enabled" do
diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
index 1f76b981292..d6490a808ce 100644
--- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
@@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name|
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
+ describe '#can_uninstall?' do
+ it 'calls allowed_to_uninstall?' do
+ expect(subject).to receive(:allowed_to_uninstall?).and_return(true)
+
+ expect(subject.can_uninstall?).to be_truthy
+ end
+ end
+
describe '#name' do
it 'is .application_name' do
expect(subject.name).to eq(described_class.application_name)
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index b8c19cab0c4..4525c03837f 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to eq(reason)
end
end
+
+ context 'application is uninstalling' do
+ subject { create(application_name, :uninstalling) }
+
+ it 'is uninstall_errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_uninstall_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
end
describe '#make_scheduled' do
@@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_scheduled
end
+ describe 'when installed' do
+ subject { create(application_name, :installed) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+ end
+
describe 'when was errored' do
subject { create(application_name, :errored) }
@@ -148,6 +169,28 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to be_nil
end
end
+
+ describe 'when was uninstall_errored' do
+ subject { create(application_name, :uninstall_errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+
+ describe '#make_uninstalling' do
+ subject { create(application_name, :scheduled) }
+
+ it 'is uninstalling' do
+ subject.make_uninstalling!
+
+ expect(subject).to be_uninstalling
+ end
end
end
@@ -155,16 +198,18 @@ shared_examples 'cluster application status specs' do |application_name|
using RSpec::Parameterized::TableSyntax
where(:trait, :available) do
- :not_installable | false
- :installable | false
- :scheduled | false
- :installing | false
- :installed | true
- :updating | false
- :updated | true
- :errored | false
- :update_errored | false
- :timeouted | false
+ :not_installable | false
+ :installable | false
+ :scheduled | false
+ :installing | false
+ :installed | true
+ :updating | false
+ :updated | true
+ :errored | false
+ :update_errored | false
+ :uninstalling | false
+ :uninstall_errored | false
+ :timed_out | false
end
with_them do
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 77376496854..e5375bc8280 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -41,7 +41,7 @@ shared_examples_for 'inherited access level as a member of entity' do
member.update(access_level: Gitlab::Access::REPORTER)
- expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
+ expect(member.errors.full_messages).to eq(["Access level should be greater than or equal to Developer inherited membership from group #{parent_entity.name}"])
end
it 'allows changing the level from a non existing member' do
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 940c24c8d67..36c486dbdd6 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -106,6 +106,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(WebMock).to have_requested(:post, webhook_url).once
end
+ it "calls Slack/Mattermost API for deployment events" do
+ deployment_event_data = { object_kind: 'deployment' }
+
+ chat_service.execute(deployment_event_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
it 'uses the username as an option for slack when configured' do
allow(chat_service).to receive(:username).and_return(username)
@@ -267,7 +275,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'does not notify push events if they are not for the default branch' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
@@ -284,7 +292,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'still notifies about pushed tags' do
ref = "#{Gitlab::Git::TAG_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
@@ -299,7 +307,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'notifies about all push events' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 9ce9a353913..a62830c35f1 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -771,6 +771,14 @@ describe ObjectStorage do
expect { avatars }.not_to exceed_query_limit(1)
end
+ it 'does not attempt to replace methods' do
+ models.each do |model|
+ expect(model.avatar.upload).to receive(:method_missing).and_call_original
+
+ model.avatar.upload.path
+ end
+ end
+
it 'fetches a unique upload for each model' do
expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url))
expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload))
diff --git a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb
deleted file mode 100644
index 9424795749d..00000000000
--- a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-describe 'projects/issues/_merge_requests_status.html.haml' do
- around do |ex|
- Timecop.freeze(Date.new(2018, 7, 22)) do
- ex.run
- end
- end
-
- it 'shows date of status change in tooltip' do
- merge_request = create(:merge_request, created_at: 1.month.ago)
-
- render partial: 'projects/issues/merge_requests_status',
- locals: { merge_request: merge_request, css_class: '' }
-
- expect(rendered).to match("Opened.*about 1 month ago")
- end
-
- it 'shows only status in tooltip if date is not set' do
- merge_request = create(:merge_request, state: :closed)
-
- render partial: 'projects/issues/merge_requests_status',
- locals: { merge_request: merge_request, css_class: '' }
-
- expect(rendered).to match("Closed")
- end
-end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
index 1d9c6d36ad7..1ca9eaf8fdb 100644
--- a/spec/views/projects/issues/show.html.haml_spec.rb
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -19,6 +19,7 @@ describe 'projects/issues/show' do
context 'when the issue is closed' do
before do
allow(issue).to receive(:closed?).and_return(true)
+ allow(view).to receive(:current_user).and_return(user)
end
context 'when the issue was moved' do
@@ -28,16 +29,30 @@ describe 'projects/issues/show' do
issue.moved_to = new_issue
end
- it 'shows "Closed (moved)" if an issue has been moved' do
- render
+ context 'when user can see the moved issue' do
+ before do
+ project.add_developer(user)
+ end
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ it 'shows "Closed (moved)" if an issue has been moved' do
+ render
+
+ expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ end
+
+ it 'links "moved" to the new issue the original issue was moved to' do
+ render
+
+ expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
- it 'links "moved" to the new issue the original issue was moved to' do
- render
+ context 'when user cannot see moved issue' do
+ it 'does not show moved issue link' do
+ render
- expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ expect(rendered).not_to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
end
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
index 065aeaf2b65..ffe8796ded9 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/build_success_worker_spec.rb
@@ -15,6 +15,7 @@ describe BuildSuccessWorker do
let!(:build) { create(:ci_build, :deploy_to_production) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
Deployment.delete_all
build.reload
end
diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
new file mode 100644
index 00000000000..aaf5c9defc4
--- /dev/null
+++ b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do
+ let(:app) { create(:clusters_applications_helm) }
+ let(:app_name) { app.name }
+ let(:app_id) { app.id }
+
+ subject { described_class.new.perform(app_name, app_id) }
+
+ context 'app exists' do
+ let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) }
+
+ it 'calls the check service' do
+ expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service)
+ expect(service).to receive(:execute).once
+
+ subject
+ end
+ end
+
+ context 'app does not exist' do
+ let(:app_id) { 0 }
+
+ it 'does not call the check service' do
+ expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/workers/deployments/finished_worker_spec.rb b/spec/workers/deployments/finished_worker_spec.rb
new file mode 100644
index 00000000000..df62821e2cd
--- /dev/null
+++ b/spec/workers/deployments/finished_worker_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::FinishedWorker do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before do
+ allow(ProjectServiceWorker).to receive(:perform_async)
+ end
+
+ it 'executes project services for deployment_hooks' do
+ deployment = create(:deployment)
+ project = deployment.project
+ service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).to have_received(:perform_async).with(service.id, an_instance_of(Hash))
+ end
+
+ it 'does not execute an inactive service' do
+ deployment = create(:deployment)
+ project = deployment.project
+ create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+
+ it 'does nothing if a deployment with the given id does not exist' do
+ worker.perform(0)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+ end
+end
diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb
new file mode 100644
index 00000000000..0e1171e8491
--- /dev/null
+++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomainRemovalCronWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ context 'when there is domain which should be removed' do
+ let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) }
+
+ before do
+ stub_feature_flags(remove_disabled_domains: true)
+ end
+
+ it 'removes domain' do
+ expect { worker.perform }.to change { PagesDomain.count }.by(-1)
+ expect(PagesDomain.exists?).to eq(false)
+ end
+
+ context 'when domain removal is disabled' do
+ before do
+ stub_feature_flags(remove_disabled_domains: false)
+ end
+
+ it 'does not remove pages domain' do
+ expect { worker.perform }.not_to change { PagesDomain.count }
+ expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present
+ end
+ end
+ end
+
+ context 'where there is a domain which scheduled for removal in the future' do
+ let!(:domain_for_removal) { create(:pages_domain, :scheduled_for_removal) }
+
+ it 'does not remove pages domain' do
+ expect { worker.perform }.not_to change { PagesDomain.count }
+ expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb
index 9b479da1cb6..186824a444f 100644
--- a/spec/workers/pages_domain_verification_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb
@@ -6,11 +6,11 @@ describe PagesDomainVerificationCronWorker do
subject(:worker) { described_class.new }
describe '#perform' do
- it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
- verified = create(:pages_domain)
- reverify = create(:pages_domain, :reverify)
- disabled = create(:pages_domain, :disabled)
+ let!(:verified) { create(:pages_domain) }
+ let!(:reverify) { create(:pages_domain, :reverify) }
+ let!(:disabled) { create(:pages_domain, :disabled) }
+ it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
[reverify, disabled].each do |domain|
expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id)
end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index f23910d23be..8c604b13297 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe PipelineScheduleWorker do
+ include ExclusiveLeaseHelpers
+
subject { described_class.new.perform }
set(:project) { create(:project, :repository) }
@@ -39,6 +41,16 @@ describe PipelineScheduleWorker do
it_behaves_like 'successful scheduling'
+ context 'when exclusive lease has already been taken by the other instance' do
+ before do
+ stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
+ end
+
+ it 'raises an error and does not start creating pipelines' do
+ expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+
context 'when the latest commit contains [ci skip]' do
before do
allow_any_instance_of(Ci::Pipeline)
diff --git a/yarn.lock b/yarn.lock
index 8a23aabba20..7c9652ba131 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -663,12 +663,13 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087"
integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g==
-"@gitlab/ui@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.4.0.tgz#174681f210eb16c3d101a36968d5e4d163c0d014"
- integrity sha512-joXNz80IHMQxEGrqcNUTEKofjfZtkOKUe34HAFI71NEeYT6H0r/lYmJ5Gcz+MmwM1CvZOVbB3DnKzxQPDbN/hQ==
+"@gitlab/ui@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.7.0.tgz#8d0892ae54ddcb3c309bd970c57a433af6098edf"
+ integrity sha512-DEIPfem9P5j0DyzZp0M62SbLQu1D4feiNO0oAYN8bJrgiMC8H3VEJwiyplNItSwFYa985O1xOr3B81eTiZEWDQ==
dependencies:
"@babel/standalone" "^7.0.0"
+ "@gitlab/vue-toasted" "^1.2.1"
bootstrap-vue "^2.0.0-rc.11"
copy-to-clipboard "^3.0.8"
echarts "^4.2.0-rc.2"
@@ -679,6 +680,11 @@
vue "^2.5.21"
vue-loader "^15.4.2"
+"@gitlab/vue-toasted@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/vue-toasted/-/vue-toasted-1.2.1.tgz#f407b5aa710863e5b7f021f4a1f66160331ab263"
+ integrity sha512-ve2PLxKqrwNpsd+4bV5zGJT5+H5N/VJBZoFS2Vp1mH5cUDBYIHTzDmbS6AbBGUDh0F3TxmFMiqfXfpO/1VjBNQ==
+
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -2735,17 +2741,17 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
-d3-array@1, d3-array@1.2.1, d3-array@^1.2.0, d3-array@^1.2.1:
+d3-array@1, d3-array@1.2.1, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
integrity sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw==
-d3-axis@1.0.8, d3-axis@^1.0.8:
+d3-axis@1, d3-axis@1.0.8, d3-axis@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
integrity sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo=
-d3-brush@1.0.4, d3-brush@^1.0.4:
+d3-brush@1, d3-brush@1.0.4, d3-brush@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
integrity sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q=
@@ -2756,7 +2762,7 @@ d3-brush@1.0.4, d3-brush@^1.0.4:
d3-selection "1"
d3-transition "1"
-d3-chord@1.0.4:
+d3-chord@1, d3-chord@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
integrity sha1-fexPC6iG9xP+ERxF92NBT290yiw=
@@ -2774,6 +2780,13 @@ d3-color@1, d3-color@1.0.3:
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
integrity sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs=
+d3-contour@1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
+ integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==
+ dependencies:
+ d3-array "^1.1.1"
+
d3-dispatch@1, d3-dispatch@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
@@ -2801,7 +2814,14 @@ d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3:
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4=
-d3-force@1.1.0:
+d3-fetch@1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.1.2.tgz#957c8fbc6d4480599ba191b1b2518bf86b3e1be2"
+ integrity sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==
+ dependencies:
+ d3-dsv "1"
+
+d3-force@1, d3-force@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
integrity sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg==
@@ -2816,14 +2836,14 @@ d3-format@1, d3-format@1.2.2:
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a"
integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw==
-d3-geo@1.9.1:
+d3-geo@1, d3-geo@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356"
integrity sha512-l9wL/cEQkyZQYXw3xbmLsH3eQ5ij+icNfo4r0GrLa5rOCZR/e/3am45IQ0FvQ5uMsv+77zBRunLc9ufTWSQYFA==
dependencies:
d3-array "1"
-d3-hierarchy@1.1.5:
+d3-hierarchy@1, d3-hierarchy@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
integrity sha1-ochFxC+Eoga88cAcAQmOpN2qeiY=
@@ -2840,7 +2860,7 @@ d3-path@1, d3-path@1.0.5:
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
integrity sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q=
-d3-polygon@1.0.3:
+d3-polygon@1, d3-polygon@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
integrity sha1-FoiOkCZGCTPysXllKtN4Ik04LGI=
@@ -2855,7 +2875,7 @@ d3-queue@3.0.7:
resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
integrity sha1-yTouVLQXwJWRKdfXP2z31Ckudhg=
-d3-random@1.1.0:
+d3-random@1, d3-random@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
integrity sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM=
@@ -2870,6 +2890,14 @@ d3-request@1.0.6:
d3-dsv "1"
xmlhttprequest "1"
+d3-scale-chromatic@1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0"
+ integrity sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw==
+ dependencies:
+ d3-color "1"
+ d3-interpolate "1"
+
d3-scale@1.0.7, d3-scale@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
@@ -2883,12 +2911,24 @@ d3-scale@1.0.7, d3-scale@^1.0.7:
d3-time "1"
d3-time-format "2"
+d3-scale@2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
+ integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA==
-d3-shape@1.2.0, d3-shape@^1.2.0:
+d3-shape@1, d3-shape@1.2.0, d3-shape@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
integrity sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c=
@@ -2924,12 +2964,12 @@ d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1:
d3-selection "^1.1.0"
d3-timer "1"
-d3-voronoi@1.1.2:
+d3-voronoi@1, d3-voronoi@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
integrity sha1-Fodmfo8TotFYyAwUgMWinLDYlzw=
-d3-zoom@1.7.1:
+d3-zoom@1, d3-zoom@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
integrity sha512-sZHQ55DGq5BZBFGnRshUT8tm2sfhPHFnOlmPbbwTkAoPeVdRTkB4Xsf9GCY0TSHrTD8PeJPZGmP/TpGicwJDJQ==
@@ -2976,6 +3016,43 @@ d3@^4.13.0:
d3-voronoi "1.1.2"
d3-zoom "1.7.1"
+d3@^5.7.0:
+ version "5.9.2"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.2.tgz#64e8a7e9c3d96d9e6e4999d2c8a2c829767e67f5"
+ integrity sha512-ydrPot6Lm3nTWH+gJ/Cxf3FcwuvesYQ5uk+j/kXEH/xbuYWYWTMAHTJQkyeuG8Y5WM5RSEYB41EctUrXQQytRQ==
+ dependencies:
+ d3-array "1"
+ d3-axis "1"
+ d3-brush "1"
+ d3-chord "1"
+ d3-collection "1"
+ d3-color "1"
+ d3-contour "1"
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-dsv "1"
+ d3-ease "1"
+ d3-fetch "1"
+ d3-force "1"
+ d3-format "1"
+ d3-geo "1"
+ d3-hierarchy "1"
+ d3-interpolate "1"
+ d3-path "1"
+ d3-polygon "1"
+ d3-quadtree "1"
+ d3-random "1"
+ d3-scale "2"
+ d3-scale-chromatic "1"
+ d3-selection "1"
+ d3-shape "1"
+ d3-time "1"
+ d3-time-format "2"
+ d3-timer "1"
+ d3-transition "1"
+ d3-voronoi "1"
+ d3-zoom "1"
+
dagre-d3-renderer@^0.5.8:
version "0.5.8"
resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz#aa071bb71d3c4d67426925906f3f6ddead49c1a3"
@@ -4937,10 +5014,10 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
-he@^1.1.0, he@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
- integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
+he@^1.1.0, he@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
highlight.js@^9.13.1, highlight.js@~9.13.0:
version "9.13.1"
@@ -7007,19 +7084,19 @@ merge@^1.2.0:
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
-mermaid@^8.0.0-rc.8:
- version "8.0.0-rc.8"
- resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72"
- integrity sha512-GbF9jHWfqE7YGx9vQySmBxy2Ahlclxmpk4tJ9ntNyafENl96s96ggUK/NQS5ydYoFab6MavTm4YMTIPKqWVvPQ==
+mermaid@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0.tgz#8f6c75017e788a8c3997e20c5e5046c2b88d1a8f"
+ integrity sha512-vUQRykev0A6RtxIVqQT3a9TDxcSbdZbQF5JDyKgidnYuJy8BE8jp6LM+HKDSQuroKm6buu4NlpMO+qhxIP/cTg==
dependencies:
- d3 "^4.13.0"
+ d3 "^5.7.0"
dagre-d3-renderer "^0.5.8"
dagre-layout "^0.8.8"
graphlibrary "^2.2.0"
- he "^1.1.1"
- lodash "^4.17.5"
- moment "^2.21.0"
- scope-css "^1.0.5"
+ he "^1.2.0"
+ lodash "^4.17.11"
+ moment "^2.23.0"
+ scope-css "^1.2.1"
methods@~1.1.2:
version "1.1.2"
@@ -7171,10 +7248,10 @@ mkdirp@0.5.x, mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp
dependencies:
minimist "0.0.8"
-moment@2.x, moment@^2.10.2, moment@^2.21.0:
- version "2.23.0"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225"
- integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==
+moment@2.x, moment@^2.10.2, moment@^2.23.0:
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+ integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
monaco-editor-webpack-plugin@^1.7.0:
version "1.7.0"
@@ -9266,7 +9343,7 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
-scope-css@^1.0.5:
+scope-css@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/scope-css/-/scope-css-1.2.1.tgz#c35768bc900cad030a3e0d663a818c0f6a57f40e"
integrity sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==