summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--CHANGELOG.md58
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock18
-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_new_issue.vue4
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js12
-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/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/ide/ide_router.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js5
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js9
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js4
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.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/discussion_notes.vue155
-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/stores/getters.js2
-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_shared/components/content_viewer/lib/viewer_utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue4
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js1
-rw-r--r--app/assets/stylesheets/pages/boards.scss1
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-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_controller.rb6
-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/markup_helper.rb2
-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.rb3
-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.rb7
-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/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_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/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/tags/destroy_service.rb11
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb13
-rw-r--r--app/views/admin/application_settings/_logging.html.haml7
-rw-r--r--app/views/admin/application_settings/_pages.html.haml29
-rw-r--r--app/views/clusters/clusters/show.html.haml2
-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/notify/new_merge_request_email.html.haml2
-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.haml2
-rw-r--r--app/views/projects/issues/_merge_requests_status.html.haml25
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-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/_wiki_blob.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/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/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/60687-enviro-dropdown.yml5
-rw-r--r--changelogs/unreleased/60906-fix-wiki-links.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/feat-sentry-environment.yml5
-rw-r--r--changelogs/unreleased/fix-webpack-assets-relative-url-bug.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/pl-upgrade-letter_opener_web.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-disable-batch-load-replace-methods.yml5
-rw-r--r--changelogs/unreleased/wiki-search-results-fix.yml5
-rw-r--r--changelogs/unreleased/winh-boards-drag-selection.yml5
-rw-r--r--config/gitlab.yml.example7
-rw-r--r--config/initializers/1_settings.rb14
-rw-r--r--config/initializers/sentry.rb1
-rw-r--r--config/karma.config.js2
-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/20190426180107_add_deployment_events_to_services.rb17
-rw-r--r--db/schema.rb6
-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/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/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/variables/README.md10
-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/testing_guide/end_to_end_tests.md5
-rw-r--r--doc/gitlab-basics/README.md46
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/topics/git/troubleshooting_git.md27
-rw-r--r--doc/user/admin_area/index.md28
-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/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/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/events_helpers.rb31
-rw-r--r--lib/api/project_events.rb29
-rw-r--r--lib/gitlab/action_view_output/context.rb41
-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_access.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb8
-rw-r--r--lib/gitlab/gitaly_client/object_pool_service.rb9
-rw-r--r--lib/gitlab/gon_helper.rb7
-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.pot171
-rw-r--r--package.json7
-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.sh1
-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/factories/clusters/applications/helm.rb19
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/pages_domains.rb4
-rw-r--r--spec/features/admin/admin_settings_spec.rb46
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb38
-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.js35
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js139
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js7
-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/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/issue_spec.js1
-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/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/object_pool_spec.rb41
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb84
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb20
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb42
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-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/ci/bridge_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb22
-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.rb8
-rw-r--r--spec/models/deployment_spec.rb27
-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_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/repository_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/project_events_spec.rb156
-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/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/shared_examples/application_setting_examples.rb37
-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/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.lock151
361 files changed, 6333 insertions, 1636 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f1573dba32a..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"
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 935b494b6f7..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:
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/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 a2d87226ac2..39fc130ef85 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.35.0 \ No newline at end of file
+1.36.0
diff --git a/Gemfile b/Gemfile
index a350f194f62..615cc7bec0d 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'
@@ -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.22.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 64f2f78a4f8..3b03a8ef691 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,7 +283,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.22.0)
+ gitaly-proto (1.26.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -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,7 +1056,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.22.0)
+ gitaly-proto (~> 1.26.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-labkit (~> 0.1.2)
@@ -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/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_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/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 4f47f1b6550..8461e01de7b 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -279,14 +279,10 @@ export default class Clusters {
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/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/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/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 518a9cf7a0f..8c84b98a108 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -3,6 +3,7 @@ import VueRouter from 'vue-router';
import { joinPaths } from '~/lib/utils/url_utility';
import flash from '~/flash';
import store from './stores';
+import { __ } from '~/locale';
Vue.use(VueRouter);
@@ -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/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/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/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/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/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/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/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/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_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/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/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/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 0e4b40b2bed..09ff518bbdf 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -210,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;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 0c99ff5341c..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;
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_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/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/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/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..557215ff4dc 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.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..5a2ead41578 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
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 3ebf7a5cfba..af648db3708 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -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/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_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/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/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/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..006ebf09576 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -1,6 +1,13 @@
= 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.
+ The specific client side DSN setting is already handled as a component from a Sentry perspective and will be removed.
+ 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/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/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/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/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 0d8d7123a01..9293aa1b309 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -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/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/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index a1ec2c887c2..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) }
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/_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/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/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/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/60687-enviro-dropdown.yml b/changelogs/unreleased/60687-enviro-dropdown.yml
deleted file mode 100644
index 1fc5a7dd6f5..00000000000
--- a/changelogs/unreleased/60687-enviro-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Metrics Environments dropdown
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/60906-fix-wiki-links.yml b/changelogs/unreleased/60906-fix-wiki-links.yml
deleted file mode 100644
index cc65a1382bf..00000000000
--- a/changelogs/unreleased/60906-fix-wiki-links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show proper wiki links in search results
-merge_request: 27634
-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/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-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-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/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-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-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/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/changelogs/unreleased/winh-boards-drag-selection.yml b/changelogs/unreleased/winh-boards-drag-selection.yml
deleted file mode 100644
index 84b23ab664b..00000000000
--- a/changelogs/unreleased/winh-boards-drag-selection.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent text selection when dragging in issue boards
-merge_request: 27724
-author:
-type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index bdac5b2a6a1..06530194907 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -315,6 +315,13 @@ production: &base
# path: shared/registry
# issuer: gitlab-issuer
+
+ ## Error Reporting and Logging with Sentry
+ sentry:
+ # enabled: false
+ # 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..154de3bc1b0 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,14 @@ 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
+
+#
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
@@ -338,6 +348,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/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/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/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 23566a31a9d..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: 20190422082247) 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: 20190422082247) 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: 20190422082247) 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
@@ -1999,6 +2002,7 @@ ActiveRecord::Schema.define(version: 20190422082247) 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
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/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/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/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/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/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/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/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 5e67cb0ef16..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](#admin-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. |
@@ -46,4 +46,28 @@ The Dashboard is the default view of the Admin Area, and is made up of the follo
| 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. | \ No newline at end of file
+| 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/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/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/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/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/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/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/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_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/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/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/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 5bc9bb3434d..ab125c5a7b0 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 ""
@@ -933,6 +942,9 @@ msgstr ""
msgid "Application settings saved successfully"
msgstr ""
+msgid "Application uninstalled but failed to destroy: %{error_message}"
+msgstr ""
+
msgid "Application was successfully destroyed."
msgstr ""
@@ -1284,6 +1296,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 +1593,9 @@ msgstr ""
msgid "Can't find HEAD commit for this branch"
msgstr ""
+msgid "Can't find variable: ZiteReader"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -1602,6 +1620,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 ""
@@ -2391,6 +2412,9 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "ComboSearch is not defined"
+msgstr ""
+
msgid "Command line instructions"
msgstr ""
@@ -2525,6 +2549,9 @@ msgstr ""
msgid "Configure Gitaly timeouts."
msgstr ""
+msgid "Configure Let's Encrypt"
+msgstr ""
+
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr ""
@@ -2699,6 +2726,9 @@ msgstr ""
msgid "Copy secret to clipboard"
msgstr ""
+msgid "Copy source to clipboard"
+msgstr ""
+
msgid "Copy to clipboard"
msgstr ""
@@ -3277,9 +3307,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 ""
@@ -3691,6 +3727,9 @@ msgstr ""
msgid "Error loading branches."
msgstr ""
+msgid "Error loading file viewer."
+msgstr ""
+
msgid "Error loading last commit."
msgstr ""
@@ -3709,6 +3748,9 @@ msgstr ""
msgid "Error loading template."
msgstr ""
+msgid "Error loading viewer"
+msgstr ""
+
msgid "Error occurred when toggling the notification subscription"
msgstr ""
@@ -3742,9 +3784,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 ""
@@ -3943,6 +3991,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 ""
@@ -3991,6 +4042,9 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
+msgid "Failed to update tag!"
+msgstr ""
+
msgid "Failed to update."
msgstr ""
@@ -4622,6 +4676,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 ""
@@ -5171,6 +5228,9 @@ msgstr ""
msgid "LFSStatus|Enabled"
msgstr ""
+msgid "LICENSE"
+msgstr ""
+
msgid "Label"
msgstr ""
@@ -5311,6 +5371,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] ""
@@ -6017,6 +6080,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 ""
@@ -6247,6 +6313,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 ""
@@ -6622,6 +6694,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 ""
@@ -6664,6 +6739,9 @@ msgstr ""
msgid "Preview"
msgstr ""
+msgid "Preview Markdown"
+msgstr ""
+
msgid "Preview changes"
msgstr ""
@@ -7159,6 +7237,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 ""
@@ -7282,6 +7411,9 @@ msgstr ""
msgid "Protected"
msgstr ""
+msgid "Protected Tag"
+msgstr ""
+
msgid "Protip:"
msgstr ""
@@ -7626,6 +7758,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 ""
@@ -7802,6 +7937,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
+msgid "Save anyway"
+msgstr ""
+
msgid "Save application"
msgstr ""
@@ -8683,6 +8821,9 @@ msgstr ""
msgid "Switch to GitLab Next"
msgstr ""
+msgid "Switch to the source to copy it to the clipboard"
+msgstr ""
+
msgid "System Hooks"
msgstr ""
@@ -8830,6 +8971,9 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "Test settings and save changes"
+msgstr ""
+
msgid "TestHooks|Ensure one of your projects has merge requests."
msgstr ""
@@ -9105,6 +9249,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 ""
@@ -9129,6 +9276,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 ""
@@ -9180,6 +9330,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 ""
@@ -9766,12 +9922,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 ""
@@ -9796,6 +9958,9 @@ msgstr ""
msgid "Unknown format"
msgstr ""
+msgid "Unknown response text"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -10192,6 +10357,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 ""
@@ -10921,6 +11089,9 @@ msgstr ""
msgid "it is too large"
msgstr ""
+msgid "jigsaw is not defined"
+msgstr ""
+
msgid "latest"
msgstr ""
diff --git a/package.json b/package.json
index d2ee29fe06d..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.5.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_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 8be22dc0278..9455e462617 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -216,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 \
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/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/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/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cf334e1e4da..4ec44cb05b3 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -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/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 5103cb4f69f..a61103397eb 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -6,7 +6,7 @@ import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import $ from 'jquery';
-const { INSTALLING, INSTALLABLE, INSTALLED, NOT_INSTALLABLE } = APPLICATION_STATUS;
+const { INSTALLING, INSTALLABLE, INSTALLED } = APPLICATION_STATUS;
describe('Clusters', () => {
setTestTimeout(1000);
@@ -317,13 +317,12 @@ describe('Clusters', () => {
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);
});
@@ -338,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/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/frontend/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/frontend/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/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/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/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/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/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/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_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/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 591a3d296c2..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
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/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..59ec7310391 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')
@@ -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 b66acf13135..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) }
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/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_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/repository_spec.rb b/spec/models/repository_spec.rb
index 4c354593b57..a6c3d5756aa 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1637,6 +1637,7 @@ describe Repository do
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
+ :metrics_dashboard_paths,
:xcode_project?
])
@@ -2487,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/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/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/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/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/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb
index e7ec24c5b7e..d8f7ba1185e 100644
--- a/spec/support/shared_examples/application_setting_examples.rb
+++ b/spec/support/shared_examples/application_setting_examples.rb
@@ -249,4 +249,41 @@ 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)
+
+ 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')
+
+ 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/42'
+ 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/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 a0446b652ac..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.5.0":
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.5.0.tgz#31ecfc16e3f7663545f31ddf07e02bba96a6d138"
- integrity sha512-eDD++hhGJuH59g2QcGshuou9/NLcLfse4Abm9KOIWIaYI3NPWW2KRGwLHPB6H0d5W0/X5pyWYQvXgF7JE2ZXbA==
+"@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"
@@ -678,7 +679,11 @@
url-search-params-polyfill "^5.0.0"
vue "^2.5.21"
vue-loader "^15.4.2"
- vue-toasted "^1.1.26"
+
+"@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"
@@ -2736,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=
@@ -2757,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=
@@ -2775,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"
@@ -2802,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==
@@ -2817,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=
@@ -2841,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=
@@ -2856,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=
@@ -2871,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"
@@ -2884,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=
@@ -2925,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==
@@ -2977,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"
@@ -4938,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"
@@ -7008,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"
@@ -7172,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"
@@ -9267,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==
@@ -10983,11 +11059,6 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
-vue-toasted@^1.1.26:
- version "1.1.26"
- resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.26.tgz#1333d1a42157ab78389c3810023a49ba94e69c7b"
- integrity sha512-Z4/gfPcqdzsRvif7UITrZOkh3C6jm0yQKJyr9kX31IGWXor5dNipE1Sc5SnlL5RLmY7vlLa+SqIjc9Gbpy7V0g==
-
vue-virtual-scroll-list@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"