summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml3
-rw-r--r--.gitlab/issue_templates/Feature Flag Roll Out.md43
-rw-r--r--CHANGELOG.md52
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock10
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue19
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue2
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js4
-rw-r--r--app/assets/javascripts/boards/stores/actions.js4
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js30
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js3
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js6
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue62
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue176
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue150
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js2
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/create_item_dropdown.js2
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue21
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue1
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue2
-rw-r--r--app/assets/javascripts/error_tracking_settings/index.js2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/gl_dropdown.js4
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/ide/stores/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js2
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js4
-rw-r--r--app/assets/javascripts/image_diff/view_types.js2
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js6
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js36
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue9
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue11
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue4
-rw-r--r--app/assets/javascripts/notes/mixins/issuable_state.js4
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue34
-rw-r--r--app/assets/javascripts/operation_settings/index.js9
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js38
-rw-r--r--app/assets/javascripts/operation_settings/store/index.js16
-rw-r--r--app/assets/javascripts/operation_settings/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/operation_settings/store/mutations.js7
-rw-r--r--app/assets/javascripts/operation_settings/store/state.js5
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue2
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js2
-rw-r--r--app/assets/javascripts/profile/account/index.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js6
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js4
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js6
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue36
-rw-r--r--app/assets/javascripts/serverless/constants.js4
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js3
-rw-r--r--app/assets/javascripts/serverless/store/actions.js29
-rw-r--r--app/assets/javascripts/serverless/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/serverless/store/mutations.js15
-rw-r--r--app/assets/javascripts/serverless/store/state.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.vue2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss4
-rw-r--r--app/assets/stylesheets/components/avatar.scss34
-rw-r--r--app/assets/stylesheets/errors.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss103
-rw-r--r--app/assets/stylesheets/framework/mixins.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss23
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss46
-rw-r--r--app/assets/stylesheets/pages/note_form.scss11
-rw-r--r--app/assets/stylesheets/pages/notes.scss10
-rw-r--r--app/controllers/concerns/import_url_params.rb19
-rw-r--r--app/controllers/concerns/milestone_actions.rb8
-rw-r--r--app/controllers/projects/imports_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb48
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/finders/clusters/knative_services_finder.rb112
-rw-r--r--app/finders/projects/serverless/functions_finder.rb40
-rw-r--r--app/graphql/resolvers/base_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb2
-rw-r--r--app/graphql/resolvers/issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb33
-rw-r--r--app/graphql/resolvers/namespace_resolver.rb13
-rw-r--r--app/graphql/types/base_field.rb2
-rw-r--r--app/graphql/types/issue_type.rb6
-rw-r--r--app/graphql/types/namespace_type.rb5
-rw-r--r--app/graphql/types/project_statistics_type.rb16
-rw-r--r--app/graphql/types/project_type.rb4
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/emails_helper.rb7
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/notifications_helper.rb4
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/ci/pipeline_schedule.rb25
-rw-r--r--app/models/clusters/applications/knative.rb48
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/concerns/noteable.rb12
-rw-r--r--app/models/concerns/referable.rb1
-rw-r--r--app/models/concerns/resolvable_note.rb2
-rw-r--r--app/models/concerns/update_project_statistics.rb52
-rw-r--r--app/models/diff_note.rb18
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/merge_request.rb72
-rw-r--r--app/models/project.rb12
-rw-r--r--app/models/project_auto_devops.rb6
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/presenters/issue_presenter.rb12
-rw-r--r--app/presenters/member_presenter.rb5
-rw-r--r--app/presenters/merge_request_presenter.rb6
-rw-r--r--app/serializers/merge_request_widget_entity.rb10
-rw-r--r--app/services/auto_merge/merge_when_pipeline_succeeds_service.rb54
-rw-r--r--app/services/auto_merge_service.rb50
-rw-r--r--app/services/ci/pipeline_schedule_service.rb13
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/merge_requests/close_service.rb5
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb20
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb47
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb82
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/service_response.rb15
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml8
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml7
-rw-r--r--app/views/profiles/keys/_key_details.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml40
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml38
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml3
-rw-r--r--app/views/search/_results.html.haml3
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml27
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/members/_member.html.haml13
-rw-r--r--app/views/shared/notes/_hints.html.haml5
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/auto_merge_process_worker.rb14
-rw-r--r--app/workers/pipeline_schedule_worker.rb41
-rw-r--r--app/workers/pipeline_success_worker.rb8
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb26
-rw-r--r--babel.config.js2
-rw-r--r--changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml5
-rw-r--r--changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml5
-rw-r--r--changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml5
-rw-r--r--changelogs/unreleased/56959-drop-project_auto_devops_domain.yml5
-rw-r--r--changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml5
-rw-r--r--changelogs/unreleased/58269-separate-update-patch.yml5
-rw-r--r--changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml5
-rw-r--r--changelogs/unreleased/60778-input-text-height.yml5
-rw-r--r--changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml5
-rw-r--r--changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml5
-rw-r--r--changelogs/unreleased/61821-tooltip-consistency.yml5
-rw-r--r--changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml5
-rw-r--r--changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml5
-rw-r--r--changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/abstract-auto-merge.yml5
-rw-r--r--changelogs/unreleased/ac-graphql-stats.yml5
-rw-r--r--changelogs/unreleased/ac-graphql-wikisize.yml5
-rw-r--r--changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml5
-rw-r--r--changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml5
-rw-r--r--changelogs/unreleased/dm-disable-two-step-rebase.yml5
-rw-r--r--changelogs/unreleased/dm-http-hostname-override.yml5
-rw-r--r--changelogs/unreleased/fix-search-dropdown-blur-close.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.43.0.yml5
-rw-r--r--changelogs/unreleased/increase-move-issue-dropdown-height.yml5
-rw-r--r--changelogs/unreleased/jp-label-fix.yml5
-rw-r--r--changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml5
-rw-r--r--changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml5
-rw-r--r--changelogs/unreleased/patch-64.yml5
-rw-r--r--changelogs/unreleased/pb-update-gitaly-1-45-0.yml5
-rw-r--r--changelogs/unreleased/remove-mr-diff-header-height.yml5
-rw-r--r--changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml5
-rw-r--r--changelogs/unreleased/security-60039.yml5
-rw-r--r--changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml5
-rw-r--r--changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml5
-rw-r--r--changelogs/unreleased/security-fix-project-existence-disclosure-master.yml5
-rw-r--r--changelogs/unreleased/security-fix_milestones_search_api_leak.yml5
-rw-r--r--changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml5
-rw-r--r--changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml5
-rw-r--r--changelogs/unreleased/security-unsubscribing-from-issue.yml5
-rw-r--r--changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml5
-rw-r--r--changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml5
-rw-r--r--changelogs/unreleased/update-pages.yml5
-rw-r--r--changelogs/unreleased/use-source-ref-name-in-webhook.yml5
-rw-r--r--changelogs/unreleased/zj-remove-delta-island-feature-flag.yml3
-rw-r--r--config/initializers/hipchat_client_patch.rb6
-rw-r--r--config/initializers/http_hostname_override.rb49
-rw-r--r--config/routes/project.rb85
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20180702134423_generate_missing_routes.rb1
-rw-r--r--db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb23
-rw-r--r--db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb21
-rw-r--r--db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb11
-rw-r--r--db/post_migrate/20190528180441_enqueue_reset_merge_status.rb30
-rw-r--r--db/schema.rb5
-rw-r--r--doc/administration/gitaly/index.md33
-rw-r--r--doc/administration/high_availability/gitaly.md73
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/integration/terminal.md5
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md17
-rw-r--r--doc/administration/raketasks/maintenance.md19
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/graphql/index.md1
-rw-r--r--doc/api/merge_requests.md20
-rw-r--r--doc/api/snippets.md10
-rw-r--r--doc/ci/quick_start/README.md10
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/fe_guide/style_guide_js.md6
-rw-r--r--doc/development/rolling_out_changes_using_feature_flags.md7
-rw-r--r--doc/development/understanding_explain_plans.md40
-rw-r--r--doc/push_rules/push_rules.md2
-rw-r--r--doc/raketasks/backup_restore.md6
-rw-r--r--doc/topics/autodevops/index.md15
-rw-r--r--doc/university/training/topics/unstage.md4
-rwxr-xr-xdoc/user/admin_area/img/index_runners_search_or_filter.pngbin0 -> 22414 bytes
-rw-r--r--doc/user/admin_area/index.md72
-rw-r--r--doc/user/admin_area/settings/instance_template_repository.md16
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md16
-rw-r--r--doc/user/admin_area/settings/third_party_offers.md23
-rw-r--r--doc/user/project/container_registry.md4
-rw-r--r--doc/user/project/import/gitlab_com.md5
-rw-r--r--doc/user/project/import/phabricator.md13
-rw-r--r--doc/user/project/issues/multiple_assignees_for_issues.md2
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md2
-rw-r--r--lib/api/commits.rb10
-rw-r--r--lib/api/discussions.rb2
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/merge_requests.rb35
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb8
-rw-r--r--lib/banzai/redactor.rb7
-rw-r--r--lib/gitlab.rb5
-rw-r--r--lib/gitlab/background_migration/reset_merge_status.rb17
-rw-r--r--lib/gitlab/ci/config/entry/image.rb2
-rw-r--r--lib/gitlab/ci/config/entry/service.rb3
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Maven.gitlab-ci.yml74
-rw-r--r--lib/gitlab/git/repository.rb28
-rw-r--r--lib/gitlab/git_ref_validator.rb23
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb23
-rw-r--r--lib/gitlab/http.rb2
-rw-r--r--lib/gitlab/http_connection_adapter.rb (renamed from lib/gitlab/proxy_http_connection_adapter.rb)24
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb7
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb3
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb31
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb36
-rw-r--r--lib/gitlab/metrics/system.rb28
-rw-r--r--lib/gitlab/project_search_results.rb6
-rw-r--r--lib/gitlab/search_results.rb28
-rw-r--r--lib/gitlab/url_blocker.rb75
-rw-r--r--lib/gitlab/url_sanitizer.rb4
-rw-r--r--lib/tasks/gitlab/shell.rake12
-rw-r--r--locale/gitlab.pot44
-rw-r--r--package.json4
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock3
-rw-r--r--qa/knapsack/gitlab-ce/review-qa-all_master_report.json42
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb2
-rw-r--r--qa/qa/specs/runner.rb19
-rw-r--r--qa/spec/runtime/env_spec.rb24
-rw-r--r--qa/spec/spec_helper.rb5
-rw-r--r--spec/controllers/concerns/import_url_params_spec.rb56
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb4
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb15
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb18
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb34
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb92
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb2
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb109
-rw-r--r--spec/controllers/sessions_controller_spec.rb34
-rw-r--r--spec/factories/ci/pipeline_schedule.rb10
-rw-r--r--spec/factories/merge_requests.rb3
-rw-r--r--spec/factories/project_auto_devops.rb1
-rw-r--r--spec/features/admin/admin_settings_spec.rb5
-rw-r--r--spec/features/global_search_spec.rb14
-rw-r--r--spec/features/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb23
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb11
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb1
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/features/projects/serverless/functions_spec.rb59
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb1
-rw-r--r--spec/features/projects_spec.rb1
-rw-r--r--spec/finders/clusters/knative_services_finder_spec.rb105
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb68
-rw-r--r--spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json422
-rw-r--r--spec/fixtures/security-reports/master/gl-dast-report.json74
-rw-r--r--spec/frontend/activities_spec.js (renamed from spec/javascripts/activities_spec.js)3
-rw-r--r--spec/frontend/api_spec.js (renamed from spec/javascripts/api_spec.js)4
-rw-r--r--spec/frontend/autosave_spec.js (renamed from spec/javascripts/autosave_spec.js)19
-rw-r--r--spec/frontend/clusters/components/applications_spec.js97
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js141
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js2
-rw-r--r--spec/frontend/helpers/jquery.js6
-rw-r--r--spec/frontend/helpers/local_storage_helper.js41
-rw-r--r--spec/frontend/helpers/vue_test_utils_helper.js4
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js (renamed from spec/javascripts/lib/utils/url_utility_spec.js)84
-rw-r--r--spec/frontend/notes/components/note_app_spec.js (renamed from spec/javascripts/notes/components/note_app_spec.js)203
-rw-r--r--spec/frontend/operation_settings/components/external_dashboard_spec.js158
-rw-r--r--spec/frontend/operation_settings/store/mutations_spec.js19
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js8
-rw-r--r--spec/frontend/serverless/components/functions_spec.js27
-rw-r--r--spec/frontend/serverless/mock_data.js110
-rw-r--r--spec/frontend/serverless/store/getters_spec.js2
-rw-r--r--spec/frontend/serverless/store/mutations_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js8
-rw-r--r--spec/graphql/resolvers/namespace_projects_resolver_spec.rb69
-rw-r--r--spec/graphql/types/base_field_spec.rb2
-rw-r--r--spec/graphql/types/issue_type_spec.rb6
-rw-r--r--spec/graphql/types/namespace_type_spec.rb (renamed from spec/graphql/types/namespace_type.rb)2
-rw-r--r--spec/graphql/types/project_statistics_type_spec.rb11
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/graphql/types/query_type_spec.rb12
-rw-r--r--spec/helpers/emails_helper_spec.rb8
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js57
-rw-r--r--spec/javascripts/boards/mock_data.js8
-rw-r--r--spec/javascripts/diffs/components/commit_item_spec.js4
-rw-r--r--spec/javascripts/helpers/vue_test_utils_helper.js4
-rw-r--r--spec/javascripts/matchers.js2
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js12
-rw-r--r--spec/javascripts/projects/project_new_spec.js14
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js26
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb42
-rw-r--r--spec/lib/banzai/redactor_spec.rb32
-rw-r--r--spec/lib/gitlab/background_migration/reset_merge_status_spec.rb48
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb52
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb92
-rw-r--r--spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb18
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb120
-rw-r--r--spec/lib/gitlab/http_spec.rb28
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/project.json4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb27
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb24
-rw-r--r--spec/lib/gitlab/search_results_spec.rb24
-rw-r--r--spec/lib/gitlab/shell_spec.rb10
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb83
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb34
-rw-r--r--spec/lib/gitlab_spec.rb30
-rw-r--r--spec/lib/mattermost/session_spec.rb7
-rw-r--r--spec/migrations/enqueue_reset_merge_status_spec.rb49
-rw-r--r--spec/migrations/generate_missing_routes_spec.rb2
-rw-r--r--spec/models/ci/job_artifact_spec.rb8
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb145
-rw-r--r--spec/models/ci/pipeline_spec.rb34
-rw-r--r--spec/models/clusters/applications/knative_spec.rb76
-rw-r--r--spec/models/clusters/cluster_spec.rb5
-rw-r--r--spec/models/concerns/noteable_spec.rb12
-rw-r--r--spec/models/merge_request_spec.rb141
-rw-r--r--spec/models/milestone_spec.rb16
-rw-r--r--spec/models/project_services/assembla_service_spec.rb6
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb3
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb10
-rw-r--r--spec/models/project_services/campfire_service_spec.rb24
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb10
-rw-r--r--spec/models/project_services/pushover_service_spec.rb6
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb3
-rw-r--r--spec/models/project_spec.rb19
-rw-r--r--spec/models/project_statistics_spec.rb14
-rw-r--r--spec/presenters/issue_presenter_spec.rb29
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb10
-rw-r--r--spec/requests/api/commits_spec.rb92
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb9
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb82
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb43
-rw-r--r--spec/requests/api/merge_requests_spec.rb67
-rw-r--r--spec/requests/api/search_spec.rb46
-rw-r--r--spec/requests/api/system_hooks_spec.rb8
-rw-r--r--spec/routing/project_routing_spec.rb19
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb46
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb (renamed from spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb)48
-rw-r--r--spec/services/auto_merge_service_spec.rb140
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb6
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb28
-rw-r--r--spec/services/merge_requests/close_service_spec.rb8
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb41
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb187
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb5
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb10
-rw-r--r--spec/services/merge_requests/update_service_spec.rb3
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb19
-rw-r--r--spec/services/service_response_spec.rb16
-rw-r--r--spec/services/submit_usage_ping_service_spec.rb4
-rw-r--r--spec/services/web_hook_service_spec.rb10
-rw-r--r--spec/support/helpers/git_helpers.rb8
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb46
-rw-r--r--spec/support/helpers/stub_requests.rb40
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb13
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb18
-rw-r--r--spec/tasks/tokens_spec.rb4
-rw-r--r--spec/workers/auto_merge_process_worker_spec.rb31
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb57
-rw-r--r--spec/workers/pipeline_success_worker_spec.rb26
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb32
-rw-r--r--yarn.lock16
470 files changed, 6665 insertions, 2468 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 80356fa1dc2..f5ed3e1ad9a 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -165,7 +165,10 @@ review-qa-all:
<<: *review-qa-base
allow_failure: true
when: manual
+ parallel: 5
script:
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/review-qa-all_master_report.json
+ - export KNAPSACK_TEST_FILE_PATTERN=qa/specs/features/**/*_spec.rb
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
.review-performance-base: &review-performance-base
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md
new file mode 100644
index 00000000000..b7db5a33faf
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Flag Roll Out.md
@@ -0,0 +1,43 @@
+<!-- Title suggestion: [Feature flag] Enable description of feature -->
+
+## What
+
+Remove the `:feature_name` feature flag ...
+
+## Owners
+
+- Team: NAME_OF_TEAM
+- Most appropriate slack channel to reach out to: `#g_TEAM_NAME`
+- Best individual to reach out to: NAME
+
+## Expectations
+
+### What are we expecting to happen?
+
+### What might happen if this goes wrong?
+
+### What can we monitor to detect problems with this?
+
+<!-- Which dashboards from https://dashboards.gitlab.net are most relevant? -->
+
+## Beta groups/projects
+
+If applicable, any groups/projects that are happy to have this feature turned on early. Some organizations may wish to test big changes they are interested in with a small subset of users ahead of time for example.
+
+- `gitlab-org/gitlab-ce`/`gitlab-org/gitlab-ee` projects
+- `gitlab-org`/`gitlab-com` groups
+- ...
+
+## Roll Out Steps
+
+- [ ] Enable on staging
+- [ ] Test on staging
+- [ ] Ensure that documentation has been updated
+- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour
+- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com
+- [ ] Enable on GitLab.com by running chatops command in `#production`
+- [ ] Cross post chatops slack command to `#support_gitlab-com` and in your team channel
+- [ ] Announce on the issue that the flag has been enabled
+- [ ] Remove feature flag and add changelog entry
+
+/label ~"feature flag"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88521222b8a..fd9c4df2f3f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.11.2 (2019-06-04)
+
+### Fixed (7 changes)
+
+- Update SAST.gitlab-ci.yml - Add SAST_GITLEAKS_ENTROPY_LEVEL. !28607
+- Fix OmniAuth OAuth2Generic strategy not loading. !28680
+- Use source ref in pipeline webhook. !28772
+- Fix migration failure when groups are missing route. !29022
+- Stop two-step rebase from hanging when errors occur. !29068
+- Fix project settings not being able to update. !29097
+- Fix display of 'Promote to group label' button.
+
+### Other (1 change)
+
+- Fix input group height.
+
+
## 11.11.0 (2019-05-22)
### Security (1 change)
@@ -187,6 +204,23 @@ entry.
- Add some frozen string to spec/**/*.rb. (gfyoung)
+## 11.10.6 (2019-06-04)
+
+### Fixed (7 changes, 1 of them is from the community)
+
+- Allow a member to have an access level equal to parent group. !27913
+- Fix uploading of LFS tracked file through UI. !28052
+- Use 3-way merge for squashing commits. !28078
+- Use a path for the related merge requests endpoint. !28171
+- Fix project visibility level validation. !28305 (Peter Marko)
+- Fix Rugged get_tree_entries recursive flag not working. !28494
+- Use source ref in pipeline webhook. !28772
+
+### Other (1 change)
+
+- Fix input group height.
+
+
## 11.10.4 (2019-05-01)
### Fixed (12 changes)
@@ -478,6 +512,24 @@ entry.
- Removes EE differences for environment_item.vue.
+## 11.9.12 (2019-05-30)
+
+### Security (12 changes, 1 of them is from the community)
+
+- Protect Gitlab::HTTP against DNS rebinding attack.
+- Fix project visibility level validation. (Peter Marko)
+- Update Knative version.
+- Add DNS rebinding protection settings.
+- Prevent XSS injection in note imports.
+- Prevent invalid branch for merge request.
+- Filter relative links in wiki for XSS.
+- Fix confidential issue label disclosure on milestone view.
+- Fix url redaction for issue links.
+- Resolve: Milestones leaked via search API.
+- Prevent bypass of restriction disabling web password sign in.
+- Hide confidential issue title on unsubscribe for anonymous users.
+
+
## 11.9.10 (2019-04-26)
### Security (5 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index e640847f99c..50aceaa7b71 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.42.1
+1.45.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index bc80560fad6..dc1e644a101 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.5.0
+1.6.0
diff --git a/Gemfile b/Gemfile
index 66348414506..55e1bd58a38 100644
--- a/Gemfile
+++ b/Gemfile
@@ -403,6 +403,7 @@ gem 'ruby-prof', '~> 0.17.0'
gem 'rbtrace', '~> 0.4', require: false
gem 'memory_profiler', '~> 0.9', require: false
gem 'benchmark-memory', '~> 0.1', require: false
+gem 'activerecord-explain-analyze', '~> 0.1', require: false
# OAuth
gem 'oauth2', '~> 1.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3f85fa958bd..3b37cec3b7f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -38,6 +38,9 @@ GEM
activemodel (= 5.1.7)
activesupport (= 5.1.7)
arel (~> 8.0)
+ activerecord-explain-analyze (0.1.0)
+ activerecord (>= 4)
+ pg
activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6)
activesupport (5.1.7)
@@ -139,9 +142,9 @@ GEM
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
- concurrent-ruby (1.1.3)
- concurrent-ruby-ext (1.1.3)
- concurrent-ruby (= 1.1.3)
+ concurrent-ruby (1.1.5)
+ concurrent-ruby-ext (1.1.5)
+ concurrent-ruby (= 1.1.5)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@@ -1012,6 +1015,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
acme-client (~> 2.0.2)
+ activerecord-explain-analyze (~> 0.1)
activerecord_sane_schema_dumper (= 1.0)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2)
diff --git a/VERSION b/VERSION
index 6a02cfa7c06..11bf1ec035c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-11.12.0-pre
+12.0.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index e583a8affd4..7cebb88f3a4 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -12,7 +12,7 @@ const Api = {
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
- projectLabelsPath: '/:namespace_path/:project_path/labels',
+ projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 670f66b005e..c8eb96a625c 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -37,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts {
}
// Sanity check: Make sure the selected text comes from a discussion : it can either contain a message...
- let foundMessage = !!documentFragment.querySelector('.md');
+ let foundMessage = Boolean(documentFragment.querySelector('.md'));
// ... Or come from a message
if (!foundMessage) {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index c9972d051aa..b1a8b13f3ac 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -142,8 +142,10 @@ export default {
const card = this.$refs.issue[e.oldIndex];
card.showDetail = false;
- boardsStore.moving.list = card.list;
- boardsStore.moving.issue = boardsStore.moving.list.findIssue(+e.item.dataset.issueId);
+
+ const { list } = card;
+ const issue = list.findIssue(Number(e.item.dataset.issueId));
+ boardsStore.startMoving(list, issue);
sortableStart();
},
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index dc1bdc23b5e..63dc99db086 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -72,7 +72,7 @@ export default {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.detail.issue = issue;
+ boardsStore.setIssueDetail(issue);
boardsStore.detail.list = this.list;
})
.catch(() => {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 17de7b2cf1e..a8516f178fc 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
@@ -136,23 +135,7 @@ export default {
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
- this.applyFilter(filter);
- },
- applyFilter(filter) {
- const filterPath = boardsStore.filter.path.split('&');
- const filterIndex = filterPath.indexOf(filter);
-
- if (filterIndex === -1) {
- filterPath.push(filter);
- } else {
- filterPath.splice(filterIndex, 1);
- }
-
- boardsStore.filter.path = filterPath.join('&');
-
- boardsStore.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
+ boardsStore.toggleFilter(filter);
},
labelStyle(label) {
return {
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 8e09e265cfb..defa1f75ba2 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -124,7 +124,7 @@ export default {
data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = !!foundSelectedIssue;
+ issue.selected = Boolean(foundSelectedIssue);
this.issues.push(issue);
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index bc6a3cf212e..e9cab3e3bba 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -168,7 +168,7 @@ export default () => {
});
}
- boardsStore.detail.issue = newIssue;
+ boardsStore.setIssueDetail(newIssue);
},
clearDetailIssue() {
boardsStore.clearDetailIssue();
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7e5d0e0f888..08aecfab8a4 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -37,8 +37,8 @@ class List {
this.type = obj.list_type;
const typeInfo = this.getTypeInfo(this.type);
- this.preset = !!typeInfo.isPreset;
- this.isExpandable = !!typeInfo.isExpandable;
+ this.preset = Boolean(typeInfo.isPreset);
+ this.isExpandable = Boolean(typeInfo.isExpandable);
this.isExpanded = true;
this.page = 1;
this.loading = true;
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 51565c597e6..da82b52330a 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,7 +1,5 @@
-import { __ } from '~/locale';
-
const notImplemented = () => {
- throw new Error(__('Not implemented!'));
+ throw new Error('Not implemented!');
};
export default {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 05efcbaa3cc..f72ab189015 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,7 @@ import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import eventHub from '../eventhub';
const boardsStore = {
disabled: false,
@@ -109,6 +110,11 @@ const boardsStore = {
});
listFrom.update();
},
+
+ startMoving(list, issue) {
+ Object.assign(this.moving, { list, issue });
+ },
+
moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists();
@@ -183,12 +189,34 @@ const boardsStore = {
findListByLabelId(id) {
return this.state.lists.find(list => list.type === 'label' && list.label.id === id);
},
+
+ toggleFilter(filter) {
+ const filterPath = this.filter.path.split('&');
+ const filterIndex = filterPath.indexOf(filter);
+
+ if (filterIndex === -1) {
+ filterPath.push(filter);
+ } else {
+ filterPath.splice(filterIndex, 1);
+ }
+
+ this.filter.path = filterPath.join('&');
+
+ this.updateFiltersUrl();
+
+ eventHub.$emit('updateTokens');
+ },
+
updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
},
clearDetailIssue() {
- this.detail.issue = {};
+ this.setIssueDetail({});
+ },
+
+ setIssueDetail(issueDetail) {
+ this.detail.issue = issueDetail;
},
};
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 8e61b93e824..77ba68be07e 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,8 +1,7 @@
import * as mutationTypes from './mutation_types';
-import { __ } from '~/locale';
const notImplemented = () => {
- throw new Error(__('Not implemented!'));
+ throw new Error('Not implemented!');
};
export default {
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index f34496f84c6..f4c3fa185d8 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -23,7 +23,7 @@ class DeleteModal {
const branchData = e.currentTarget.dataset;
this.branchName = branchData.branchName || '';
this.deletePath = branchData.deletePath || '';
- this.isMerged = !!branchData.isMerged;
+ this.isMerged = Boolean(branchData.isMerged);
this.updateModal();
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 70af333a0dd..bc2e71b99f2 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -353,8 +353,10 @@ export default class Clusters {
saveKnativeDomain(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
- this.service.updateApplication(appId, data.params);
+ this.store.updateApplication(appId);
+ this.service.updateApplication(appId, data.params).catch(() => {
+ this.store.notifyUpdateFailure(appId);
+ });
}
setKnativeHostname(data) {
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 5f7675bb432..7b173be599a 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -89,6 +89,10 @@ export default {
type: Boolean,
required: false,
},
+ updateable: {
+ type: Boolean,
+ default: true,
+ },
updateSuccessful: {
type: Boolean,
required: false,
@@ -138,7 +142,7 @@ export default {
);
},
hasLogo() {
- return !!this.logoUrl;
+ return Boolean(this.logoUrl);
},
identiconId() {
// generate a deterministic integer id for the identicon background
@@ -326,36 +330,38 @@ export default {
</ul>
</div>
- <div
- v-if="shouldShowUpgradeDetails"
- class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
- >
- {{ versionLabel }}
- <span v-if="updateSuccessful">to</span>
-
- <gl-link
- v-if="updateSuccessful"
- :href="chartRepo"
- target="_blank"
- class="js-cluster-application-upgrade-version"
- >chart v{{ version }}</gl-link
+ <div v-if="updateable">
+ <div
+ v-if="shouldShowUpgradeDetails"
+ class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
>
- </div>
+ {{ versionLabel }}
+ <span v-if="updateSuccessful">to</span>
- <div
- v-if="updateFailed && !isUpgrading"
- class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
- >
- {{ upgradeFailureDescription }}
+ <gl-link
+ v-if="updateSuccessful"
+ :href="chartRepo"
+ target="_blank"
+ class="js-cluster-application-upgrade-version"
+ >chart v{{ version }}</gl-link
+ >
+ </div>
+
+ <div
+ v-if="updateFailed && !isUpgrading"
+ class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
+ >
+ {{ upgradeFailureDescription }}
+ </div>
+ <loading-button
+ v-if="upgradeAvailable || updateFailed || isUpgrading"
+ class="btn btn-primary js-cluster-application-upgrade-button mt-2"
+ :loading="isUpgrading"
+ :disabled="isUpgrading"
+ :label="upgradeButtonLabel"
+ @click="upgradeClicked"
+ />
</div>
- <loading-button
- v-if="upgradeAvailable || updateFailed || isUpgrading"
- class="btn btn-primary js-cluster-application-upgrade-button mt-2"
- :loading="isUpgrading"
- :disabled="isUpgrading"
- :label="upgradeButtonLabel"
- @click="upgradeClicked"
- />
</div>
<div
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 73760da9b98..2d129245d37 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -15,6 +15,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
@@ -25,6 +26,7 @@ export default {
clipboardButton,
LoadingButton,
GlLoadingIcon,
+ KnativeDomainEditor,
},
props: {
type: {
@@ -154,64 +156,21 @@ export default {
knative() {
return this.applications.knative;
},
- knativeInstalled() {
- return (
- this.knative.status === APPLICATION_STATUS.INSTALLED ||
- this.knativeUpgrading ||
- this.knativeUpgradeFailed ||
- this.knative.status === APPLICATION_STATUS.UPDATED
- );
- },
- knativeUpgrading() {
- return (
- this.knative.status === APPLICATION_STATUS.UPDATING ||
- this.knative.status === APPLICATION_STATUS.SCHEDULED
- );
- },
- knativeUpgradeFailed() {
- return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED;
- },
- knativeExternalEndpoint() {
- return this.knative.externalIp || this.knative.externalHostname;
- },
- knativeDescription() {
- return sprintf(
- _.escape(
- s__(
- `ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`,
- ),
- ),
- {
- pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
- target="_blank" rel="noopener noreferrer">
- ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
- },
- false,
- );
- },
- canUpdateKnativeEndpoint() {
- return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading;
- },
- knativeHostname: {
- get() {
- return this.knative.hostname;
- },
- set(hostname) {
- eventHub.$emit('setKnativeHostname', {
- id: 'knative',
- hostname,
- });
- },
- },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
},
methods: {
- saveKnativeDomain() {
+ saveKnativeDomain(hostname) {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
- params: { hostname: this.knative.hostname },
+ params: { hostname },
+ });
+ },
+ setKnativeHostname(hostname) {
+ eventHub.$emit('setKnativeHostname', {
+ id: 'knative',
+ hostname,
});
},
},
@@ -318,9 +277,9 @@ export default {
generated endpoint in order to access
your application after it has been deployed.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</div>
@@ -330,9 +289,9 @@ export default {
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</template>
<template v-if="!ingressInstalled">
@@ -361,9 +320,9 @@ export default {
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
- <label for="cert-manager-issuer-email">{{
- s__('ClusterIntegration|Issuer Email')
- }}</label>
+ <label for="cert-manager-issuer-email">
+ {{ s__('ClusterIntegration|Issuer Email') }}
+ </label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
@@ -491,9 +450,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
</div>
</template>
@@ -514,6 +473,7 @@ export default {
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
+ :updateable="false"
:disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
@@ -525,9 +485,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
- <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
+ <a :href="helpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
</p>
<br />
</span>
@@ -540,83 +500,13 @@ export default {
}}
</p>
- <div class="row">
- <template v-if="knativeInstalled || (helmInstalled && rbac)">
- <div
- :class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }"
- class="form-group col-sm-12 mb-0"
- >
- <label for="knative-domainname">
- <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
- </label>
- <input
- id="knative-domainname"
- v-model="knativeHostname"
- type="text"
- class="form-control js-knative-domainname"
- />
- </div>
- </template>
- <template v-if="knativeInstalled">
- <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
- <label for="knative-endpoint">
- <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
- </label>
- <div v-if="knativeExternalEndpoint" class="input-group">
- <input
- id="knative-endpoint"
- :value="knativeExternalEndpoint"
- type="text"
- class="form-control js-knative-endpoint"
- readonly
- />
- <span class="input-group-append">
- <clipboard-button
- :text="knativeExternalEndpoint"
- :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
- class="input-group-text js-knative-endpoint-clipboard-btn"
- />
- </span>
- </div>
- <div v-else class="input-group">
- <input type="text" class="form-control js-endpoint" readonly />
- <gl-loading-icon
- class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
- />
- </div>
- </div>
-
- <p class="form-text text-muted col-12">
- {{
- s__(
- `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
- )
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
- </p>
-
- <p
- v-if="!knativeExternalEndpoint"
- class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
- >
- {{
- s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
- }}
- </p>
-
- <button
- v-if="canUpdateKnativeEndpoint"
- class="btn btn-success js-knative-save-domain-button mt-3 ml-3"
- @click="saveKnativeDomain"
- >
- {{ s__('ClusterIntegration|Save changes') }}
- </button>
- </template>
- </div>
+ <knative-domain-editor
+ v-if="knative.installed || (helmInstalled && rbac)"
+ :knative="knative"
+ :ingress-dns-help-path="ingressDnsHelpPath"
+ @save="saveKnativeDomain"
+ @set="setKnativeHostname"
+ />
</div>
</application-row>
</div>
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
new file mode 100644
index 00000000000..480228619a5
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -0,0 +1,150 @@
+<script>
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
+
+export default {
+ components: {
+ LoadingButton,
+ ClipboardButton,
+ GlLoadingIcon,
+ },
+ props: {
+ knative: {
+ type: Object,
+ required: true,
+ },
+ ingressDnsHelpPath: {
+ type: String,
+ default: '',
+ },
+ },
+ computed: {
+ saveButtonDisabled() {
+ return [UNINSTALLING, UPDATING].includes(this.knative.status);
+ },
+ saving() {
+ return [UPDATING].includes(this.knative.status);
+ },
+ saveButtonLabel() {
+ return this.saving ? this.__('Saving') : this.__('Save changes');
+ },
+ knativeInstalled() {
+ return this.knative.installed;
+ },
+ knativeExternalEndpoint() {
+ return this.knative.externalIp || this.knative.externalHostname;
+ },
+ knativeUpdateSuccessful() {
+ return this.knative.updateSuccessful;
+ },
+ knativeHostname: {
+ get() {
+ return this.knative.hostname;
+ },
+ set(hostname) {
+ this.$emit('set', hostname);
+ },
+ },
+ },
+ watch: {
+ knativeUpdateSuccessful(updateSuccessful) {
+ if (updateSuccessful) {
+ this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row">
+ <div
+ v-if="knative.updateFailed"
+ class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message"
+ >
+ {{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
+ </div>
+
+ <template>
+ <div
+ :class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
+ class="form-group col-sm-12 mb-0"
+ >
+ <label for="knative-domainname">
+ <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="knativeHostname"
+ type="text"
+ class="form-control js-knative-domainname"
+ />
+ </div>
+ </template>
+ <template v-if="knativeInstalled">
+ <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
+ <label for="knative-endpoint">
+ <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
+ </label>
+ <div v-if="knativeExternalEndpoint" class="input-group">
+ <input
+ id="knative-endpoint"
+ :value="knativeExternalEndpoint"
+ type="text"
+ class="form-control js-knative-endpoint"
+ readonly
+ />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="knativeExternalEndpoint"
+ :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
+ class="input-group-text js-knative-endpoint-clipboard-btn"
+ />
+ </span>
+ </div>
+ <div v-else class="input-group">
+ <input type="text" class="form-control js-endpoint" readonly />
+ <gl-loading-icon
+ class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
+ />
+ </div>
+ </div>
+
+ <p class="form-text text-muted col-12">
+ {{
+ s__(
+ `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
+ )
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ <p
+ v-if="!knativeExternalEndpoint"
+ class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
+ >
+ {{
+ s__(`ClusterIntegration|The endpoint is in
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
+ }}
+ </p>
+
+ <loading-button
+ class="btn-success js-knative-save-domain-button mt-3 ml-3"
+ :loading="saving"
+ :disabled="saveButtonDisabled"
+ :label="saveButtonLabel"
+ @click="$emit('save', knativeHostname)"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 1b4d7e8372c..89e61c10a46 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -77,6 +77,8 @@ export default class ClusterStore {
isEditingHostName: false,
externalIp: null,
externalHostname: null,
+ updateSuccessful: false,
+ updateFailed: false,
},
},
};
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 37a3ceb5341..5bfe158ceda 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -40,7 +40,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
},
selectable: true,
filterable: true,
- filterRemote: !!$dropdown.data('refsUrl'),
+ filterRemote: Boolean($dropdown.data('refsUrl')),
fieldName: $dropdown.data('fieldName'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 916b190f469..fa0f04c7d82 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -12,7 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
- this.getDataRemote = !!options.filterRemote;
+ this.getDataRemote = Boolean(options.filterRemote);
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index a767379d662..bd7259ce3ee 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -69,7 +69,7 @@ export default {
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
- :img-size="36"
+ :img-size="40"
class="avatar-cell d-none d-sm-block"
/>
<div class="commit-detail flex-list">
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d26b58d461a..eb9f1465945 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -254,16 +254,17 @@ export default {
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<div class="btn-group" role="group">
<template v-if="diffFile.blob && diffFile.blob.readable_text">
- <button
- :disabled="!diffHasDiscussions(diffFile)"
- :class="{ active: hasExpandedDiscussions }"
- :title="s__('MergeRequests|Toggle comments for this file')"
- class="js-btn-vue-toggle-comments btn"
- type="button"
- @click="handleToggleDiscussions"
- >
- <icon name="comment" />
- </button>
+ <span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')">
+ <gl-button
+ :disabled="!diffHasDiscussions(diffFile)"
+ :class="{ active: hasExpandedDiscussions }"
+ class="js-btn-vue-toggle-comments btn"
+ type="button"
+ @click="handleToggleDiscussions"
+ >
+ <icon name="comment" />
+ </gl-button>
+ </span>
<edit-button
v-if="!diffFile.deleted_file"
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 0c0a0faa59d..7cf3d90d468 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -86,7 +86,6 @@ export default {
:key="note.id"
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
- :size="19"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions"
/>
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index f0cc5de4b33..dcb79cd5e16 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -38,7 +38,7 @@ export default {
<template>
<gl-button
- v-gl-tooltip.bottom
+ v-gl-tooltip.top
:href="editPath"
:title="__('Edit file')"
class="js-edit-blob"
diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js
index e39452353f5..ce315963723 100644
--- a/app/assets/javascripts/error_tracking_settings/index.js
+++ b/app/assets/javascripts/error_tracking_settings/index.js
@@ -1,10 +1,8 @@
import Vue from 'vue';
import ErrorTrackingSettings from './components/app.vue';
import createStore from './store';
-import initSettingsPanels from '~/settings_panels';
export default () => {
- initSettingsPanels();
const formContainerEl = document.querySelector('.js-error-tracking-form');
const {
dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js
index a008b181907..d77e5f15469 100644
--- a/app/assets/javascripts/error_tracking_settings/store/getters.js
+++ b/app/assets/javascripts/error_tracking_settings/store/getters.js
@@ -2,10 +2,10 @@ import _ from 'underscore';
import { __, s__, sprintf } from '~/locale';
import { getDisplayName } from '../utils';
-export const hasProjects = state => !!state.projects && state.projects.length > 0;
+export const hasProjects = state => Boolean(state.projects) && state.projects.length > 0;
export const isProjectInvalid = (state, getters) =>
- !!state.selectedProject &&
+ Boolean(state.selectedProject) &&
getters.hasProjects &&
!state.projects.some(project => _.isMatch(state.selectedProject, project));
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index 3dd89a82a42..ba62ab67e50 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -51,7 +51,7 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
const params = {
simple: true,
per_page: 20,
- membership: !!gon.current_user_id,
+ membership: Boolean(gon.current_user_id),
};
if (state.namespace === 'projects') {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index f437954881c..0af9aabd8cf 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import 'at.js';
import _ from 'underscore';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index a143d79097b..18fa6265108 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -307,8 +307,8 @@ GitLabDropdown = (function() {
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
- this.highlight = !!this.options.highlight;
- this.icon = !!this.options.icon;
+ this.highlight = Boolean(this.options.highlight);
+ this.icon = Boolean(this.options.icon);
this.filterInputBlur =
this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 5a6d44ef838..a66555838ba 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -13,7 +13,7 @@ export default class GLForm {
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
if (item !== 'emojis') {
- this.enableGFM[item] = !!dataSources[item];
+ this.enableGFM[item] = Boolean(dataSources[item]);
}
});
// Before we start, we should clean up any previous data for this form
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index c98dda00817..6999746f115 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -105,7 +105,7 @@ export default {
.then(() => {
this.initManager('#ide-preview', this.sandboxOpts, {
fileResolver: {
- isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
+ isFile: p => Promise.resolve(Boolean(this.entries[createPathWithExt(p)])),
readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content),
},
});
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 99f1d4a573d..5201c33b1b4 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -30,7 +30,7 @@ export default {
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() {
- return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
+ return Boolean(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
},
activeFileKey() {
return this.activeFile ? this.activeFile.key : null;
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index e35595ab1fd..dac2a8e8b51 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -11,7 +11,7 @@ export const defaultEditorOptions = {
export default [
{
- readOnly: model => !!model.file.file_lock,
+ readOnly: model => Boolean(model.file.file_lock),
quickSuggestions: model => !(model.language === 'markdown'),
},
];
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 7a88ac5b116..5a736805fdc 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -42,9 +42,10 @@ export const emptyRepo = state =>
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+export const hasChanges = state =>
+ Boolean(state.changedFiles.length) || Boolean(state.stagedFiles.length);
-export const hasMergeRequest = state => !!state.currentMergeRequestId;
+export const hasMergeRequest = state => Boolean(state.currentMergeRequestId);
export const allBlobs = state =>
Object.keys(state.entries)
@@ -70,7 +71,7 @@ export const isCommitModeActive = state => state.currentActivityView === activit
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommittedChanges = state =>
- !!(state.changedFiles.length || state.stagedFiles.length);
+ Boolean(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length;
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index f81bdb8a30e..77ea2084877 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -102,7 +102,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content,
- changed: !!changedFile,
+ changed: Boolean(changedFile),
});
});
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index ef7cd4ff8e8..1d127d915d7 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -1,6 +1,6 @@
import { states } from './constants';
-export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
+export const hasLatestPipeline = state => !state.isLoadingPipeline && Boolean(state.latestPipeline);
export const pipelineFailed = state =>
state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 344b189decf..ae42b87c9a7 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -142,7 +142,7 @@ export default {
Object.assign(state.entries[file.path], {
raw: file.content,
- changed: !!changedFile,
+ changed: Boolean(changedFile),
staged: false,
prevPath: '',
moved: false,
diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
index 05000c73052..7051a968dac 100644
--- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
@@ -14,7 +14,7 @@ export function addCommentIndicator(containerEl, { x, y }) {
export function removeCommentIndicator(imageFrameEl) {
const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator');
const imageEl = imageFrameEl.querySelector('img');
- const willRemove = !!commentIndicatorEl;
+ const willRemove = Boolean(commentIndicatorEl);
let meta = {};
if (willRemove) {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 3587f073a00..26c1b0ec7be 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -6,8 +6,8 @@ import { isImageLoaded } from '../lib/utils/image_utility';
export default class ImageDiff {
constructor(el, options) {
this.el = el;
- this.canCreateNote = !!(options && options.canCreateNote);
- this.renderCommentBadge = !!(options && options.renderCommentBadge);
+ this.canCreateNote = Boolean(options && options.canCreateNote);
+ this.renderCommentBadge = Boolean(options && options.renderCommentBadge);
this.$noteContainer = $('.note-container', this.el);
this.imageBadges = [];
}
diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js
index ab0a595571f..1a5123de220 100644
--- a/app/assets/javascripts/image_diff/view_types.js
+++ b/app/assets/javascripts/image_diff/view_types.js
@@ -5,5 +5,5 @@ export const viewTypes = {
};
export function isValidViewType(validate) {
- return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate);
+ return Boolean(Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate));
}
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index f51c7a2f990..16f88cddce3 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -12,7 +12,7 @@ export default class IssuableIndex {
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
- const alreadyInitialized = !!this.bulkUpdateSidebar;
+ const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index ab0b4231255..e88ca4747c5 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -156,7 +156,7 @@ export default {
return this.store.formState;
},
hasUpdated() {
- return !!this.state.updatedAt;
+ return Boolean(this.state.updatedAt);
},
issueChanged() {
const {
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index d2f33dc31a7..1e1dce5f4fc 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -71,7 +71,7 @@ export default {
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
}"
- class="title"
+ class="title qa-title"
dir="auto"
v-html="titleHtml"
></h2>
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index c6dd21cd2d4..7064731a5ea 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -53,7 +53,7 @@ export default class LabelManager {
toggleEmptyState($label, $btn, action) {
this.emptyState.classList.toggle(
'hidden',
- !!this.prioritizedLabels[0].querySelector(':scope > li'),
+ Boolean(this.prioritizedLabels[0].querySelector(':scope > li')),
);
}
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js
index 1d18992af63..39cffedcac6 100644
--- a/app/assets/javascripts/lib/utils/accessor.js
+++ b/app/assets/javascripts/lib/utils/accessor.js
@@ -2,7 +2,7 @@ function isPropertyAccessSafe(base, property) {
let safe;
try {
- safe = !!base[property];
+ safe = Boolean(base[property]);
} catch (error) {
safe = false;
}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 32cafb74d91..d3e6851496b 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -513,7 +513,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
+ const isNonZero = Boolean(unitValue);
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 84a617acb42..b7922e29bb0 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -223,9 +223,9 @@ export function insertMarkdownText({
return tag.replace(textPlaceholder, val);
}
if (val.indexOf(tag) === 0) {
- return '' + val.replace(tag, '');
+ return String(val.replace(tag, ''));
} else {
- return '' + tag + val;
+ return String(tag) + val;
}
})
.join('\n');
@@ -233,7 +233,7 @@ export function insertMarkdownText({
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else {
- textToInsert = '' + startChar + tag + selected + (wrap ? tag : ' ');
+ textToInsert = String(startChar) + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index bdfd06fc250..4a9cd1b6f42 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -121,4 +121,40 @@ export function webIDEUrl(route = undefined) {
return returnUrl;
}
+/**
+ * Returns current base URL
+ */
+export function getBaseURL() {
+ const { protocol, host } = window.location;
+ return `${protocol}//${host}`;
+}
+
+/**
+ * Returns true if url is an absolute or root-relative URL
+ *
+ * @param {String} url
+ */
+export function isAbsoluteOrRootRelative(url) {
+ return /^(https?:)?\//.test(url);
+}
+
+/**
+ * Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL)
+ *
+ * @param {String} url that will be checked
+ * @returns {Boolean}
+ */
+export function isSafeURL(url) {
+ if (!isAbsoluteOrRootRelative(url)) {
+ return false;
+ }
+
+ try {
+ const parsedUrl = new URL(url, getBaseURL());
+ return ['http:', 'https:'].includes(parsedUrl.protocol);
+ } catch {
+ return false;
+ }
+}
+
export { join as joinPaths } from 'path';
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index a55a47c277d..def810e879a 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -38,7 +38,7 @@ export default {
GlModalDirective,
},
props: {
- externalDashboardPath: {
+ externalDashboardUrl: {
type: String,
required: false,
default: '',
@@ -248,7 +248,7 @@ export default {
>
</gl-dropdown>
</div>
- <div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
+ <div v-if="showTimeWindowDropdown" class="d-flex align-items-center prepend-left-8">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
@@ -299,10 +299,11 @@ export default {
</gl-modal>
</div>
<gl-button
- v-if="externalDashboardPath.length"
+ v-if="externalDashboardUrl.length"
class="js-external-dashboard-link prepend-left-8"
variant="primary"
- :href="externalDashboardPath"
+ :href="externalDashboardUrl"
+ target="_blank"
>
{{ __('View full dashboard') }}
<icon name="external-link" />
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 74c4ae64712..3fd9e07fa8b 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -8,8 +8,5 @@ export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
-export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT';
-export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT';
-export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
index b10e9f9f9f1..e48cfcd9564 100644
--- a/app/assets/javascripts/mr_notes/stores/getters.js
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -1,5 +1,5 @@
export default {
isLoggedIn(state, getters) {
- return !!getters.getUserData.id;
+ return Boolean(getters.getUserData.id);
},
};
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 5b6163a6214..228bb652597 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -49,7 +49,7 @@ export default {
computed: {
...mapGetters(['userCanReply']),
hasReplies() {
- return !!this.replies.length;
+ return Boolean(this.replies.length);
},
replies() {
return this.discussion.notes.slice(1);
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 47d74c2f892..aa80e25a3e0 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -10,7 +10,7 @@ import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
-import noteBody from './note_body.vue';
+import NoteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -21,7 +21,7 @@ export default {
userAvatarLink,
noteHeader,
noteActions,
- noteBody,
+ NoteBody,
TimelineEntryItem,
},
mixins: [noteable, resolvable, draftMixin],
@@ -75,7 +75,7 @@ export default {
};
},
canReportAsAbuse() {
- return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
+ return Boolean(this.note.report_abuse_path) && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
@@ -209,7 +209,10 @@ export default {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.note.note = noteText;
+ const { noteBody } = this.$refs;
+ if (noteBody) {
+ noteBody.note.note = noteText;
+ }
},
},
};
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 0f1976db37d..4d00e957973 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -127,6 +127,9 @@ export default {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
+ beforeDestroy() {
+ this.stopPolling();
+ },
methods: {
...mapActions([
'setLoadingState',
@@ -144,6 +147,7 @@ export default {
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
+ 'stopPolling',
]),
fetchNotes() {
if (this.isFetching) return null;
diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js
index 97f3ea0d5de..ded0ac3cfa9 100644
--- a/app/assets/javascripts/notes/mixins/issuable_state.js
+++ b/app/assets/javascripts/notes/mixins/issuable_state.js
@@ -1,11 +1,11 @@
export default {
methods: {
isConfidential(issue) {
- return !!issue.confidential;
+ return Boolean(issue.confidential);
},
isLocked(issue) {
- return !!issue.discussion_locked;
+ return Boolean(issue.discussion_locked);
},
hasWarning(issue) {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 2d150e64ef7..d7982be3e4b 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -20,7 +20,7 @@ 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 userCanReply = state => Boolean(state.noteableData.current_user.can_create_note);
export const openState = state => state.noteableData.state;
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
index 0a87d193b72..ed518611d0b 100644
--- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -1,4 +1,5 @@
<script>
+import { mapState, mapActions } from 'vuex';
import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
export default {
@@ -8,26 +9,34 @@ export default {
GlFormInput,
GlLink,
},
- props: {
- externalDashboardPath: {
- type: String,
- required: false,
- default: '',
- },
- externalDashboardHelpPagePath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'externalDashboardHelpPagePath',
+ 'externalDashboardUrl',
+ 'operationsSettingsEndpoint',
+ ]),
+ userDashboardUrl: {
+ get() {
+ return this.externalDashboardUrl;
+ },
+ set(url) {
+ this.setExternalDashboardUrl(url);
+ },
},
},
+ methods: {
+ ...mapActions(['setExternalDashboardUrl', 'updateExternalDashboardUrl']),
+ },
};
</script>
<template>
- <section class="settings expanded">
+ <section class="settings no-animate">
<div class="settings-header">
<h4 class="js-section-header">
{{ s__('ExternalMetrics|External Dashboard') }}
</h4>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{
s__(
@@ -44,11 +53,12 @@ export default {
:description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
>
<gl-form-input
- :value="externalDashboardPath"
+ v-model="userDashboardUrl"
placeholder="https://my-org.gitlab.io/my-dashboards"
+ @keydown.enter.native.prevent="updateExternalDashboardUrl"
/>
</gl-form-group>
- <gl-button variant="success">
+ <gl-button variant="success" @click="updateExternalDashboardUrl">
{{ __('Save Changes') }}
</gl-button>
</form>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
index 1171f3ece9f..6946578e6d2 100644
--- a/app/assets/javascripts/operation_settings/index.js
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import store from './store';
import ExternalDashboardForm from './components/external_dashboard.vue';
export default () => {
@@ -14,13 +15,9 @@ export default () => {
return new Vue({
el,
+ store: store(el.dataset),
render(createElement) {
- return createElement(ExternalDashboardForm, {
- props: {
- ...el.dataset,
- expanded: false,
- },
- });
+ return createElement(ExternalDashboardForm);
},
});
};
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
new file mode 100644
index 00000000000..ec05b0c76cf
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -0,0 +1,38 @@
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as mutationTypes from './mutation_types';
+
+export const setExternalDashboardUrl = ({ commit }, url) =>
+ commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url);
+
+export const updateExternalDashboardUrl = ({ state, dispatch }) =>
+ axios
+ .patch(state.operationsSettingsEndpoint, {
+ project: {
+ metrics_setting_attributes: {
+ external_dashboard_url: state.externalDashboardUrl,
+ },
+ },
+ })
+ .then(() => dispatch('receiveExternalDashboardUpdateSuccess'))
+ .catch(error => dispatch('receiveExternalDashboardUpdateError', error));
+
+export const receiveExternalDashboardUpdateSuccess = () => {
+ /**
+ * The operations_controller currently handles successful requests
+ * by creating a flash banner messsage to notify the user.
+ */
+ refreshCurrentPage();
+};
+
+export const receiveExternalDashboardUpdateError = (_, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js
new file mode 100644
index 00000000000..e96bb1e8aad
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: createState(initialState),
+ actions,
+ mutations,
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/operation_settings/store/mutation_types.js b/app/assets/javascripts/operation_settings/store/mutation_types.js
new file mode 100644
index 00000000000..237d2b6122f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/mutation_types.js
@@ -0,0 +1,3 @@
+/* eslint-disable import/prefer-default-export */
+
+export const SET_EXTERNAL_DASHBOARD_URL = 'SET_EXTERNAL_DASHBOARD_URL';
diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js
new file mode 100644
index 00000000000..64bb33bb89f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/mutations.js
@@ -0,0 +1,7 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_EXTERNAL_DASHBOARD_URL](state, url) {
+ state.externalDashboardUrl = url;
+ },
+};
diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js
new file mode 100644
index 00000000000..72167141c48
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/store/state.js
@@ -0,0 +1,5 @@
+export default (initialState = {}) => ({
+ externalDashboardUrl: initialState.externalDashboardUrl || '',
+ operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
+ externalDashboardHelpPagePath: initialState.externalDashboardHelpPagePath,
+});
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index bd4309e47ad..bb490919a9a 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -29,7 +29,7 @@ export default {
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
- return !!(this.customInputEnabled || !this.intervalIsPreset);
+ return Boolean(this.customInputEnabled || !this.intervalIsPreset);
},
},
watch: {
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 5270a7924ec..98e19705976 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,7 +1,9 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
import mountOperationSettings from '~/operation_settings';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
mountOperationSettings();
+ initSettingsPanels();
});
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 59c13e1a042..f0d9642a2b2 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -35,7 +35,7 @@ export default () => {
return createElement('delete-account-modal', {
props: {
actionUrl: deleteAccountModalEl.dataset.actionUrl,
- confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword,
+ confirmWithPassword: Boolean(deleteAccountModalEl.dataset.confirmWithPassword),
username: deleteAccountModalEl.dataset.username,
},
});
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
index 4834a856271..f05ad7773a2 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
@@ -57,7 +57,7 @@ export const validateProjectBilling = ({ dispatch, commit, state }) =>
resp => {
const { billingEnabled } = resp.result;
- commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
+ commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled));
dispatch('setIsValidatingProjectBilling', false);
resolve();
},
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
index e39f02d0894..f9e2e2f74fb 100644
--- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
@@ -1,3 +1,3 @@
-export const hasProject = state => !!state.selectedProject.projectId;
-export const hasZone = state => !!state.selectedZone;
-export const hasMachineType = state => !!state.selectedMachineType;
+export const hasProject = state => Boolean(state.selectedProject.projectId);
+export const hasZone = state => Boolean(state.selectedZone);
+export const hasMachineType = state => Boolean(state.selectedMachineType);
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 1ac699c538f..8ace6657ad1 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -9,7 +9,7 @@ export default {
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
- canDelete: !!el.destroy_path,
+ canDelete: Boolean(el.destroy_path),
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
@@ -42,7 +42,7 @@ export default {
location: element.location,
createdAt: element.created_at,
destroyPath: element.destroy_path,
- canDelete: !!element.destroy_path,
+ canDelete: Boolean(element.destroy_path),
}));
},
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 72e061df573..930c0d5e958 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -82,9 +82,9 @@ Sidebar.prototype.toggleTodo = function(e) {
ajaxType = $this.data('deletePath') ? 'delete' : 'post';
if ($this.data('deletePath')) {
- url = '' + $this.data('deletePath');
+ url = String($this.data('deletePath'));
} else {
- url = '' + $this.data('createPath');
+ url = String($this.data('createPath'));
}
$this.tooltip('hide');
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index ab43c2139bf..6aca4067ba7 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -379,7 +379,7 @@ export class SearchAutocomplete {
}
}
}
- this.wrap.toggleClass('has-value', !!e.target.value);
+ this.wrap.toggleClass('has-value', Boolean(e.target.value));
}
onSearchInputFocus() {
@@ -396,7 +396,7 @@ export class SearchAutocomplete {
onClearInputClick(e) {
e.preventDefault();
- this.wrap.toggleClass('has-value', !!e.target.value);
+ this.wrap.toggleClass('has-value', Boolean(e.target.value));
return this.searchInput.val('').focus();
}
@@ -405,7 +405,7 @@ export class SearchAutocomplete {
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
- return this.restoreOriginalState();
+ this.restoreOriginalState();
}
this.dropdownMenu.removeClass('show');
}
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index f9b4e789563..94341050b86 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
+import { CHECKING_INSTALLED } from '../constants';
export default {
components: {
@@ -13,10 +14,6 @@ export default {
GlLoadingIcon,
},
props: {
- installed: {
- type: Boolean,
- required: true,
- },
clustersPath: {
type: String,
required: true,
@@ -31,8 +28,15 @@ export default {
},
},
computed: {
- ...mapState(['isLoading', 'hasFunctionData']),
+ ...mapState(['installed', 'isLoading', 'hasFunctionData']),
...mapGetters(['getFunctions']),
+
+ checkingInstalled() {
+ return this.installed === CHECKING_INSTALLED;
+ },
+ isInstalled() {
+ return this.installed === true;
+ },
},
created() {
this.fetchFunctions({
@@ -47,15 +51,16 @@ export default {
<template>
<section id="serverless-functions">
- <div v-if="installed">
+ <gl-loading-icon
+ v-if="checkingInstalled"
+ :size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+
+ <div v-else-if="isInstalled">
<div v-if="hasFunctionData">
- <gl-loading-icon
- v-if="isLoading"
- :size="2"
- class="prepend-top-default append-bottom-default"
- />
- <template v-else>
- <div class="groups-list-tree-container">
+ <template>
+ <div class="groups-list-tree-container js-functions-wrapper">
<ul class="content-list group-list-tree">
<environment-row
v-for="(env, index) in getFunctions"
@@ -66,6 +71,11 @@ export default {
</ul>
</div>
</template>
+ <gl-loading-icon
+ v-if="isLoading"
+ :size="2"
+ class="prepend-top-default append-bottom-default js-functions-loader"
+ />
</div>
<div v-else class="empty-state js-empty-state">
<div class="text-content">
diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js
index 35f77205f2c..2fa15e56ccb 100644
--- a/app/assets/javascripts/serverless/constants.js
+++ b/app/assets/javascripts/serverless/constants.js
@@ -1,3 +1,7 @@
export const MAX_REQUESTS = 3; // max number of times to retry
export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis
+
+export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed
+
+export const TIMEOUT = 'timeout';
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index 2d3f086ffee..ed3b633d766 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -45,7 +45,7 @@ export default class Serverless {
},
});
} else {
- const { statusPath, clustersPath, helpPath, installed } = document.querySelector(
+ const { statusPath, clustersPath, helpPath } = document.querySelector(
'.js-serverless-functions-page',
).dataset;
@@ -56,7 +56,6 @@ export default class Serverless {
render(createElement) {
return createElement(Functions, {
props: {
- installed: installed !== undefined,
clustersPath,
helpPath,
statusPath,
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index 826501c9022..a0a9fdf7ace 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
-import { MAX_REQUESTS } from '../constants';
+import { __ } from '~/locale';
+import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING);
export const receiveFunctionsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_FUNCTIONS_SUCCESS, data);
-export const receiveFunctionsNoDataSuccess = ({ commit }) =>
- commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS);
+export const receiveFunctionsPartial = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_PARTIAL, data);
+export const receiveFunctionsTimeout = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data);
+export const receiveFunctionsNoDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data);
export const receiveFunctionsError = ({ commit }, error) =>
commit(types.RECEIVE_FUNCTIONS_ERROR, error);
@@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) =>
export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
let retryCount = 0;
+ const functionsPartiallyFetched = data => {
+ if (data.functions !== null && data.functions.length) {
+ dispatch('receiveFunctionsPartial', data);
+ }
+ };
+
dispatch('requestFunctionsLoading');
backOff((next, stop) => {
axios
.get(functionsPath)
.then(response => {
- if (response.status === statusCodes.NO_CONTENT) {
+ if (response.data.knative_installed === CHECKING_INSTALLED) {
retryCount += 1;
if (retryCount < MAX_REQUESTS) {
+ functionsPartiallyFetched(response.data);
next();
} else {
- stop(null);
+ stop(TIMEOUT);
}
} else {
stop(response.data);
@@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => {
.catch(stop);
})
.then(data => {
- if (data !== null) {
+ if (data === TIMEOUT) {
+ dispatch('receiveFunctionsTimeout');
+ createFlash(__('Loading functions timed out. Please reload the page to try again.'));
+ } else if (data.functions !== null && data.functions.length) {
dispatch('receiveFunctionsSuccess', data);
} else {
- dispatch('receiveFunctionsNoDataSuccess');
+ dispatch('receiveFunctionsNoDataSuccess', data);
}
})
.catch(error => {
diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js
index 25b2f7ac38a..b8fa9ea1a01 100644
--- a/app/assets/javascripts/serverless/store/mutation_types.js
+++ b/app/assets/javascripts/serverless/store/mutation_types.js
@@ -1,5 +1,7 @@
export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING';
export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS';
+export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL';
+export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT';
export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS';
export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR';
diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js
index 991f32a275d..2685a5b11ff 100644
--- a/app/assets/javascripts/serverless/store/mutations.js
+++ b/app/assets/javascripts/serverless/store/mutations.js
@@ -5,12 +5,23 @@ export default {
state.isLoading = true;
},
[types.RECEIVE_FUNCTIONS_SUCCESS](state, data) {
- state.functions = data;
+ state.functions = data.functions;
+ state.installed = data.knative_installed;
state.isLoading = false;
state.hasFunctionData = true;
},
- [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) {
+ [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) {
+ state.functions = data.functions;
+ state.installed = true;
+ state.isLoading = true;
+ state.hasFunctionData = true;
+ },
+ [types.RECEIVE_FUNCTIONS_TIMEOUT](state) {
+ state.isLoading = false;
+ },
+ [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) {
state.isLoading = false;
+ state.installed = data.knative_installed;
state.hasFunctionData = false;
},
[types.RECEIVE_FUNCTIONS_ERROR](state, error) {
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
index afc3f37d7ba..fdd29299749 100644
--- a/app/assets/javascripts/serverless/store/state.js
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -1,5 +1,6 @@
export default () => ({
error: null,
+ installed: 'checking',
isLoading: true,
// functions
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index c03b2a68c78..d84d5344935 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -49,10 +49,10 @@ export default {
},
computed: {
hasTimeSpent() {
- return !!this.timeSpent;
+ return Boolean(this.timeSpent);
},
hasTimeEstimate() {
- return !!this.timeEstimate;
+ return Boolean(this.timeEstimate);
},
showComparisonState() {
return this.hasTimeEstimate && this.hasTimeSpent;
@@ -67,7 +67,7 @@ export default {
return !this.hasTimeEstimate && !this.hasTimeSpent;
},
showHelpState() {
- return !!this.showHelp;
+ return Boolean(this.showHelp);
},
},
created() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index ad0464a3a98..abe5bdd2901 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -77,16 +77,16 @@ export default {
return this.deployment.external_url;
},
hasExternalUrls() {
- return !!(this.deployment.external_url && this.deployment.external_url_formatted);
+ return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
hasDeploymentTime() {
- return !!(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
+ return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
},
hasDeploymentMeta() {
- return !!(this.deployment.url && this.deployment.name);
+ return Boolean(this.deployment.url && this.deployment.name);
},
hasMetrics() {
- return !!this.deployment.metrics_url;
+ return Boolean(this.deployment.metrics_url);
},
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
index 040315b3c66..19a222462b3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="m-3 ml-5" :class="messageClass">
+ <div class="m-3 ml-7" :class="messageClass">
<slot></slot>
<gl-link v-if="helpPath" :href="helpPath" target="_blank">
<icon :size="16" name="question-o" class="align-middle" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 0686409a785..03a15ba81ed 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -56,7 +56,7 @@ export default {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
- return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
+ return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index a3a44dd8e99..83e7d6db9fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -35,9 +35,7 @@ export default {
<status-icon status="warning" />
<div class="media-body space-children">
<span class="bold">
- <template v-if="mr.mergeError"
- >{{ mr.mergeError }}.</template
- >
+ <template v-if="mr.mergeError">{{ mr.mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span>
<button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 1b3af2fccf2..88e1ccbaf35 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -57,7 +57,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
should_remove_source_branch: true,
};
@@ -85,7 +85,7 @@ export default {
<h4 class="d-flex align-items-start">
<span class="append-right-10">
{{ s__('mrWidget|Set by') }}
- <mr-widget-author :author="mr.setToMWPSBy" />
+ <mr-widget-author :author="mr.setToAutoMergeBy" />
{{ s__('mrWidget|to be merged automatically when the pipeline succeeds') }}
</span>
<a
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 851939d5d4e..615d59a7b8e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -31,7 +31,7 @@ export default {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
- setToMergeWhenPipelineSucceeds: false,
+ autoMergeStrategy: undefined,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
@@ -42,7 +42,7 @@ export default {
};
},
computed: {
- shouldShowMergeWhenPipelineSucceedsText() {
+ shouldShowAutoMergeText() {
return this.mr.isPipelineActive;
},
status() {
@@ -87,7 +87,7 @@ export default {
mergeButtonText() {
if (this.isMergingImmediately) {
return __('Merge in progress');
- } else if (this.shouldShowMergeWhenPipelineSucceedsText) {
+ } else if (this.shouldShowAutoMergeText) {
return __('Merge when pipeline succeeds');
}
@@ -104,7 +104,7 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1;
},
shouldShowMergeControls() {
- return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
+ return this.mr.isMergeAllowed || this.shouldShowAutoMergeText;
},
shouldShowSquashEdit() {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
@@ -126,12 +126,12 @@ export default {
this.isMergingImmediately = true;
}
- this.setToMergeWhenPipelineSucceeds = mergeWhenBuildSucceeds === true;
+ this.autoMergeStrategy = mergeWhenBuildSucceeds ? 'merge_when_pipeline_succeeds' : undefined;
const options = {
sha: this.mr.sha,
commit_message: this.commitMessage,
- merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
+ auto_merge_strategy: this.autoMergeStrategy,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
squash_commit_message: this.squashCommitMessage,
@@ -330,6 +330,7 @@ export default {
:commits-count="mr.commitsCount"
:target-branch="mr.targetBranch"
:is-fast-forward-enabled="mr.ffOnlyEnabled"
+ :class="{ 'border-bottom': mr.mergeError }"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index b1f5655a15a..accb9d9fef1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -29,8 +29,8 @@ export default {
</script>
<template>
- <div class="accept-control inline">
- <label class="merge-param-checkbox">
+ <div class="inline">
+ <label>
<input
:checked="value"
:disabled="isDisabled"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index bf175eb5f69..d02bb2f341d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import { __ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -97,7 +97,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
+ return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
},
shouldRenderSourceBranchRemovalStatus() {
return (
@@ -125,6 +125,11 @@ export default {
this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
);
},
+ mergeError() {
+ return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), {
+ mergeError: this.mr.mergeError,
+ });
+ },
},
watch: {
state(newVal, oldVal) {
@@ -370,6 +375,10 @@ export default {
}}
</mr-widget-alert-message>
+ <mr-widget-alert-message v-if="mr.mergeError" type="danger">
+ {{ mergeError }}
+ </mr-widget-alert-message>
+
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 0cc4fd59f5e..3ab229567f6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -23,8 +23,8 @@ export default function deviseState(data) {
return stateKey.pipelineBlocked;
} else if (this.isSHAMismatch) {
return stateKey.shaMismatch;
- } else if (this.mergeWhenPipelineSucceeds) {
- return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
+ } else if (this.autoMergeEnabled) {
+ return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled;
} else if (!this.canMerge) {
return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 45708d78886..32badb0fb08 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -61,7 +61,7 @@ export default class MergeRequestStore {
this.updatedAt = data.updated_at;
this.metrics = MergeRequestStore.buildMetrics(data.metrics);
- this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {});
+ this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
@@ -70,15 +70,16 @@ export default class MergeRequestStore {
this.targetBranchPath = data.target_branch_commits_path;
this.targetBranchTreePath = data.target_branch_tree_path;
this.conflictResolutionPath = data.conflict_resolution_path;
- this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
+ this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
- this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
+ this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
+ this.autoMergeStrategy = data.auto_merge_strategy;
this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled;
- this.shouldBeRebased = !!data.should_be_rebased;
+ this.shouldBeRebased = Boolean(data.should_be_rebased);
this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
@@ -91,9 +92,9 @@ export default class MergeRequestStore {
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
- this.canMerge = !!data.merge_path;
+ this.canMerge = Boolean(data.merge_path);
this.canCreateIssue = currentUser.can_create_issue || false;
- this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
+ this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index e080ce5c229..48bc6a867f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -13,7 +13,7 @@ const stateToComponentMap = {
unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed',
- mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
+ autoMergeEnabled: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'sha-mismatch',
@@ -45,7 +45,7 @@ export const stateKey = {
pipelineBlocked: 'pipelineBlocked',
shaMismatch: 'shaMismatch',
autoMergeFailed: 'autoMergeFailed',
- mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
+ autoMergeEnabled: 'autoMergeEnabled',
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
rebase: 'rebase',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 3b57b5e8da4..d6c398c8946 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -33,37 +33,36 @@ export default {
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">
- Markdown is supported
- </gl-link>
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1"
+ >Markdown is supported</gl-link
+ >
</template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath">
- <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1"> Markdown </gl-link>
- and
- <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">
- quick actions
- </gl-link>
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">Markdown</gl-link> and
+ <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">quick actions</gl-link>
are supported
</template>
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
- <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i>
- <span class="attaching-file-message"></span> <span class="uploading-progress">0%</span>
+ <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i>
+ <span class="attaching-file-message"></span>
+ <span class="uploading-progress">0%</span>
<span class="uploading-spinner">
- <i class="fa fa-spinner fa-spin toolbar-button-icon" aria-hidden="true"> </i>
+ <i class="fa fa-spinner fa-spin toolbar-button-icon" aria-hidden="true"></i>
</span>
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
- <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i>
+ <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i>
</span>
<span class="uploading-error-message"></span>
<button class="retry-uploading-link" type="button">Try again</button> or
<button class="attach-new-file markdown-selector" type="button">attach a new file</button>
</span>
- <button class="markdown-selector button-attach-file" tabindex="-1" type="button">
- <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i> Attach a file
+ <button class="markdown-selector button-attach-file btn-link" tabindex="-1" type="button">
+ <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i
+ ><span class="text-attach-file">Attach a file</span>
</button>
<button class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button">
Cancel
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index fa502b9beb9..8104d919bf6 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -34,7 +34,7 @@ export default {
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
- setDefaultDate: !!this.selectedDate,
+ setDefaultDate: Boolean(this.selectedDate),
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue
index 8e0b08032f7..9cce9a4e542 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue
@@ -121,7 +121,7 @@ export default {
this.change(1);
break;
default:
- this.change(+text);
+ this.change(Number(text));
break;
}
},
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 93377b8dd91..7f6384f4eea 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -22,7 +22,9 @@ body,
.form-control,
.search form {
// Override default font size used in non-csslab UI
- font-size: 14px;
+ // Use rem to keep default font-size at 14px on body so 1rem still
+ // fits 8px grid, but also allow users to change browser font size
+ font-size: .875rem;
}
legend {
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 25ee3ca944d..1afa5ed90f4 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -7,9 +7,6 @@ $avatar-sizes: (
18: (
border-radius: $border-radius-small
),
- 19: (
- border-radius: $border-radius-small
- ),
20: (
border-radius: $border-radius-small
),
@@ -28,17 +25,11 @@ $avatar-sizes: (
line-height: 32px,
border-radius: $border-radius-default
),
- 36: (
- border-radius: $border-radius-default
- ),
40: (
font-size: 16px,
line-height: 38px,
border-radius: $border-radius-default
),
- 46: (
- border-radius: $border-radius-default
- ),
48: (
font-size: 20px,
line-height: 48px,
@@ -54,37 +45,16 @@ $avatar-sizes: (
line-height: 64px,
border-radius: $border-radius-large
),
- 70: (
- font-size: 34px,
- line-height: 70px,
- border-radius: $border-radius-large
- ),
90: (
font-size: 36px,
line-height: 88px,
border-radius: $border-radius-large
),
- 96: (
- font-size: 48px,
- line-height: 96px,
- border-radius: $border-radius-large
- ),
100: (
font-size: 36px,
line-height: 98px,
border-radius: $border-radius-large
),
- 110: (
- font-size: 40px,
- line-height: 108px,
- font-weight: $gl-font-weight-normal,
- border-radius: $border-radius-large
- ),
- 140: (
- font-size: 72px,
- line-height: 138px,
- border-radius: $border-radius-large
- ),
160: (
font-size: 96px,
line-height: 158px,
@@ -97,13 +67,13 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
.avatar-circle {
float: left;
- margin-right: 15px;
+ margin-right: $gl-padding;
border-radius: $avatar-radius;
border: 1px solid $gray-normal;
@each $size, $size-config in $avatar-sizes {
&.s#{$size} {
- @include avatar-size(#{$size}px, if($size < 36, 8px, 16px));
+ @include avatar-size(#{$size}px, if($size < 48, 8px, 16px));
}
}
}
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index 658e0ff638e..8c32b6c8985 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -17,7 +17,7 @@ body {
text-align: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
- font-size: 14px;
+ font-size: .875rem;
}
h1 {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b85abfd9c14..97a763671ba 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,12 +1,12 @@
@mixin btn-comment-icon {
border-radius: 50%;
background: $white-light;
- padding: 1px 5px;
+ padding: 1px;
font-size: 12px;
color: $blue-500;
+ border: 1px solid $blue-500;
width: 24px;
height: 24px;
- border: 1px solid $blue-500;
&:hover,
&.inverted {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 8493dfff1c5..ef6f0633150 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -329,7 +329,7 @@ span.idiff {
background-color: $gray-light;
border-bottom: 1px solid $border-color;
border-top: 1px solid $border-color;
- padding: 5px $gl-padding;
+ padding: $gl-padding-8 $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 33930871cdc..2a601afff53 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -47,14 +47,6 @@ label {
margin: 0;
}
-.form-label {
- @extend label;
-}
-
-.form-control-label {
- @extend .col-md-2;
-}
-
.inline-input-group {
width: 250px;
}
@@ -87,44 +79,14 @@ label {
margin-left: 0;
margin-right: 0;
- .form-control-label {
- font-weight: $gl-font-weight-bold;
- padding-top: 4px;
- }
-
.form-control {
height: 29px;
background: $white-light;
font-family: $monospace-font;
}
- .input-group-prepend .btn,
- .input-group-append .btn {
- padding: 3px $gl-btn-padding;
- background-color: $gray-light;
- border: 1px solid $border-color;
- }
-
- .text-block {
- line-height: 0.8;
- padding-top: 9px;
-
- code {
- line-height: 1.8;
- }
-
- img {
- margin-right: $gl-padding;
- }
- }
-
@include media-breakpoint-down(xs) {
padding: 0 $gl-padding;
-
- .form-control-label,
- .text-block {
- padding-left: 0;
- }
}
}
@@ -146,19 +108,6 @@ label {
}
}
-.select-wrapper {
- position: relative;
-
- .fa-chevron-down {
- position: absolute;
- font-size: 10px;
- right: 10px;
- top: 12px;
- color: $gray-darkest;
- pointer-events: none;
- }
-}
-
.select-control {
padding-left: 10px;
padding-right: 10px;
@@ -181,12 +130,6 @@ label {
margin-top: 35px;
}
-.form-group .form-control-label,
-.form-group .form-control-label-full-width {
- font-weight: $gl-font-weight-normal;
-}
-
-
.form-control::placeholder {
color: $gl-text-color-tertiary;
}
@@ -230,7 +173,8 @@ label {
border: 1px solid $green-600;
&:focus {
- box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600;
+ box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset,
+ 0 0 4px 0 $green-600;
border: 0 none;
}
}
@@ -239,7 +183,8 @@ label {
border: 1px solid $red-500;
&:focus {
- box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error;
+ box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset,
+ 0 0 4px 0 $gl-field-focus-shadow-error;
border: 0 none;
}
}
@@ -265,16 +210,26 @@ label {
}
}
-.input-icon-wrapper {
+.input-icon-wrapper,
+.select-wrapper {
position: relative;
+}
- .input-icon-right {
- position: absolute;
- right: 0.8em;
- top: 50%;
- transform: translateY(-50%);
- color: $gray-600;
- }
+.select-wrapper > .fa-chevron-down {
+ position: absolute;
+ font-size: 10px;
+ right: 10px;
+ top: 12px;
+ color: $gray-darkest;
+ pointer-events: none;
+}
+
+.input-icon-wrapper > .input-icon-right {
+ position: absolute;
+ right: 0.8em;
+ top: 50%;
+ transform: translateY(-50%);
+ color: $gray-600;
}
.input-md {
@@ -290,3 +245,17 @@ label {
.input-group-text {
max-height: $input-height;
}
+
+.gl-form-checkbox {
+ align-items: baseline;
+
+ &.form-check-inline .form-check-input {
+ align-self: flex-start;
+ margin-right: $gl-padding-8;
+ height: 1.5 * $gl-font-size;
+ }
+
+ .help-text {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 18671f7c4d8..df40149f0a6 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -385,3 +385,8 @@
height: $size;
margin-right: $margin-right;
}
+
+@mixin code-icon-size() {
+ width: $gl-font-size * $code-line-height * 0.9;
+ height: $gl-font-size * $code-line-height * 0.9;
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 66cd113db84..77a36e59b03 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -154,8 +154,6 @@
}
.avatar-cell {
- width: 46px;
-
img {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b3a634e23a3..5e5d298f8f2 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -15,7 +15,6 @@
position: sticky;
top: $mr-file-header-top;
z-index: 102;
- height: $mr-version-controls-height;
&::before {
content: '';
@@ -615,10 +614,9 @@ table.code {
.diff-comment-avatar-holders {
position: absolute;
- height: 19px;
- width: 19px;
- margin-left: -15px;
+ margin-left: -$gl-padding;
z-index: 100;
+ @include code-icon-size();
&:hover {
.diff-comment-avatar,
@@ -652,26 +650,28 @@ table.code {
.diff-comments-more-count {
position: absolute;
left: 0;
- width: 19px;
- height: 19px;
margin-right: 0;
border-color: $white-light;
cursor: pointer;
transition: all 0.1s ease-out;
+ @include code-icon-size();
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
z-index: (4 - $i);
}
}
+
+ .avatar {
+ @include code-icon-size();
+ }
}
.diff-comments-more-count {
- width: 19px;
- min-width: 19px;
padding-left: 0;
padding-right: 0;
overflow: hidden;
+ @include code-icon-size();
}
.diff-comments-more-count,
@@ -680,12 +680,15 @@ table.code {
}
.diff-notes-collapse {
- width: 24px;
- height: 24px;
+ border: 0;
border-radius: 50%;
padding: 0;
transition: transform 0.1s ease-out;
z-index: 100;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include code-icon-size();
.collapse-icon {
height: 50%;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ab5a9e170f0..77b40fe2d30 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -185,46 +185,6 @@
}
}
}
-
- .accept-control {
- display: inline-block;
- float: left;
- margin: 0;
- margin-left: 20px;
- padding: 5px;
- padding-top: 8px;
- line-height: 20px;
-
- &.right {
- float: right;
- padding-right: 0;
- }
-
- .modify-merge-commit-link {
- padding: 0;
- background-color: transparent;
- border: 0;
- color: $gl-text-color;
-
- &:hover,
- &:focus {
- text-decoration: underline;
- }
- }
-
- .merge-param-checkbox {
- margin: 0;
- }
-
- a .fa-question-circle {
- color: $gl-text-color-secondary;
-
- &:hover,
- &:focus {
- color: $link-hover-color;
- }
- }
- }
}
.ci-widget {
@@ -407,12 +367,6 @@
width: 100%;
text-align: center;
}
-
- .accept-control {
- width: 100%;
- text-align: center;
- margin: 0;
- }
}
.commit-message-editor {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3343b55d24b..8c7b124dd33 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -334,7 +334,7 @@ table {
.toolbar-button-icon {
position: relative;
top: 1px;
- margin-right: 3px;
+ margin-right: $gl-padding-4;
color: inherit;
font-size: 16px;
}
@@ -461,6 +461,15 @@ table {
border: 0;
font-size: 14px;
line-height: 16px;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+
+ .text-attach-file {
+ text-decoration: underline;
+ }
+ }
}
.markdown-selector {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 170432a9e62..32477c20db6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -437,7 +437,9 @@ $note-form-margin-left: 72px;
.diff-file {
.is-over {
.add-diff-note {
- display: inline-block;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
}
}
@@ -741,7 +743,7 @@ $note-form-margin-left: 72px;
.add-diff-note {
@include btn-comment-icon;
opacity: 0;
- margin-left: -50px;
+ margin-left: -52px;
position: absolute;
top: 50%;
transform: translateY(-50%);
@@ -900,10 +902,6 @@ $note-form-margin-left: 72px;
.diff-comment-form {
display: block;
}
-
- .add-diff-note svg {
- margin-top: 4px;
- }
}
.discussion-filter-container {
diff --git a/app/controllers/concerns/import_url_params.rb b/app/controllers/concerns/import_url_params.rb
new file mode 100644
index 00000000000..e51e4157f50
--- /dev/null
+++ b/app/controllers/concerns/import_url_params.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module ImportUrlParams
+ def import_url_params
+ return {} unless params.dig(:project, :import_url).present?
+
+ { import_url: import_params_to_full_url(params[:project]) }
+ end
+
+ def import_params_to_full_url(params)
+ Gitlab::UrlSanitizer.new(
+ params[:import_url],
+ credentials: {
+ user: params[:import_url_user],
+ password: params[:import_url_password]
+ }
+ ).full_url
+ end
+end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index cfff154c3dd..8b8b7db72f8 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -26,16 +26,22 @@ module MilestoneActions
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
+ milestone_labels = @milestone.issue_labels_visible_by_user(current_user)
+
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: @milestone.labels.map { |label| label.present(issuable_subject: @milestone.parent) } # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ labels: milestone_labels.map do |label|
+ label.present(issuable_subject: @milestone.parent)
+ end
})
end
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
private
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 4640be015de..afbf9fd7720 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -2,6 +2,7 @@
class Projects::ImportsController < Projects::ApplicationController
include ContinueParams
+ include ImportUrlParams
# Authorize
before_action :authorize_admin_project!
@@ -67,10 +68,12 @@ class Projects::ImportsController < Projects::ApplicationController
end
def import_params_attributes
- [:import_url]
+ []
end
def import_params
- params.require(:project).permit(import_params_attributes)
+ params.require(:project)
+ .permit(import_params_attributes)
+ .merge(import_url_params)
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 8f177895b08..135117926be 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show
close_merge_request_if_no_source_project
- mark_merge_request_mergeable
+ @merge_request.check_mergeability
respond_to do |format|
format.html do
@@ -145,14 +145,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render partial: 'projects/merge_requests/widget/commit_change_content', layout: false
end
- def cancel_merge_when_pipeline_succeeds
- unless @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
+ def cancel_auto_merge
+ unless @merge_request.can_cancel_auto_merge?(current_user)
return access_denied!
end
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(@project, current_user)
- .cancel(@merge_request)
+ AutoMergeService.new(project, current_user).cancel(@merge_request)
render json: serialize_widget(@merge_request)
end
@@ -229,12 +227,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def merge_params_attributes
- [:should_remove_source_branch, :commit_message, :squash_commit_message, :squash]
+ [:should_remove_source_branch, :commit_message, :squash_commit_message, :squash, :auto_merge_strategy]
end
- def merge_when_pipeline_succeeds_active?
- params[:merge_when_pipeline_succeeds].present? &&
- @merge_request.head_pipeline && @merge_request.head_pipeline.active?
+ def auto_merge_requested?
+ # Support params[:merge_when_pipeline_succeeds] during the transition period
+ params[:auto_merge_strategy].present? || params[:merge_when_pipeline_succeeds].present?
end
def close_merge_request_if_no_source_project
@@ -253,14 +251,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end
- def mark_merge_request_mergeable
- @merge_request.check_if_can_be_merged
- end
-
def merge!
- # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
+ # Disable the CI check if auto_merge_strategy is specified since we have
# to wait until CI completes to know
- unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
+ unless @merge_request.mergeable?(skip_ci_check: auto_merge_requested?)
return :failed
end
@@ -274,24 +268,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
- if params[:merge_when_pipeline_succeeds].present?
- return :failed unless @merge_request.actual_head_pipeline
-
- if @merge_request.actual_head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(@project, current_user, merge_params)
- .execute(@merge_request)
-
- :merge_when_pipeline_succeeds
- elsif @merge_request.actual_head_pipeline.success?
- # This can be triggered when a user clicks the auto merge button while
- # the tests finish at about the same time
- @merge_request.merge_async(current_user.id, merge_params)
-
- :success
- else
- :failed
- end
+ if auto_merge_requested?
+ AutoMergeService.new(project, current_user, merge_params)
+ .execute(merge_request,
+ params[:auto_merge_strategy] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
@merge_request.merge_async(current_user.id, merge_params)
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 79030da64d3..4b0d001fca6 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -10,15 +10,13 @@ module Projects
format.json do
functions = finder.execute
- if functions.any?
- render json: serialize_function(functions)
- else
- head :no_content
- end
+ render json: {
+ knative_installed: finder.knative_installed,
+ functions: serialize_function(functions)
+ }.to_json
end
format.html do
- @installed = finder.installed?
render
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e88c46144ef..12db493978b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,6 +7,7 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown
include SendFileUpload
include RecordUserLastActivity
+ include ImportUrlParams
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
@@ -333,6 +334,7 @@ class ProjectsController < Projects::ApplicationController
def project_params(attributes: [])
params.require(:project)
.permit(project_params_attributes + attributes)
+ .merge(import_url_params)
end
def project_params_attributes
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 6fea61cf45d..a841859621e 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -18,6 +18,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :ldap_servers, only: [:new, :create]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
+ prepend_before_action :ensure_password_authentication_enabled!, if: :password_based_login?, only: [:create]
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -138,6 +139,14 @@ class SessionsController < Devise::SessionsController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def ensure_password_authentication_enabled!
+ render_403 unless Gitlab::CurrentSettings.password_authentication_enabled_for_web?
+ end
+
+ def password_based_login?
+ user_params[:login].present? || user_params[:password].present?
+ end
+
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response)
end
diff --git a/app/finders/clusters/knative_services_finder.rb b/app/finders/clusters/knative_services_finder.rb
new file mode 100644
index 00000000000..7d3b53ef663
--- /dev/null
+++ b/app/finders/clusters/knative_services_finder.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+module Clusters
+ class KnativeServicesFinder
+ include ReactiveCaching
+ include Gitlab::Utils::StrongMemoize
+
+ KNATIVE_STATES = {
+ 'checking' => 'checking',
+ 'installed' => 'installed',
+ 'not_found' => 'not_found'
+ }.freeze
+
+ self.reactive_cache_key = ->(finder) { finder.model_name }
+ self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
+
+ attr_reader :cluster, :project
+
+ def initialize(cluster, project)
+ @cluster = cluster
+ @project = project
+ end
+
+ def with_reactive_cache_memoized(*cache_args, &block)
+ strong_memoize(:reactive_cache) do
+ with_reactive_cache(*cache_args, &block)
+ end
+ end
+
+ def clear_cache!
+ clear_reactive_cache!(*cache_args)
+ end
+
+ def self.from_cache(cluster_id, project_id)
+ cluster = Clusters::Cluster.find(cluster_id)
+ project = ::Project.find(project_id)
+
+ new(cluster, project)
+ end
+
+ def calculate_reactive_cache(*)
+ # read_services calls knative_client.discover implicitily. If we stop
+ # detecting services but still want to detect knative, we'll need to
+ # explicitily call: knative_client.discover
+ #
+ # We didn't create it separately to avoid 2 cluster requests.
+ ksvc = read_services
+ pods = knative_client.discovered ? read_pods : []
+ { services: ksvc, pods: pods, knative_detected: knative_client.discovered }
+ end
+
+ def services
+ return [] unless search_namespace
+
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+ cached_data.to_h.fetch(:services, [])
+ end
+
+ def cache_args
+ [cluster.id, project.id]
+ end
+
+ def service_pod_details(service)
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+ cached_data.to_h.fetch(:pods, []).select do |pod|
+ filter_pods(pod, service)
+ end
+ end
+
+ def knative_detected
+ cached_data = with_reactive_cache_memoized(*cache_args) { |data| data }
+
+ knative_state = cached_data.to_h[:knative_detected]
+
+ return KNATIVE_STATES['checking'] if knative_state.nil?
+ return KNATIVE_STATES['installed'] if knative_state
+
+ KNATIVE_STATES['uninstalled']
+ end
+
+ def model_name
+ self.class.name.underscore.tr('/', '_')
+ end
+
+ private
+
+ def search_namespace
+ @search_namespace ||= cluster.kubernetes_namespace_for(project)
+ end
+
+ def knative_client
+ cluster.kubeclient.knative_client
+ end
+
+ def filter_pods(pod, service)
+ pod["metadata"]["labels"]["serving.knative.dev/service"] == service
+ end
+
+ def read_services
+ knative_client.get_services(namespace: search_namespace).as_json
+ rescue Kubeclient::ResourceNotFoundError
+ []
+ end
+
+ def read_pods
+ cluster.kubeclient.core_client.get_pods(namespace: search_namespace).as_json
+ end
+
+ def id
+ nil
+ end
+ end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index e5bffccabfe..ebe50806ca1 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -14,8 +14,16 @@ module Projects
knative_services.flatten.compact
end
- def installed?
- clusters_with_knative_installed.exists?
+ # Possible return values: Clusters::KnativeServicesFinder::KNATIVE_STATE
+ def knative_installed
+ states = @clusters.map do |cluster|
+ cluster.application_knative
+ cluster.knative_services_finder(project).knative_detected.tap do |state|
+ return state if state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['checking'] # rubocop:disable Cop/AvoidReturnFromBlocks
+ end
+ end
+
+ states.any? { |state| state == ::Clusters::KnativeServicesFinder::KNATIVE_STATES['installed'] }
end
def service(environment_scope, name)
@@ -25,7 +33,7 @@ module Projects
def invocation_metrics(environment_scope, name)
return unless prometheus_adapter&.can_query?
- cluster = clusters_with_knative_installed.preload_knative.find do |c|
+ cluster = @clusters.find do |c|
environment_scope == c.environment_scope
end
@@ -34,7 +42,7 @@ module Projects
end
def has_prometheus?(environment_scope)
- clusters_with_knative_installed.preload_knative.to_a.any? do |cluster|
+ @clusters.any? do |cluster|
environment_scope == cluster.environment_scope && cluster.application_prometheus_available?
end
end
@@ -42,10 +50,12 @@ module Projects
private
def knative_service(environment_scope, name)
- clusters_with_knative_installed.preload_knative.map do |cluster|
+ @clusters.map do |cluster|
next if environment_scope != cluster.environment_scope
- services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+ services = cluster
+ .knative_services_finder(project)
+ .services
.select { |svc| svc["metadata"]["name"] == name }
add_metadata(cluster, services).first unless services.nil?
@@ -53,8 +63,11 @@ module Projects
end
def knative_services
- clusters_with_knative_installed.preload_knative.map do |cluster|
- services = cluster.application_knative.services_for(ns: cluster.kubernetes_namespace_for(project))
+ @clusters.map do |cluster|
+ services = cluster
+ .knative_services_finder(project)
+ .services
+
add_metadata(cluster, services) unless services.nil?
end
end
@@ -65,17 +78,14 @@ module Projects
s["cluster_id"] = cluster.id
if services.length == 1
- s["podcount"] = cluster.application_knative.service_pod_details(
- cluster.kubernetes_namespace_for(project),
- s["metadata"]["name"]).length
+ s["podcount"] = cluster
+ .knative_services_finder(project)
+ .service_pod_details(s["metadata"]["name"])
+ .length
end
end
end
- def clusters_with_knative_installed
- @clusters.with_knative_installed
- end
-
# rubocop: disable CodeReuse/ServiceClass
def prometheus_adapter
@prometheus_adapter ||= ::Prometheus::AdapterService.new(project).prometheus_adapter
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 31850c2cadb..5b7eb57841c 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -10,7 +10,7 @@ module Resolvers
end
end
- def self.resolver_complexity(args)
+ def self.resolver_complexity(args, child_complexity:)
complexity = 1
complexity += 1 if args[:sort]
complexity += 5 if args[:search]
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index a166211fc18..a6f82cc8505 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -20,7 +20,7 @@ module ResolvesPipelines
end
class_methods do
- def resolver_complexity(args)
+ def resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:sha]
complexity += 2 if args[:ref]
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index f7e49166ca0..3ee3849f483 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -58,7 +58,7 @@ module Resolvers
IssuesFinder.new(context[:current_user], args).execute
end
- def self.resolver_complexity(args)
+ def self.resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:labelName]
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
new file mode 100644
index 00000000000..677ea808aeb
--- /dev/null
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class NamespaceProjectsResolver < BaseResolver
+ argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ default_value: false,
+ description: 'Include also subgroup projects'
+
+ type Types::ProjectType, null: true
+
+ alias_method :namespace, :object
+
+ def resolve(include_subgroups:)
+ # The namespace could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` or the `full_path` of the namespace
+ # to query for projects, so make sure it's loaded and not `nil` before continuing.
+ namespace.sync if namespace.respond_to?(:sync)
+ return Project.none if namespace.nil?
+
+ if include_subgroups
+ namespace.all_projects.with_route
+ else
+ namespace.projects.with_route
+ end
+ end
+
+ def self.resolver_complexity(args, child_complexity:)
+ complexity = super
+ complexity + 10
+ end
+ end
+end
diff --git a/app/graphql/resolvers/namespace_resolver.rb b/app/graphql/resolvers/namespace_resolver.rb
new file mode 100644
index 00000000000..17b3800d151
--- /dev/null
+++ b/app/graphql/resolvers/namespace_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class NamespaceResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::NamespaceType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Namespace, full_path)
+ end
+ end
+end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 15331129134..a374851e835 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -33,7 +33,7 @@ module Types
limit_value = [args[:first], args[:last], page_size].compact.min
# Resolvers may add extra complexity depending on used arguments
- complexity = child_complexity + self.resolver&.try(:resolver_complexity, args).to_i
+ complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
# Resolvers may add extra complexity depending on number of items being loaded.
multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index b21a226d07f..dd5133189dc 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -15,6 +15,10 @@ module Types
field :description, GraphQL::STRING_TYPE, null: true
field :state, IssueStateEnum, null: false
+ field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference do
+ argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false
+ end
+
field :author, Types::UserType,
null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
@@ -37,7 +41,9 @@ module Types
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :user_notes_count, GraphQL::INT_TYPE, null: false
+ field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path
field :web_url, GraphQL::STRING_TYPE, null: false
+ field :relative_position, GraphQL::INT_TYPE, null: true
field :closed_at, Types::TimeType, null: true
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 36d8ee8c878..f6d91320e50 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -15,5 +15,10 @@ module Types
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
+
+ field :projects,
+ Types::ProjectType.connection_type,
+ null: false,
+ resolver: ::Resolvers::NamespaceProjectsResolver
end
end
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
new file mode 100644
index 00000000000..62537361918
--- /dev/null
+++ b/app/graphql/types/project_statistics_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ class ProjectStatisticsType < BaseObject
+ graphql_name 'ProjectStatistics'
+
+ field :commit_count, GraphQL::INT_TYPE, null: false
+
+ field :storage_size, GraphQL::INT_TYPE, null: false
+ field :repository_size, GraphQL::INT_TYPE, null: false
+ field :lfs_objects_size, GraphQL::INT_TYPE, null: false
+ field :build_artifacts_size, GraphQL::INT_TYPE, null: false
+ field :packages_size, GraphQL::INT_TYPE, null: false
+ field :wiki_size, GraphQL::INT_TYPE, null: true
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 06a1aab09f6..2236ffa394d 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -69,6 +69,10 @@ module Types
field :namespace, Types::NamespaceType, null: false
field :group, Types::GroupType, null: true
+ field :statistics, Types::ProjectStatisticsType,
+ null: false,
+ resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
+
field :repository, Types::RepositoryType, null: false
field :merge_requests,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 40d7de1a49a..536bdb077ad 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -14,6 +14,11 @@ module Types
resolver: Resolvers::GroupResolver,
description: "Find a group"
+ field :namespace, Types::NamespaceType,
+ null: true,
+ resolver: Resolvers::NamespaceResolver,
+ description: "Find a namespace"
+
field :metadata, Types::MetadataType,
null: true,
resolver: Resolvers::MetadataResolver,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 971d1052824..4469118f065 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -160,6 +160,7 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:allow_local_requests_from_hooks_and_services,
+ :dns_rebinding_protection_enabled,
:archive_builds_in_human_readable,
:authorized_keys_enabled,
:auto_devops_enabled,
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index dc0e5511fcf..2beb081ab77 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -98,16 +98,17 @@ module EmailsHelper
case format
when :html
- " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}"
+ merge_request_link = link_to(merge_request.to_reference, merge_request.web_url)
+ _("via merge request %{link}").html_safe % { link: merge_request_link }
else
# If it's not HTML nor text then assume it's text to be safe
- " via merge request #{merge_request.to_reference} (#{merge_request.web_url})"
+ _("via merge request %{link}") % { link: "#{merge_request.to_reference} (#{merge_request.web_url})" }
end
when String
# Technically speaking this should be Commit but per
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
# we can't deserialize Commit without custom serializer for ActiveJob
- " via #{closed_via}"
+ _("via %{closed_via}") % { closed_via: closed_via }
else
""
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 991ca42c445..2de4e92e33e 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -103,7 +103,7 @@ module MergeRequestsHelper
def merge_params(merge_request)
{
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
should_remove_source_branch: true,
sha: merge_request.diff_head_sha,
squash: merge_request.squash
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index a7ce7667916..11b9cf22142 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -100,4 +100,8 @@ module NotificationsHelper
css_class: "icon notifications-icon js-notifications-icon"
)
end
+
+ def show_unsubscribe_title?(noteable)
+ can?(current_user, "read_#{noteable.to_ability_name}".to_sym, noteable)
+ end
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e51619b0f9c..904d650ef96 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -21,6 +21,7 @@ module ApplicationSettingImplementation
after_sign_up_text: nil,
akismet_enabled: false,
allow_local_requests_from_hooks_and_services: false,
+ dns_rebinding_protection_enabled: true,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f743fca423a..aaa326afea5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +15,6 @@ module Ci
include Gitlab::Utils::StrongMemoize
include Deployable
include HasRef
- include UpdateProjectStatistics
BuildArchivedError = Class.new(StandardError)
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 0dbeab30498..f80e98e5bca 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -59,7 +59,7 @@ module Ci
validate :valid_file_format?, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
- update_project_statistics stat: :build_artifacts_size
+ update_project_statistics project_statistics_name: :build_artifacts_size
after_save :update_file_store, if: :saved_change_to_file?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 80401ca0a1e..3727a9861aa 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -166,6 +166,16 @@ module Ci
end
end
+ after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
+ pipeline.run_after_commit do
+ pipeline.all_merge_requests.each do |merge_request|
+ next unless merge_request.auto_merge_enabled?
+
+ AutoMergeProcessWorker.perform_async(merge_request.id)
+ end
+ end
+ end
+
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index c0a0ca9acf6..c40ad39be61 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -27,9 +27,13 @@ module Ci
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
+ scope :runnable_schedules, -> { active.where("next_run_at < ?", Time.now) }
+ scope :preloaded, -> { preload(:owner, :project) }
accepts_nested_attributes_for :variables, allow_destroy: true
+ alias_attribute :real_next_run, :next_run_at
+
def owned_by?(current_user)
owner == current_user
end
@@ -46,8 +50,14 @@ module Ci
update_attribute(:active, false)
end
+ ##
+ # The `next_run_at` column is set to the actual execution date of `PipelineScheduleWorker`.
+ # This way, a schedule like `*/1 * * * *` won't be triggered in a short interval
+ # when PipelineScheduleWorker runs irregularly by Sidekiq Memory Killer.
def set_next_run_at
- self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
+ self.next_run_at = Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'],
+ Time.zone.name)
+ .next_time_from(ideal_next_run_at)
end
def schedule_next_run!
@@ -56,15 +66,14 @@ module Ci
update_attribute(:next_run_at, nil) # update without validation
end
- def real_next_run(
- worker_cron: Settings.cron_jobs['pipeline_schedule_worker']['cron'],
- worker_time_zone: Time.zone.name)
- Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
- .next_time_from(next_run_at)
- end
-
def job_variables
variables&.map(&:to_runner_variable) || []
end
+
+ private
+
+ def ideal_next_run_at
+ Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
+ end
end
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 9fbf5d8af04..d5a3bd62e3d 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -15,9 +15,6 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
- include ReactiveCaching
-
- self.reactive_cache_key = ->(knative) { [knative.class.model_name.singular, knative.id] }
def set_initial_status
return unless not_installable?
@@ -41,8 +38,6 @@ module Clusters
scope :for_cluster, -> (cluster) { where(cluster: cluster) }
- after_save :clear_reactive_cache!
-
def chart
'knative/knative'
end
@@ -77,55 +72,12 @@ module Clusters
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
- def client
- cluster.kubeclient.knative_client
- end
-
- def services
- with_reactive_cache do |data|
- data[:services]
- end
- end
-
- def calculate_reactive_cache
- { services: read_services, pods: read_pods }
- end
-
def ingress_service
cluster.kubeclient.get_service('istio-ingressgateway', 'istio-system')
end
- def services_for(ns: namespace)
- return [] unless services
- return [] unless ns
-
- services.select do |service|
- service.dig('metadata', 'namespace') == ns
- end
- end
-
- def service_pod_details(ns, service)
- with_reactive_cache do |data|
- data[:pods].select { |pod| filter_pods(pod, ns, service) }
- end
- end
-
private
- def read_pods
- cluster.kubeclient.core_client.get_pods.as_json
- end
-
- def filter_pods(pod, namespace, service)
- pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service
- end
-
- def read_services
- client.get_services.as_json
- rescue Kubeclient::ResourceNotFoundError
- []
- end
-
def install_knative_metrics
["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 0dff91c3fe2..db7fd8524c2 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.5.1'.freeze
+ VERSION = '0.5.2'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 57a1e461b2d..e1d6b2a802b 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -223,6 +223,10 @@ module Clusters
end
end
+ def knative_services_finder(project)
+ @knative_services_finder ||= KnativeServicesFinder.new(self, project)
+ end
+
private
def instance_domain
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index bfd0c36942b..4b428b0af83 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -3,14 +3,16 @@
module Noteable
extend ActiveSupport::Concern
- # `Noteable` class names that support resolvable notes.
- RESOLVABLE_TYPES = %w(MergeRequest).freeze
-
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
%w(Issue MergeRequest)
end
+
+ # `Noteable` class names that support resolvable notes.
+ def resolvable_types
+ %w(MergeRequest)
+ end
end
# The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
@@ -36,7 +38,7 @@ module Noteable
end
def supports_resolvable_notes?
- RESOLVABLE_TYPES.include?(base_class_name)
+ self.class.resolvable_types.include?(base_class_name)
end
def supports_discussions?
@@ -131,3 +133,5 @@ module Noteable
)
end
end
+
+Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 58143a32fdc..4a506146de3 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -73,6 +73,7 @@ module Referable
(?<url>
#{Regexp.escape(Gitlab.config.gitlab.url)}
\/#{Project.reference_pattern}
+ (?:\/\-)?
\/#{Regexp.escape(route)}
\/#{pattern}
(?<path>
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 16ea330701d..2d2d5fb7168 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -12,7 +12,7 @@ module ResolvableNote
validates :resolved_by, presence: true, if: :resolved?
# Keep this scope in sync with `#potentially_resolvable?`
- scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable::RESOLVABLE_TYPES) }
+ scope :potentially_resolvable, -> { where(type: RESOLVABLE_TYPES).where(noteable_type: Noteable.resolvable_types) }
# Keep this scope in sync with `#resolvable?`
scope :resolvable, -> { potentially_resolvable.user }
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index 67e1f0ec930..1f881249322 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -5,43 +5,47 @@
# It deals with `ProjectStatistics.increment_statistic` making sure not to update statistics on a cascade delete from the
# project, and keeping track of value deltas on each save. It updates the DB only when a change is needed.
#
-# How to use
-# - Invoke `update_project_statistics stat: :a_project_statistics_column, attribute: :an_attr_to_track` in a model class body.
+# Example:
#
-# Expectation
-# - `attribute` must be an ActiveRecord attribute
+# module Ci
+# class JobArtifact < ApplicationRecord
+# include UpdateProjectStatistics
+#
+# update_project_statistics project_statistics_name: :build_artifacts_size
+# end
+# end
+#
+# Expectation:
+#
+# - `statistic_attribute` must be an ActiveRecord attribute
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
+#
module UpdateProjectStatistics
extend ActiveSupport::Concern
class_methods do
- attr_reader :statistic_name, :statistic_attribute
+ attr_reader :project_statistics_name, :statistic_attribute
- # Configure the model to update +stat+ on ProjectStatistics when +attribute+ changes
+ # Configure the model to update `project_statistics_name` on ProjectStatistics,
+ # when `statistic_attribute` changes
+ #
+ # - project_statistics_name: A column of `ProjectStatistics` to update
+ # - statistic_attribute: An attribute of the current model, default to `size`
#
- # +stat+:: a column of ProjectStatistics to update
- # +attribute+:: an attribute of the current model, default to +:size+
- def update_project_statistics(stat:, attribute: :size)
- @statistic_name = stat
- @statistic_attribute = attribute
+ def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
+ @project_statistics_name = project_statistics_name
+ @statistic_attribute = statistic_attribute
after_save(:update_project_statistics_after_save, if: :update_project_statistics_attribute_changed?)
after_destroy(:update_project_statistics_after_destroy, unless: :project_destroyed?)
end
+
private :update_project_statistics
end
included do
private
- def project_destroyed?
- project.pending_delete?
- end
-
- def update_project_statistics_attribute_changed?
- saved_change_to_attribute?(self.class.statistic_attribute)
- end
-
def update_project_statistics_after_save
attr = self.class.statistic_attribute
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
@@ -49,12 +53,20 @@ module UpdateProjectStatistics
update_project_statistics(delta)
end
+ def update_project_statistics_attribute_changed?
+ saved_change_to_attribute?(self.class.statistic_attribute)
+ end
+
def update_project_statistics_after_destroy
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
end
+ def project_destroyed?
+ project.pending_delete?
+ end
+
def update_project_statistics(delta)
- ProjectStatistics.increment_statistic(project_id, self.class.statistic_name, delta)
+ ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
end
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index feabea9b8ba..1a87fc47c56 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -15,7 +15,9 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
validates :line_code, presence: true, line_code: true, if: :on_text?
- validates :noteable_type, inclusion: { in: noteable_types }
+ # We need to evaluate the `noteable` types when running the validation since
+ # EE might have added a type when the module was prepended
+ validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } }
validate :positions_complete
validate :verify_supported
validate :diff_refs_match_commit, if: :for_commit?
@@ -44,7 +46,7 @@ class DiffNote < Note
# Returns the diff file from `position`
def latest_diff_file
strong_memoize(:latest_diff_file) do
- position.diff_file(project.repository)
+ position.diff_file(repository)
end
end
@@ -111,7 +113,7 @@ class DiffNote < Note
if note_diff_file
diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
Gitlab::Diff::File.new(diff,
- repository: project.repository,
+ repository: repository,
diff_refs: original_position.diff_refs)
elsif created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
@@ -122,7 +124,7 @@ class DiffNote < Note
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(original_position.diff_options).diff_files.first
else
- original_position.diff_file(self.project.repository)
+ original_position.diff_file(repository)
end
# Since persisted diff files already have its content "unfolded"
@@ -137,7 +139,7 @@ class DiffNote < Note
end
def set_line_code
- self.line_code = self.position.line_code(self.project.repository)
+ self.line_code = self.position.line_code(repository)
end
def verify_supported
@@ -171,6 +173,10 @@ class DiffNote < Note
shas << self.position.head_sha
end
- project.repository.keep_around(*shas)
+ repository.keep_around(*shas)
+ end
+
+ def repository
+ noteable.respond_to?(:repository) ? noteable.repository : project.repository
end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index b097be8cc89..8aa25924c28 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -59,6 +59,11 @@ class Key < ApplicationRecord
"key-#{id}"
end
+ # EE overrides this
+ def can_delete?
+ true
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def update_last_used_at
Keys::LastUsedService.new(self).execute
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 311ba1ce6bd..59416fb4b51 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -165,7 +165,7 @@ class MergeRequest < ApplicationRecord
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
- validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
+ validates :merge_user, presence: true, if: :auto_merge_enabled?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
validate :validate_target_project, on: :create
@@ -196,6 +196,7 @@ class MergeRequest < ApplicationRecord
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
+ alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
def self.reference_prefix
'!'
@@ -391,7 +392,7 @@ class MergeRequest < ApplicationRecord
def merge_participants
participants = [author]
- if merge_when_pipeline_succeeds? && !participants.include?(merge_user)
+ if auto_merge_enabled? && !participants.include?(merge_user)
participants << merge_user
end
@@ -588,6 +589,8 @@ class MergeRequest < ApplicationRecord
return
end
+ [:source_branch, :target_branch].each { |attr| validate_branch_name(attr) }
+
if opened?
similar_mrs = target_project
.merge_requests
@@ -608,6 +611,16 @@ class MergeRequest < ApplicationRecord
end
end
+ def validate_branch_name(attr)
+ return unless changes_include?(attr)
+
+ branch = read_attribute(attr)
+
+ return unless branch
+
+ errors.add(attr) unless Gitlab::GitRefValidator.validate_merge_request_branch(branch)
+ end
+
def validate_target_project
return true if target_project.merge_requests_enabled?
@@ -712,19 +725,16 @@ class MergeRequest < ApplicationRecord
MergeRequests::ReloadDiffsService.new(self, current_user).execute
end
- # rubocop: enable CodeReuse/ServiceClass
-
- def check_if_can_be_merged
- return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
- can_be_merged =
- !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
+ def check_mergeability
+ MergeRequests::MergeabilityCheckService.new(self).execute
+ end
+ # rubocop: enable CodeReuse/ServiceClass
- if can_be_merged
- mark_as_mergeable
- else
- mark_as_unmergeable
- end
+ # Returns boolean indicating the merge_status should be rechecked in order to
+ # switch to either can_be_merged or cannot_be_merged.
+ def recheck_merge_status?
+ self.class.state_machines[:merge_status].check_state?(merge_status)
end
def merge_event
@@ -750,7 +760,7 @@ class MergeRequest < ApplicationRecord
def mergeable?(skip_ci_check: false)
return false unless mergeable_state?(skip_ci_check: skip_ci_check)
- check_if_can_be_merged
+ check_mergeability
can_be_merged? && !should_be_rebased?
end
@@ -765,15 +775,6 @@ class MergeRequest < ApplicationRecord
true
end
- def mergeable_to_ref?
- return false unless mergeable_state?(skip_ci_check: true, skip_discussions_check: true)
-
- # Given the `merge_ref_path` will have the same
- # state the `target_branch` would have. Ideally
- # we need to check if it can be merged to it.
- project.repository.can_be_merged?(diff_head_sha, target_branch)
- end
-
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
end
@@ -782,7 +783,7 @@ class MergeRequest < ApplicationRecord
project.ff_merge_must_be_possible? && !ff_merge_possible?
end
- def can_cancel_merge_when_pipeline_succeeds?(current_user)
+ def can_cancel_auto_merge?(current_user)
can_be_merged_by?(current_user) || self.author == current_user
end
@@ -801,6 +802,16 @@ class MergeRequest < ApplicationRecord
Gitlab::Utils.to_boolean(merge_params['force_remove_source_branch'])
end
+ def auto_merge_strategy
+ return unless auto_merge_enabled?
+
+ merge_params['auto_merge_strategy'] || AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
+ end
+
+ def auto_merge_strategy=(strategy)
+ merge_params['auto_merge_strategy'] = strategy
+ end
+
def remove_source_branch?
should_remove_source_branch? || force_remove_source_branch?
end
@@ -973,15 +984,16 @@ class MergeRequest < ApplicationRecord
end
end
- def reset_merge_when_pipeline_succeeds
- return unless merge_when_pipeline_succeeds?
+ def reset_auto_merge
+ return unless auto_merge_enabled?
- self.merge_when_pipeline_succeeds = false
+ self.auto_merge_enabled = false
self.merge_user = nil
if merge_params
merge_params.delete('should_remove_source_branch')
merge_params.delete('commit_message')
merge_params.delete('squash_commit_message')
+ merge_params.delete('auto_merge_strategy')
end
self.save
@@ -1090,6 +1102,12 @@ class MergeRequest < ApplicationRecord
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end
+ # Returns the current merge-ref HEAD commit.
+ #
+ def merge_ref_head
+ project.repository.commit(merge_ref_path)
+ end
+
def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 20895923d3b..78d54571d94 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -407,6 +407,7 @@ class Project < ApplicationRecord
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
+ scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
@@ -597,6 +598,17 @@ class Project < ApplicationRecord
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
+
+ # Returns ids of projects with milestones available for given user
+ #
+ # Used on queries to find milestones which user can see
+ # For example: Milestone.where(project_id: ids_with_milestone_available_for(user))
+ def ids_with_milestone_available_for(user)
+ with_issues_enabled = with_issues_available_for_user(user).select(:id)
+ with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)
+
+ from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
+ end
end
def all_pipelines
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 90bcb3067f6..67c12363a3c 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class ProjectAutoDevops < ApplicationRecord
+ include IgnorableColumn
+
+ ignore_column :domain
+
belongs_to :project
enum deploy_strategy: {
@@ -12,8 +16,6 @@ class ProjectAutoDevops < ApplicationRecord
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
- validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
-
after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token?
def predefined_variables
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index dd0654aec0b..11e3737298c 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -16,6 +16,8 @@ class ProjectStatistics < ApplicationRecord
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
+ scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
+
def total_repository_size
repository_size + lfs_objects_size
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d43f991bb3e..e05d3dd58ac 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1077,7 +1077,7 @@ class Repository
end
def rebase(user, merge_request)
- if Feature.disabled?(:two_step_rebase, default_enabled: false)
+ if Feature.disabled?(:two_step_rebase, default_enabled: true)
return rebase_deprecated(user, merge_request)
end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index c12a202efbc..c9dc0dbf443 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -4,6 +4,16 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents :issue
def web_url
- Gitlab::UrlBuilder.build(issue)
+ url_builder.url
+ end
+
+ def issue_path
+ url_builder.issue_path(issue)
+ end
+
+ private
+
+ def url_builder
+ @url_builder ||= Gitlab::UrlBuilder.new(issue)
end
end
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 9e9b6973b8e..2561c3f0244 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -32,6 +32,11 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
request? && can_update?
end
+ # This functionality is only available in EE.
+ def can_override?
+ false
+ end
+
private
def admin_member_permission
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index ba0711ca867..9c44ed711a6 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -22,9 +22,9 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
- def cancel_merge_when_pipeline_succeeds_path
- if can_cancel_merge_when_pipeline_succeeds?(current_user)
- cancel_merge_when_pipeline_succeeds_project_merge_request_path(project, merge_request)
+ def cancel_auto_merge_path
+ if can_cancel_auto_merge?(current_user)
+ cancel_auto_merge_project_merge_request_path(project, merge_request)
end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index b130f447cce..a428930dbbf 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -9,7 +9,11 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_params
expose :merge_status
expose :merge_user_id
- expose :merge_when_pipeline_succeeds
+ expose :auto_merge_enabled
+ expose :auto_merge_strategy
+ expose :available_auto_merge_strategies do |merge_request|
+ AutoMergeService.new(merge_request.project, current_user).available_strategies(merge_request) # rubocop: disable CodeReuse/ServiceClass
+ end
expose :source_branch
expose :source_branch_protected do |merge_request|
merge_request.source_project.present? && ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
@@ -182,8 +186,8 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).remove_wip_path
end
- expose :cancel_merge_when_pipeline_succeeds_path do |merge_request|
- presenter(merge_request).cancel_merge_when_pipeline_succeeds_path
+ expose :cancel_auto_merge_path do |merge_request|
+ presenter(merge_request).cancel_auto_merge_path
end
expose :create_issue_to_resolve_discussions_path do |merge_request|
diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
new file mode 100644
index 00000000000..d0586468859
--- /dev/null
+++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module AutoMerge
+ class MergeWhenPipelineSucceedsService < BaseService
+ def execute(merge_request)
+ return :failed unless merge_request.actual_head_pipeline
+
+ if merge_request.actual_head_pipeline.active?
+ merge_request.merge_params.merge!(params)
+
+ unless merge_request.auto_merge_enabled?
+ merge_request.auto_merge_enabled = true
+ merge_request.merge_user = @current_user
+ merge_request.auto_merge_strategy = AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
+
+ SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
+ end
+
+ return :failed unless merge_request.save
+
+ :merge_when_pipeline_succeeds
+ elsif merge_request.actual_head_pipeline.success?
+ # This can be triggered when a user clicks the auto merge button while
+ # the tests finish at about the same time
+ merge_request.merge_async(current_user.id, merge_params)
+
+ :success
+ else
+ :failed
+ end
+ end
+
+ def process(merge_request)
+ return unless merge_request.actual_head_pipeline&.success?
+ return unless merge_request.mergeable?
+
+ merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
+ end
+
+ def cancel(merge_request)
+ if merge_request.reset_auto_merge
+ SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
+
+ success
+ else
+ error("Can't cancel the automatic merge", 406)
+ end
+ end
+
+ def available_for?(merge_request)
+ merge_request.actual_head_pipeline&.active?
+ end
+ end
+end
diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb
new file mode 100644
index 00000000000..a3a780ff388
--- /dev/null
+++ b/app/services/auto_merge_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+class AutoMergeService < BaseService
+ STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds'.freeze
+ STRATEGIES = [STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze
+
+ class << self
+ def all_strategies
+ STRATEGIES
+ end
+
+ def get_service_class(strategy)
+ return unless all_strategies.include?(strategy)
+
+ "::AutoMerge::#{strategy.camelize}Service".constantize
+ end
+ end
+
+ def execute(merge_request, strategy)
+ service = get_service_instance(strategy)
+
+ return :failed unless service&.available_for?(merge_request)
+
+ service.execute(merge_request)
+ end
+
+ def process(merge_request)
+ return unless merge_request.auto_merge_enabled?
+
+ get_service_instance(merge_request.auto_merge_strategy).process(merge_request)
+ end
+
+ def cancel(merge_request)
+ return error("Can't cancel the automatic merge", 406) unless merge_request.auto_merge_enabled?
+
+ get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request)
+ end
+
+ def available_strategies(merge_request)
+ self.class.all_strategies.select do |strategy|
+ get_service_instance(strategy).available_for?(merge_request)
+ end
+ end
+
+ private
+
+ def get_service_instance(strategy)
+ self.class.get_service_class(strategy)&.new(project, current_user, params)
+ end
+end
diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb
new file mode 100644
index 00000000000..387d0351490
--- /dev/null
+++ b/app/services/ci/pipeline_schedule_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineScheduleService < BaseService
+ def execute(schedule)
+ # Ensure `next_run_at` is set properly before creating a pipeline.
+ # Otherwise, multiple pipelines could be created in a short interval.
+ schedule.schedule_next_run!
+
+ RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner.id)
+ end
+ end
+end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 2a19e57a94f..805721212ba 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -29,7 +29,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note
- closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit)
+ closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit)
notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications
todo_service.close_issue(issue, current_user)
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index e77051bb1c9..b0f6166ea1c 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -18,6 +18,7 @@ module MergeRequests
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
cleanup_environments(merge_request)
+ cancel_auto_merge(merge_request)
end
merge_request
@@ -33,5 +34,9 @@ module MergeRequests
merge_request_metrics_service(merge_request).close(close_event)
end
end
+
+ def cancel_auto_merge(merge_request)
+ AutoMergeService.new(project, current_user).cancel(merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index 87147d90c32..8670b9ccf3d 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -20,20 +20,14 @@ module MergeRequests
raise_error('Conflicts detected during merge') unless commit_id
- commit = project.commit(commit_id)
- target_id, source_id = commit.parent_ids
-
- success(commit_id: commit.id,
- target_id: target_id,
- source_id: source_id)
- rescue MergeError => error
+ success(commit_id: commit_id)
+ rescue MergeError, ArgumentError => error
error(error.message)
end
private
def validate!
- authorization_check!
error_check!
end
@@ -43,21 +37,13 @@ module MergeRequests
error =
if !hooks_validation_pass?(merge_request)
hooks_validation_error(merge_request)
- elsif !@merge_request.mergeable_to_ref?
- "Merge request is not mergeable to #{target_ref}"
- elsif !source
+ elsif source.blank?
'No source for merge'
end
raise_error(error) if error
end
- def authorization_check!
- unless Ability.allowed?(current_user, :admin_merge_request, project)
- raise_error("You are not allowed to merge to this ref")
- end
- end
-
def target_ref
merge_request.merge_ref_path
end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
deleted file mode 100644
index 973e5b64e88..00000000000
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- class MergeWhenPipelineSucceedsService < MergeRequests::BaseService
- # Marks the passed `merge_request` to be merged when the pipeline succeeds or
- # updates the params for the automatic merge
- def execute(merge_request)
- merge_request.merge_params.merge!(params)
-
- # The service is also called when the merge params are updated.
- already_approved = merge_request.merge_when_pipeline_succeeds?
-
- unless already_approved
- merge_request.merge_when_pipeline_succeeds = true
- merge_request.merge_user = @current_user
-
- SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit)
- end
-
- merge_request.save
- end
-
- # Triggers the automatic merge of merge_request once the pipeline succeeds
- def trigger(pipeline)
- return unless pipeline.success?
-
- pipeline_merge_requests(pipeline) do |merge_request|
- next unless merge_request.merge_when_pipeline_succeeds?
- next unless merge_request.mergeable?
-
- merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
- end
- end
-
- # Cancels the automatic merge
- def cancel(merge_request)
- if merge_request.merge_when_pipeline_succeeds? && merge_request.open?
- merge_request.reset_merge_when_pipeline_succeeds
- SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user)
-
- success
- else
- error("Can't cancel the automatic merge", 406)
- end
- end
- end
-end
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
new file mode 100644
index 00000000000..ef833774e65
--- /dev/null
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class MergeabilityCheckService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ delegate :project, to: :@merge_request
+ delegate :repository, to: :project
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ # Updates the MR merge_status. Whenever it switches to a can_be_merged state,
+ # the merge-ref is refreshed.
+ #
+ # Returns a ServiceResponse indicating merge_status is/became can_be_merged
+ # and the merge-ref is synced. Success in case of being/becoming mergeable,
+ # error otherwise.
+ def execute
+ return ServiceResponse.error(message: 'Invalid argument') unless merge_request
+ return ServiceResponse.error(message: 'Unsupported operation') if Gitlab::Database.read_only?
+
+ update_merge_status
+
+ unless merge_request.can_be_merged?
+ return ServiceResponse.error(message: 'Merge request is not mergeable')
+ end
+
+ unless payload.fetch(:merge_ref_head)
+ return ServiceResponse.error(message: 'Merge ref was not found')
+ end
+
+ ServiceResponse.success(payload: payload)
+ end
+
+ private
+
+ attr_reader :merge_request
+
+ def payload
+ strong_memoize(:payload) do
+ {
+ merge_ref_head: merge_ref_head_payload
+ }
+ end
+ end
+
+ def merge_ref_head_payload
+ commit = merge_request.merge_ref_head
+
+ return unless commit
+
+ target_id, source_id = commit.parent_ids
+
+ {
+ commit_id: commit.id,
+ source_id: source_id,
+ target_id: target_id
+ }
+ end
+
+ def update_merge_status
+ return unless merge_request.recheck_merge_status?
+
+ if can_git_merge?
+ merge_to_ref && merge_request.mark_as_mergeable
+ else
+ merge_request.mark_as_unmergeable
+ end
+ end
+
+ def can_git_merge?
+ !merge_request.broken? && repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
+ end
+
+ def merge_to_ref
+ result = MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
+ result[:status] == :success
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 3abea1ad1ae..08130a531ee 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -24,7 +24,7 @@ module MergeRequests
reload_merge_requests
outdate_suggestions
refresh_pipelines_on_merge_requests
- reset_merge_when_pipeline_succeeds
+ cancel_auto_merge
mark_pending_todos_done
cache_merge_requests_closing_issues
@@ -142,8 +142,10 @@ module MergeRequests
end
end
- def reset_merge_when_pipeline_succeeds
- merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds)
+ def cancel_auto_merge
+ merge_requests_for_source_branch.each do |merge_request|
+ AutoMergeService.new(project, current_user).cancel(merge_request)
+ end
end
def mark_pending_todos_done
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 55546432ce4..6a0f3000ffb 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -89,7 +89,7 @@ module MergeRequests
merge_request.update(merge_error: nil)
if merge_request.head_pipeline && merge_request.head_pipeline.active?
- MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
+ AutoMergeService.new(project, current_user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
merge_request.merge_async(current_user.id, {})
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f797c0f11c6..5aa804666f0 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -238,7 +238,7 @@ class NotificationService
merge_request,
current_user,
:merged_merge_request_email,
- skip_current_user: !merge_request.merge_when_pipeline_succeeds?
+ skip_current_user: !merge_request.auto_merge_enabled?
)
end
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 1de30e68d87..f3437ba16de 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -1,19 +1,20 @@
# frozen_string_literal: true
class ServiceResponse
- def self.success(message: nil)
- new(status: :success, message: message)
+ def self.success(message: nil, payload: {})
+ new(status: :success, message: message, payload: payload)
end
- def self.error(message:, http_status: nil)
- new(status: :error, message: message, http_status: http_status)
+ def self.error(message:, payload: {}, http_status: nil)
+ new(status: :error, message: message, payload: payload, http_status: http_status)
end
- attr_reader :status, :message, :http_status
+ attr_reader :status, :message, :http_status, :payload
- def initialize(status:, message: nil, http_status: nil)
+ def initialize(status:, message: nil, payload: {}, http_status: nil)
self.status = status
self.message = message
+ self.payload = payload
self.http_status = http_status
end
@@ -27,5 +28,5 @@ class ServiceResponse
private
- attr_writer :status, :message, :http_status
+ attr_writer :status, :message, :http_status, :payload
end
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index f4bfb5af385..dd56bb99a06 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -8,4 +8,12 @@
= f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
Allow requests to the local network from hooks and services
+ .form-group
+ .form-check
+ = f.check_box :dns_rebinding_protection_enabled, class: 'form-check-input'
+ = f.label :dns_rebinding_protection_enabled, class: 'form-check-label' do
+ = _('Enforce DNS rebinding attack protection')
+ %span.form-text.text-muted
+ = _('Resolves IP addresses once and uses them to submit requests')
+
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index a6023a1cbb9..496ec3c78b0 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -16,7 +16,7 @@
mr_path: merge_requests_dashboard_path },
aria: { label: _('Search or jump to…') }
%button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- .dropdown-menu.dropdown-select
+ .dropdown-menu.dropdown-select.js-dashboard-search-options
= dropdown_content do
%ul
%li.dropdown-menu-empty-item
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index f21cf1ad34b..d3733ab3a09 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
+ = _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) }
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index 5567adc9165..ff2548a4b42 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
+= _("Issue was closed by %{name} %{reason}").html_safe % { name: sanitize_name(@updated_by.name), reason: closure_reason_text(@closed_via, format: formats.first) }
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 47494fc3f06..b9d73d89334 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -18,6 +18,7 @@
.float-right
%span.key-created-at
= s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)}
- = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do
- %span.sr-only= _('Remove')
- = icon('trash')
+ - if key.can_delete?
+ = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do
+ %span.sr-only= _('Remove')
+ = icon('trash')
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index dcdb7fc63b1..0ef01dec493 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -24,4 +24,5 @@
= @key.key
.col-md-12
.float-right
- = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
+ - if @key.can_delete?
+ = link_to _('Remove'), path_to_key(@key, is_admin), data: {confirm: _('Are you sure?')}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 771e1881e94..87b9920e8b4 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -15,7 +15,7 @@
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
.avatar-cell.d-none.d-sm-block
- = author_avatar(commit, size: 36, has_tooltip: false)
+ = author_avatar(commit, size: 40, has_tooltip: false)
.commit-detail.flex-list
.commit-content.qa-commit-content
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 9409418bbcc..82c1d57c97e 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -18,22 +18,22 @@
.help-form
.form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :display_name, 'Display name', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#display_name', class: 'input-group-text')
.form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :description, 'Description', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#description', class: 'input-group-text')
.form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
+ = label_tag nil, 'Command trigger word', class: 'col-12 col-form-label label-bold'
+ .col-12
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -42,44 +42,44 @@
%code= @project.full_path
.form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :request_url, 'Request URL', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#request_url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block POST
+ = label_tag nil, 'Request method', class: 'col-12 col-form-label label-bold'
+ .col-12 POST
.form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :response_username, 'Response username', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#response_username', class: 'input-group-text')
.form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :response_icon, 'Response icon', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#response_icon', class: 'input-group-text')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block Yes
+ = label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold'
+ .col-12 Yes
.form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-12 col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 9a7004f89c0..9b7732abc62 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -27,8 +27,8 @@
.help-form
.form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
+ = label_tag nil, 'Command', class: 'col-12 col-form-label label-bold'
+ .col-12
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -37,50 +37,50 @@
%code= @project.full_path
.form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :url, 'URL', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block POST
+ = label_tag nil, 'Method', class: 'col-12 col-form-label label-bold'
+ .col-12 POST
.form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :customize_name, 'Customize name', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
- = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = label_tag nil, 'Customize icon', class: 'col-12 col-form-label label-bold'
+ .col-12
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36, class: 'mr-3')
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block Show this command in the autocomplete list
+ = label_tag nil, 'Autocomplete', class: 'col-12 col-form-label label-bold'
+ .col-12 Show this command in the autocomplete list
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-12 col-form-label label-bold'
+ .col-12.input-group
= text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#descriptive_label', class: 'input-group-text')
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
index f049c35b38d..a124283921d 100644
--- a/app/views/projects/settings/operations/_external_dashboard.html.haml
+++ b/app/views/projects/settings/operations/_external_dashboard.html.haml
@@ -1,2 +1,3 @@
-.js-operation-settings{ data: { external_dashboard: { path: metrics_external_dashboard_url,
+.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
+ external_dashboard: { url: metrics_external_dashboard_url,
help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } }
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 8ae2807729b..cb8a8a24be8 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,5 +1,6 @@
- if @search_objects.to_a.empty?
= render partial: "search/results/empty"
+ = render_if_exists 'shared/promotions/promote_advanced_search'
- else
.row-content-block
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
@@ -11,7 +12,7 @@
- elsif @group
- link_to_group = link_to(@group.name, @group)
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
-
+ = render_if_exists 'shared/promotions/promote_advanced_search'
.results.prepend-top-10
- if @scope == 'commits'
%ul.content-list.commit-list
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index ca392e1adfc..22fcfcda297 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,6 +1,6 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
-- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
+- noteable_text = show_unsubscribe_title?(noteable) ? %(#{noteable.title} (#{noteable.to_reference})) : %(#{noteable.to_reference})
- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 3ee713cf499..d0f9374e832 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,11 +1,26 @@
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+- import_url = Gitlab::UrlSanitizer.new(f.object.import_url)
-.form-group.import-url-data
- = f.label :import_url, class: 'label-bold' do
- %span
- = _('Git repository URL')
+.import-url-data
+ .form-group
+ = f.label :import_url, class: 'label-bold' do
+ %span
+ = _('Git repository URL')
+ = f.text_field :import_url, value: import_url.sanitized_url,
+ autocomplete: 'off', class: 'form-control', placeholder: 'https://gitlab.company.com/group/project.git', required: true
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
+ .row
+ .form-group.col-md-6
+ = f.label :import_url_user, class: 'label-bold' do
+ %span
+ = _('Username (optional)')
+ = f.text_field :import_url_user, value: import_url.user, class: 'form-control', required: false, autocomplete: 'new-password'
+
+ .form-group.col-md-6
+ = f.label :import_url_password, class: 'label-bold' do
+ %span
+ = _('Password (optional)')
+ = f.password_field :import_url_password, class: 'form-control', required: false, autocomplete: 'new-password'
.info-well.prepend-top-20
.well-segment
@@ -13,7 +28,7 @@
%li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
%li
- = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe
+ = _('If your HTTP repository is not publicly accessible, add your credentials.')
%li
= import_will_timeout_message(ci_cd_only)
%li
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 311dc69d213..c50826a7cda 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -32,7 +32,7 @@
%span.dropdown-toggle-text
{{ labelDropdownTitle }}
= icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default"
- if can?(current_user, :admin_label, current_board_parent)
= render partial: "shared/issuable/label_page_create", locals: { show_add_list: true }
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index f2c0c77a583..483652852b6 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -25,7 +25,7 @@
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
= icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)
= render partial: "shared/issuable/label_page_create"
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 63557c882f4..3a5adb34ad1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -118,7 +118,7 @@
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default"
- if issuable_sidebar.dig(:current_user, :can_admin_label)
= render partial: "shared/issuable/label_page_create"
@@ -158,7 +158,7 @@
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
data: { toggle: 'dropdown', display: 'static' } }
= _('Move issue')
- .dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
= dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search')
= dropdown_content
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2db1f67a793..afcb2b71472 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -4,8 +4,9 @@
- member = local_assigns.fetch(:member)
- user = local_assigns.fetch(:user, member.user)
- source = member.source
+- override = member.try(:override)
-%li.member{ class: dom_class(member), id: dom_id(member) }
+%li.member{ class: [dom_class(member), ("is-overridden" if override)], id: dom_id(member) }
%span.list-item-name
- if user
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
@@ -54,6 +55,7 @@
- if show_roles
- current_resource = @project || @group
.controls.member-controls
+ = render_if_exists 'shared/members/ee/ldap_tag', can_override: member.can_override?
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
@@ -67,6 +69,7 @@
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
+ disabled: member.can_override? && !override,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text
= member.human_access
@@ -80,8 +83,13 @@
= link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
+ = render_if_exists 'shared/members/ee/revert_ldap_group_sync_option',
+ group: @group,
+ member: member,
+ can_override: member.can_override?
.prepend-left-5.clearable-input.member-form-control
= f.text_field :expires_at,
+ disabled: member.can_override? && !override,
class: 'form-control js-access-expiration-date js-member-update-control',
placeholder: _('Expiration date'),
id: "member_expires_at_#{member.id}",
@@ -116,5 +124,8 @@
= _("Delete")
- unless force_mobile_view
= icon('trash', class: 'd-none d-sm-block')
+ = render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override?
- else
%span.member-access-text= member.human_access
+
+= render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: member.can_override?
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 46f3f8428f1..fae7d6526e8 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -28,8 +28,9 @@
or
%button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
- %button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
+ %button.markdown-selector.button-attach-file.btn-link{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- = _("Attach a file")
+ %span.text-attach-file<>
+ = _("Attach a file")
%button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' }= _("Cancel")
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e4e85de93da..fd0cc5fb24e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1,6 +1,8 @@
---
- auto_devops:auto_devops_disable
+- auto_merge:auto_merge_process
+
- cronjob:admin_email
- cronjob:expire_build_artifacts
- cronjob:gitlab_usage_ping
diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb
new file mode 100644
index 00000000000..cd81cdbc60c
--- /dev/null
+++ b/app/workers/auto_merge_process_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AutoMergeProcessWorker
+ include ApplicationWorker
+
+ queue_namespace :auto_merge
+
+ def perform(merge_request_id)
+ MergeRequest.find_by_id(merge_request_id).try do |merge_request|
+ AutoMergeService.new(merge_request.project, merge_request.merge_user)
+ .process(merge_request)
+ end
+ end
+end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 8a9ee7808e4..9410fd1a786 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,47 +3,12 @@
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
- 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)
+ Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
+ schedules.each do |schedule|
+ Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def error(schedule, error)
- failed_creation_counter.increment
-
- Rails.logger.error "Failed to create a scheduled pipeline. " \
- "schedule_id: #{schedule.id} message: #{error.message}"
-
- Gitlab::Sentry
- .track_exception(error,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: schedule.id })
- end
-
- def failed_creation_counter
- @failed_creation_counter ||=
- Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
- "Counter of failed attempts of pipeline schedule creation")
- end
end
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 4f349ed922c..666331e6cd4 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -6,13 +6,7 @@ class PipelineSuccessWorker
queue_namespace :pipeline_processing
- # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
- MergeRequests::MergeWhenPipelineSucceedsService
- .new(pipeline.project, nil)
- .trigger(pipeline)
- end
+ # no-op
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index f72331c003a..43e0b9db22f 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -21,6 +21,30 @@ class RunPipelineScheduleWorker
Ci::CreatePipelineService.new(schedule.project,
user,
ref: schedule.ref)
- .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
+ rescue Ci::CreatePipelineService::CreateError
+ # no-op. This is a user operation error such as corrupted .gitlab-ci.yml.
+ rescue => e
+ error(schedule, e)
+ end
+
+ private
+
+ def error(schedule, error)
+ failed_creation_counter.increment
+
+ Rails.logger.error "Failed to create a scheduled pipeline. " \
+ "schedule_id: #{schedule.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: schedule.id })
+ end
+
+ def failed_creation_counter
+ @failed_creation_counter ||=
+ Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
+ "Counter of failed attempts of pipeline schedule creation")
end
end
diff --git a/babel.config.js b/babel.config.js
index df30892731d..05554e8763e 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -39,7 +39,7 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
}
// Jest is running in node environment, so we need additional plugins
-const isJest = !!process.env.JEST_WORKER_ID;
+const isJest = Boolean(process.env.JEST_WORKER_ID);
if (isJest) {
plugins.push('@babel/plugin-transform-modules-commonjs');
/*
diff --git a/changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml b/changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml
new file mode 100644
index 00000000000..4c85d4f9acb
--- /dev/null
+++ b/changelogs/unreleased/10795-add-epic-tree-BE-CE-epic-graphql-support.yml
@@ -0,0 +1,5 @@
+---
+title: Added reference, web_path, and relative_position fields to GraphQL Issue
+merge_request: 28998
+author:
+type: changed
diff --git a/changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml b/changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml
new file mode 100644
index 00000000000..45770e1012c
--- /dev/null
+++ b/changelogs/unreleased/50850-kerrizor-extend-api-to-accept-start_project-option.yml
@@ -0,0 +1,5 @@
+---
+title: Add API support for committing changes to different projects in same fork network
+merge_request: 27915
+author:
+type: added
diff --git a/changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml b/changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml
new file mode 100644
index 00000000000..07bf8b04bbe
--- /dev/null
+++ b/changelogs/unreleased/51022-added-extended-height-to-labels-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: "Added the `.extended-height` class to the labels-dropdown"
+merge_request: 28659
+author: Michel Engelen
+type: other
diff --git a/changelogs/unreleased/56959-drop-project_auto_devops_domain.yml b/changelogs/unreleased/56959-drop-project_auto_devops_domain.yml
new file mode 100644
index 00000000000..c529749670d
--- /dev/null
+++ b/changelogs/unreleased/56959-drop-project_auto_devops_domain.yml
@@ -0,0 +1,5 @@
+---
+title: Removes project_auto_devops#domain column
+merge_request: 28574
+author:
+type: other
diff --git a/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml b/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml
new file mode 100644
index 00000000000..a2de6cd6d45
--- /dev/null
+++ b/changelogs/unreleased/57037-fix-mr-checkboxes-mobile-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Merge Request merge checkbox alignment on mobile view
+merge_request: 28845
+author:
+type: fixed
diff --git a/changelogs/unreleased/58269-separate-update-patch.yml b/changelogs/unreleased/58269-separate-update-patch.yml
new file mode 100644
index 00000000000..e9b44257b07
--- /dev/null
+++ b/changelogs/unreleased/58269-separate-update-patch.yml
@@ -0,0 +1,5 @@
+---
+title: Do not display Update app button when saving Knative domain name
+merge_request: 28904
+author:
+type: changed
diff --git a/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
new file mode 100644
index 00000000000..53be008816d
--- /dev/null
+++ b/changelogs/unreleased/58941-use-gitlab-serverless-with-existing-knative-installation.yml
@@ -0,0 +1,5 @@
+---
+title: Enable function features for external Knative installations
+merge_request: 27173
+author:
+type: changed
diff --git a/changelogs/unreleased/60778-input-text-height.yml b/changelogs/unreleased/60778-input-text-height.yml
deleted file mode 100644
index c956ead5db2..00000000000
--- a/changelogs/unreleased/60778-input-text-height.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix input group height
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml b/changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml
new file mode 100644
index 00000000000..53cc0a15417
--- /dev/null
+++ b/changelogs/unreleased/61045-charts-with-many-overlapping-series-display-incorrectly.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate color inconsistencies in metric graphs
+merge_request: 29127
+author:
+type: fixed
diff --git a/changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml b/changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml
new file mode 100644
index 00000000000..c3808709fca
--- /dev/null
+++ b/changelogs/unreleased/61339-Add-underline-to-attach-a-file.yml
@@ -0,0 +1,5 @@
+---
+title: Add hover and focus to Attach a file
+merge_request: 28682
+author: Marcel van Remmerden
+type: fixed
diff --git a/changelogs/unreleased/61821-tooltip-consistency.yml b/changelogs/unreleased/61821-tooltip-consistency.yml
new file mode 100644
index 00000000000..9b131907ebf
--- /dev/null
+++ b/changelogs/unreleased/61821-tooltip-consistency.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Tooltip Consistency
+merge_request: 28839
+author:
+type: fixed
diff --git a/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml b/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml
new file mode 100644
index 00000000000..50b3efba0a5
--- /dev/null
+++ b/changelogs/unreleased/61960-translatable-strings-in-issue-closure-emails.yml
@@ -0,0 +1,5 @@
+---
+title: I18n for issue closure reason in emails
+merge_request: 28489
+author: Michał Zając
+type: changed
diff --git a/changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml b/changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml
new file mode 100644
index 00000000000..46d3f439a44
--- /dev/null
+++ b/changelogs/unreleased/61988-collapse-icon-on-merge-request-diff-larger-than-profile-picture.yml
@@ -0,0 +1,5 @@
+---
+title: Change collapse icon size to size of profile picture
+merge_request: 28512
+author: Marcel van Remmerden
+type: other
diff --git a/changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml b/changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml
new file mode 100644
index 00000000000..3317d505924
--- /dev/null
+++ b/changelogs/unreleased/62092-missing-padding-next-to-time-windows-dropdown-on-metrics-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Added padding to time window dropdown in monitor dashboard
+merge_request: 28897
+author:
+type: fixed
diff --git a/changelogs/unreleased/abstract-auto-merge.yml b/changelogs/unreleased/abstract-auto-merge.yml
new file mode 100644
index 00000000000..d3069a3e500
--- /dev/null
+++ b/changelogs/unreleased/abstract-auto-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor and abstract Auto Merge Processes
+merge_request: 28595
+author:
+type: other
diff --git a/changelogs/unreleased/ac-graphql-stats.yml b/changelogs/unreleased/ac-graphql-stats.yml
new file mode 100644
index 00000000000..8837dce4d89
--- /dev/null
+++ b/changelogs/unreleased/ac-graphql-stats.yml
@@ -0,0 +1,5 @@
+---
+title: Add Namespace and ProjectStatistics to GraphQL API
+merge_request: 28277
+author:
+type: added
diff --git a/changelogs/unreleased/ac-graphql-wikisize.yml b/changelogs/unreleased/ac-graphql-wikisize.yml
new file mode 100644
index 00000000000..be9c347ec21
--- /dev/null
+++ b/changelogs/unreleased/ac-graphql-wikisize.yml
@@ -0,0 +1,5 @@
+---
+title: Expose wiki_size on GraphQL API
+merge_request: 29123
+author:
+type: added
diff --git a/changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml b/changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml
new file mode 100644
index 00000000000..ecfbc97a8c5
--- /dev/null
+++ b/changelogs/unreleased/bump-auto-devops-helm-2-14-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Helm version in Auto-DevOps.gitlab-ci.yml to 2.14.0
+merge_request: 28527
+author:
+type: other
diff --git a/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml b/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml
new file mode 100644
index 00000000000..d38046ebcbf
--- /dev/null
+++ b/changelogs/unreleased/cancel-auto-merge-when-merge-request-is-closed.yml
@@ -0,0 +1,5 @@
+---
+title: Cancel auto merge when merge request is closed
+merge_request: 28782
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-disable-two-step-rebase.yml b/changelogs/unreleased/dm-disable-two-step-rebase.yml
deleted file mode 100644
index 342d61a20d6..00000000000
--- a/changelogs/unreleased/dm-disable-two-step-rebase.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disable two-step rebase which could cause rebases to hang
-merge_request: 28778
-author:
-type: other
diff --git a/changelogs/unreleased/dm-http-hostname-override.yml b/changelogs/unreleased/dm-http-hostname-override.yml
new file mode 100644
index 00000000000..f84f36a0010
--- /dev/null
+++ b/changelogs/unreleased/dm-http-hostname-override.yml
@@ -0,0 +1,5 @@
+---
+title: Protect Gitlab::HTTP against DNS rebinding attack
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/fix-search-dropdown-blur-close.yml b/changelogs/unreleased/fix-search-dropdown-blur-close.yml
new file mode 100644
index 00000000000..1ac9dc674fe
--- /dev/null
+++ b/changelogs/unreleased/fix-search-dropdown-blur-close.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search dropdown not closing on blur if empty
+merge_request: 28730
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.43.0.yml b/changelogs/unreleased/gitaly-version-v1.43.0.yml
new file mode 100644
index 00000000000..67acd2725e1
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.43.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.43.0
+merge_request: 28867
+author:
+type: changed
diff --git a/changelogs/unreleased/increase-move-issue-dropdown-height.yml b/changelogs/unreleased/increase-move-issue-dropdown-height.yml
new file mode 100644
index 00000000000..bb67e9341b2
--- /dev/null
+++ b/changelogs/unreleased/increase-move-issue-dropdown-height.yml
@@ -0,0 +1,5 @@
+---
+title: Increase height of move issue dropdown
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/jp-label-fix.yml b/changelogs/unreleased/jp-label-fix.yml
deleted file mode 100644
index de64286cc1f..00000000000
--- a/changelogs/unreleased/jp-label-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix display of 'Promote to group label' button.
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml b/changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml
new file mode 100644
index 00000000000..6b5f97f24b3
--- /dev/null
+++ b/changelogs/unreleased/osw-reset-merge-status-from-mergeable-mrs.yml
@@ -0,0 +1,5 @@
+---
+title: Reset merge status from mergeable MRs
+merge_request: 28843
+author:
+type: other
diff --git a/changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml b/changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml
new file mode 100644
index 00000000000..1f40089adb8
--- /dev/null
+++ b/changelogs/unreleased/osw-sync-merge-ref-upon-mergeability-check.yml
@@ -0,0 +1,5 @@
+---
+title: Sync merge ref upon mergeability check
+merge_request: 28513
+author:
+type: added
diff --git a/changelogs/unreleased/patch-64.yml b/changelogs/unreleased/patch-64.yml
deleted file mode 100644
index 1bf022e7e41..00000000000
--- a/changelogs/unreleased/patch-64.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update SAST.gitlab-ci.yml - Add SAST_GITLEAKS_ENTROPY_LEVEL
-merge_request: 28607
-author:
-type: fixed
diff --git a/changelogs/unreleased/pb-update-gitaly-1-45-0.yml b/changelogs/unreleased/pb-update-gitaly-1-45-0.yml
new file mode 100644
index 00000000000..eaad7a8378b
--- /dev/null
+++ b/changelogs/unreleased/pb-update-gitaly-1-45-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GITALY_SERVER_VERSION to 1.45.0
+merge_request: 29109
+author:
+type: changed
diff --git a/changelogs/unreleased/remove-mr-diff-header-height.yml b/changelogs/unreleased/remove-mr-diff-header-height.yml
new file mode 100644
index 00000000000..c06c7281c58
--- /dev/null
+++ b/changelogs/unreleased/remove-mr-diff-header-height.yml
@@ -0,0 +1,5 @@
+---
+title: Remove fixed height from MR diff headers
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml b/changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml
new file mode 100644
index 00000000000..d9ad5af256a
--- /dev/null
+++ b/changelogs/unreleased/security-58856-persistent-xss-in-note-objects.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent XSS injection in note imports
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-60039.yml b/changelogs/unreleased/security-60039.yml
new file mode 100644
index 00000000000..5edbf32ec97
--- /dev/null
+++ b/changelogs/unreleased/security-60039.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent invalid branch for merge request
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml b/changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml
new file mode 100644
index 00000000000..5b79258af54
--- /dev/null
+++ b/changelogs/unreleased/security-60143-address-xss-issue-in-wiki-links.yml
@@ -0,0 +1,5 @@
+---
+title: Filter relative links in wiki for XSS
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml b/changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml
new file mode 100644
index 00000000000..adfd8e1298f
--- /dev/null
+++ b/changelogs/unreleased/security-fix-confidential-issue-label-visibility-master.yml
@@ -0,0 +1,5 @@
+---
+title: Fix confidential issue label disclosure on milestone view
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix-project-existence-disclosure-master.yml b/changelogs/unreleased/security-fix-project-existence-disclosure-master.yml
new file mode 100644
index 00000000000..084439c71d9
--- /dev/null
+++ b/changelogs/unreleased/security-fix-project-existence-disclosure-master.yml
@@ -0,0 +1,5 @@
+---
+title: Fix url redaction for issue links
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-fix_milestones_search_api_leak.yml b/changelogs/unreleased/security-fix_milestones_search_api_leak.yml
new file mode 100644
index 00000000000..5691550b602
--- /dev/null
+++ b/changelogs/unreleased/security-fix_milestones_search_api_leak.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve: Milestones leaked via search API'
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml b/changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml
new file mode 100644
index 00000000000..df636ec37fb
--- /dev/null
+++ b/changelogs/unreleased/security-id-leaked-password-in-import-url-frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Add extra fields for handling basic auth on import by url page
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml b/changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml
new file mode 100644
index 00000000000..02773fa1d7c
--- /dev/null
+++ b/changelogs/unreleased/security-jej-prevent-web-sign-in-bypass.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent bypass of restriction disabling web password sign in
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-unsubscribing-from-issue.yml b/changelogs/unreleased/security-unsubscribing-from-issue.yml
new file mode 100644
index 00000000000..3a33a457c69
--- /dev/null
+++ b/changelogs/unreleased/security-unsubscribing-from-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Hide confidential issue title on unsubscribe for anonymous users
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml b/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml
new file mode 100644
index 00000000000..04eb035b157
--- /dev/null
+++ b/changelogs/unreleased/set-real-next-run-at-for-preventing-duplciate-pipeline-creations.yml
@@ -0,0 +1,5 @@
+---
+title: Make pipeline schedule worker resilient
+merge_request: 28407
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml b/changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml
deleted file mode 100644
index 561c19c9685..00000000000
--- a/changelogs/unreleased/sh-fix-omniauth-generic-strategy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix OmniAuth OAuth2Generic strategy not loading
-merge_request: 28680
-author:
-type: fixed
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml
new file mode 100644
index 00000000000..9ca6d18c2a8
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-5-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.5.2
+merge_request: 29050
+author:
+type: other
diff --git a/changelogs/unreleased/update-pages.yml b/changelogs/unreleased/update-pages.yml
new file mode 100644
index 00000000000..97a20b6b8fa
--- /dev/null
+++ b/changelogs/unreleased/update-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Pages to v1.6.0
+merge_request: 29048
+author:
+type: other
diff --git a/changelogs/unreleased/use-source-ref-name-in-webhook.yml b/changelogs/unreleased/use-source-ref-name-in-webhook.yml
deleted file mode 100644
index 1a5c56d79ca..00000000000
--- a/changelogs/unreleased/use-source-ref-name-in-webhook.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use source ref in pipeline webhook
-merge_request: 28772
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-remove-delta-island-feature-flag.yml b/changelogs/unreleased/zj-remove-delta-island-feature-flag.yml
new file mode 100644
index 00000000000..e752e01b701
--- /dev/null
+++ b/changelogs/unreleased/zj-remove-delta-island-feature-flag.yml
@@ -0,0 +1,3 @@
+merge_request: 28871
+title: Improve clone performance by using delta islands
+type: performance
diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb
index 1879ecb15fb..51bd48af320 100644
--- a/config/initializers/hipchat_client_patch.rb
+++ b/config/initializers/hipchat_client_patch.rb
@@ -2,14 +2,14 @@
# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb.
module HipChat
class Client
- connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ connection_adapter ::Gitlab::HTTPConnectionAdapter
end
class Room
- connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ connection_adapter ::Gitlab::HTTPConnectionAdapter
end
class User
- connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter
+ connection_adapter ::Gitlab::HTTPConnectionAdapter
end
end
diff --git a/config/initializers/http_hostname_override.rb b/config/initializers/http_hostname_override.rb
new file mode 100644
index 00000000000..58dd380326f
--- /dev/null
+++ b/config/initializers/http_hostname_override.rb
@@ -0,0 +1,49 @@
+# This override allows passing `@hostname_override` to the SNI protocol,
+# which is used to lookup the correct SSL certificate in the
+# request handshake process.
+#
+# Given we've forced the HTTP request to be sent to the resolved
+# IP address in a few scenarios (e.g.: `Gitlab::HTTP` through
+# `Gitlab::UrlBlocker.validate!`), we need to provide the _original_
+# hostname via SNI in order to have a clean connection setup.
+#
+# This is ultimately needed in order to avoid DNS rebinding attacks
+# through HTTP requests.
+#
+class OpenSSL::SSL::SSLContext
+ attr_accessor :hostname_override
+end
+
+class OpenSSL::SSL::SSLSocket
+ module HostnameOverride
+ # rubocop: disable Gitlab/ModuleWithInstanceVariables
+ def hostname=(hostname)
+ super(@context.hostname_override || hostname)
+ end
+
+ def post_connection_check(hostname)
+ super(@context.hostname_override || hostname)
+ end
+ # rubocop: enable Gitlab/ModuleWithInstanceVariables
+ end
+
+ prepend HostnameOverride
+end
+
+class Net::HTTP
+ attr_accessor :hostname_override
+ SSL_IVNAMES << :@hostname_override
+ SSL_ATTRIBUTES << :hostname_override
+
+ module HostnameOverride
+ def addr_port
+ return super unless hostname_override
+
+ addr = hostname_override
+ default_port = use_ssl? ? Net::HTTP.https_default_port : Net::HTTP.http_default_port
+ default_port == port ? addr : "#{addr}:#{port}"
+ end
+ end
+
+ prepend HostnameOverride
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 1e94bdc245f..d44ff62bc2a 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -124,6 +124,44 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
put :revoke
end
end
+
+ resources :milestones, constraints: { id: /\d+/ } do
+ member do
+ post :promote
+ put :sort_issues
+ put :sort_merge_requests
+ get :merge_requests
+ get :participants
+ get :labels
+ end
+ end
+
+ resources :labels, except: [:show], constraints: { id: /\d+/ } do
+ collection do
+ post :generate
+ post :set_priorities
+ end
+
+ member do
+ post :promote
+ post :toggle_subscription
+ delete :remove_priority
+ end
+ end
+
+ resources :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
+ member do
+ put :test
+ end
+ end
+
+ resources :boards, only: [:index, :show], constraints: { id: /\d+/ }
+ resources :releases, only: [:index]
+ resources :forks, only: [:index, :new, :create]
+ resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
+
+ resource :import, only: [:new, :create, :show]
+ resource :avatar, only: [:show, :destroy]
end
# End of the /-/ scope.
@@ -132,7 +170,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
#
get '/templates/:template_type/:key' => 'templates#show', as: :template, constraints: { key: %r{[^/]+} }
- resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
@@ -159,12 +196,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
- member do
- put :test
- end
- end
-
resource :mattermost, only: [:new, :create]
namespace :prometheus do
@@ -173,15 +204,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :releases, only: [:index]
- resources :forks, only: [:index, :new, :create]
- resource :import, only: [:new, :create, :show]
-
resources :merge_requests, concerns: :awardable, except: [:new, :create], constraints: { id: /\d+/ } do
member do
get :commit_change_content
post :merge
- post :cancel_merge_when_pipeline_succeeds
+ post :cancel_auto_merge
get :pipeline_status
get :ci_environments_status
post :toggle_subscription
@@ -372,31 +399,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :milestones, constraints: { id: /\d+/ } do
- member do
- post :promote
- put :sort_issues
- put :sort_merge_requests
- get :merge_requests
- get :participants
- get :labels
- end
- end
-
- resources :labels, except: [:show], constraints: { id: /\d+/ } do
- collection do
- post :generate
- post :set_priorities
- end
-
- member do
- post :promote
- post :toggle_subscription
- delete :remove_priority
- end
- end
-
get :issues, to: 'issues#calendar', constraints: lambda { |req| req.format == :ics }
+
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
@@ -408,14 +412,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :create_merge_request
get :discussions, format: :json
end
+
collection do
post :bulk_update
post :import_csv
end
end
- resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
-
resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
@@ -426,8 +429,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
- resources :boards, only: [:index, :show], constraints: { id: /\d+/ }
-
resources :todos, only: [:create]
resources :uploads, only: [:create] do
@@ -510,7 +511,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
as: :project) do
Gitlab::Routing.redirect_legacy_paths(self, :settings, :branches, :tags,
:network, :graphs, :autocomplete_sources,
- :project_members, :deploy_keys, :deploy_tokens)
+ :project_members, :deploy_keys, :deploy_tokens,
+ :labels, :milestones, :services, :boards, :releases,
+ :forks, :group_links, :import, :avatar)
end
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 0615da2d241..fd9ce4d3374 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -30,6 +30,7 @@
- [pipeline_default, 3]
- [pipeline_cache, 3]
- [deployment, 3]
+ - [auto_merge, 3]
- [pipeline_hooks, 2]
- [gitlab_shell, 2]
- [email_receiver, 2]
diff --git a/db/migrate/20180702134423_generate_missing_routes.rb b/db/migrate/20180702134423_generate_missing_routes.rb
index a440bc3179c..dd1106c9e6a 100644
--- a/db/migrate/20180702134423_generate_missing_routes.rb
+++ b/db/migrate/20180702134423_generate_missing_routes.rb
@@ -98,6 +98,7 @@ class GenerateMissingRoutes < ActiveRecord::Migration[4.2]
class Namespace < ActiveRecord::Base
self.table_name = 'namespaces'
+ self.inheritance_column = :_type_disabled
include EachBatch
include GenerateMissingRoutes::Routable
diff --git a/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb b/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..8835dc8b7ba
--- /dev/null
+++ b/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
@@ -0,0 +1,23 @@
+# 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 AddDnsRebindingProtectionEnabledToApplicationSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :dns_rebinding_protection_enabled,
+ :boolean,
+ default: true,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :dns_rebinding_protection_enabled)
+ end
+end
diff --git a/db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb b/db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.rb
new file mode 100644
index 00000000000..e669f81ca35
--- /dev/null
+++ b/db/migrate/20190530154715_add_index_to_merge_requests_state_and_merge_status.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 AddIndexToMergeRequestsStateAndMergeStatus < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_requests, [:state, :merge_status],
+ where: "state = 'opened' AND merge_status = 'can_be_merged'"
+ end
+
+ def down
+ remove_concurrent_index :merge_requests, [:state, :merge_status]
+ end
+end
diff --git a/db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb b/db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb
new file mode 100644
index 00000000000..36278d83927
--- /dev/null
+++ b/db/post_migrate/20190522143720_drop_project_auto_devops_domain.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class DropProjectAutoDevopsDomain < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :project_auto_devops, :domain, :string
+ end
+end
diff --git a/db/post_migrate/20190528180441_enqueue_reset_merge_status.rb b/db/post_migrate/20190528180441_enqueue_reset_merge_status.rb
new file mode 100644
index 00000000000..1b668d85bac
--- /dev/null
+++ b/db/post_migrate/20190528180441_enqueue_reset_merge_status.rb
@@ -0,0 +1,30 @@
+# 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 EnqueueResetMergeStatus < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+ MIGRATION = 'ResetMergeStatus'
+ DELAY_INTERVAL = 5.minutes.to_i
+
+ disable_ddl_transaction!
+
+ def up
+ say 'Scheduling `ResetMergeStatus` jobs'
+
+ # We currently have around 135_000 opened, mergeable MRs in GitLab.com. This iteration
+ # will schedule around 13 batches of 10_000 MRs, which should take around 1 hour to
+ # complete.
+ relation = MergeRequest.where(state: 'opened', merge_status: 'can_be_merged')
+
+ relation.each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, range)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 39d9743bcc9..fcf9e397ac1 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: 20190527194900) do
+ActiveRecord::Schema.define(version: 20190530154715) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -193,6 +193,7 @@ ActiveRecord::Schema.define(version: 20190527194900) do
t.integer "elasticsearch_replicas", default: 1, null: false
t.text "encrypted_lets_encrypt_private_key"
t.text "encrypted_lets_encrypt_private_key_iv"
+ t.boolean "dns_rebinding_protection_enabled", default: true, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -1361,6 +1362,7 @@ ActiveRecord::Schema.define(version: 20190527194900) do
t.index ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_and_branch_state_opened", where: "((state)::text = 'opened'::text)", using: :btree
t.index ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
+ t.index ["state", "merge_status"], name: "index_merge_requests_on_state_and_merge_status", where: "(((state)::text = 'opened'::text) AND ((merge_status)::text = 'can_be_merged'::text))", using: :btree
t.index ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
@@ -1633,7 +1635,6 @@ ActiveRecord::Schema.define(version: 20190527194900) do
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.boolean "enabled"
- t.string "domain"
t.integer "deploy_strategy", default: 0, null: false
t.index ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
end
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index f1cedb85455..dcf8d8715ca 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -53,6 +53,10 @@ But since 11.8 the indexer uses Gitaly for data access as well. NFS can still
be leveraged for redudancy on block level of the Git data. But only has to
be mounted on the Gitaly server.
+NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend
+using EFS as it may impact GitLab's performance. Please review the [relevant documentation](../high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs)
+for more details.
+
### Network architecture
- gitlab-rails shards repositories into "repository storages"
@@ -73,18 +77,29 @@ be mounted on the Gitaly server.
- Gitaly servers must not be exposed to the public internet
Gitaly network traffic is unencrypted by default, but supports
-[TLS](#tls-support). Authentication is done through a static token. For
-security in depth, its recommended to use a firewall to restrict access
-to your Gitaly server.
+[TLS](#tls-support). Authentication is done through a static token.
+
+NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to
+restrict access to your Gitaly server.
Below we describe how to configure a Gitaly server at address
`gitaly.internal:8075` with secret token `abc123secret`. We assume
your GitLab installation has two repository storages, `default` and
`storage1`.
+### Installation
+
+First install Gitaly using either Omnibus or from source.
+
+Omnibus: [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+package you want using **steps 1 and 2** from the GitLab downloads page but
+**_do not_** provide the `EXTERNAL_URL=` value.
+
+Source: [Install Gitaly](../../install/installation.md#install-gitaly)
+
### Client side token configuration
-Start by configuring a token on the client side.
+Configure a token on the client side.
Omnibus installations:
@@ -110,7 +125,7 @@ changes to be picked up.
Next, on the Gitaly server, we need to configure storage paths, enable
the network listener and configure the token.
-Note: if you want to reduce the risk of downtime when you enable
+NOTE: **Note:** if you want to reduce the risk of downtime when you enable
authentication you can temporarily disable enforcement, see [the
documentation on configuring Gitaly
authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication)
@@ -122,12 +137,17 @@ the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gi
from an existing GitLab server to the Gitaly server. Without this shared secret,
Git operations in GitLab will result in an API error.
-> **NOTE:** In most or all cases the storage paths below end in `/repositories` which is
+NOTE: **Note:** In most or all cases the storage paths below end in `/repositories` which is
different than `path` in `git_data_dirs` of Omnibus installations. Check the
directory layout on your Gitaly server to be sure.
Omnibus installations:
+<!--
+updates to following example must also be made at
+https://gitlab.com/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
+-->
+
```ruby
# /etc/gitlab/gitlab.rb
@@ -147,6 +167,7 @@ gitlab_rails['auto_migrate'] = false
# Configure the gitlab-shell API callback URL. Without this, `git push` will
# fail. This can be your 'front door' GitLab URL or an internal load
# balancer.
+# Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server.
gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
# Make Gitaly accept connections on all network interfaces. You must use
diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md
index d44744f2af8..40f85f28cb8 100644
--- a/doc/administration/high_availability/gitaly.md
+++ b/doc/administration/high_availability/gitaly.md
@@ -12,77 +12,8 @@ environments and [High Availability Architecture](./README.md#high-availability-
## Running Gitaly on its own server
-Starting with GitLab 11.4, Gitaly is a replacement for NFS except
-when the [Elastic Search indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer)
-is used.
-
-NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend using EFS as it may impact GitLab's performance. Please review the [relevant documentation](nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
-
-NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to
-restrict access to your Gitaly server.
-
-The steps below are the minimum necessary to configure a Gitaly server with
-Omnibus:
-
-1. SSH into the Gitaly server.
-1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
- package you want using **steps 1 and 2** from the GitLab downloads page.
- - Do not complete any other steps on the download page.
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
-
- Gitaly must trigger some callbacks to GitLab via GitLab Shell. As a result,
- the GitLab Shell secret must be the same between the other GitLab servers and
- the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gitlab-secrets.json`
- from an existing GitLab server to the Gitaly server. Without this shared secret,
- Git operations in GitLab will result in an API error.
-
- > **NOTE:** In most or all cases the storage paths below end in `repositories` which is
- different than `path` in `git_data_dirs` of Omnibus installations. Check the
- directory layout on your Gitaly server to be sure.
-
- ```ruby
- # Enable Gitaly
- gitaly['enable'] = true
-
- ## Disable all other services
- sidekiq['enable'] = false
- gitlab_workhorse['enable'] = false
- unicorn['enable'] = false
- postgresql['enable'] = false
- nginx['enable'] = false
- prometheus['enable'] = false
- alertmanager['enable'] = false
- pgbouncer_exporter['enable'] = false
- redis_exporter['enable'] = false
- gitlab_monitor['enable'] = false
-
- # Prevent database connections during 'gitlab-ctl reconfigure'
- gitlab_rails['rake_cache_clear'] = false
- gitlab_rails['auto_migrate'] = false
-
- # Configure the gitlab-shell API callback URL. Without this, `git push` will
- # fail. This can be your 'front door' GitLab URL or an internal load
- # balancer.
- gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
-
- # Make Gitaly accept connections on all network interfaces. You must use
- # firewalls to restrict access to this address/port.
- gitaly['listen_addr'] = "0.0.0.0:8075"
- gitaly['auth_token'] = 'abc123secret'
-
- gitaly['storage'] = [
- { 'name' => 'default', 'path' => '/mnt/gitlab/default/repositories' },
- { 'name' => 'storage1', 'path' => '/mnt/gitlab/storage1/repositories' },
- ]
-
- # To use tls for gitaly you need to add
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "path/to/cert.pem"
- gitaly['key_path'] = "path/to/key.pem"
- ```
-
-Again, reconfigure (Omnibus) or restart (source).
+See [Running Gitaly on its own server](../gitaly/index.md#running-gitaly-on-its-own-server)
+in Gitaly documentation.
Continue configuration of other components by going back to:
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 46ad3ecd9bb..1f37224a184 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -863,7 +863,7 @@ You can check if everything is correct by connecting to each server using
`redis-cli` application, and sending the `info replication` command as below.
```
-/opt/gitlab/embedded/bin/redis-cli -a <redis-password> info replication
+/opt/gitlab/embedded/bin/redis-cli -h <redis-host-or-ip> -a '<redis-password>' info replication
```
When connected to a `master` redis, you will see the number of connected
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 2596e3fe68b..c34858cd0db 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -43,6 +43,11 @@ detail below.
## Enabling and disabling terminal support
+NOTE: **Note:** AWS Elastic Load Balancers (ELBs) do not support web sockets.
+AWS Application Load Balancers (ALBs) must be used if you want web terminals
+to work. See [AWS Elastic Load Balancing Product Comparison](https://aws.amazon.com/elasticloadbalancing/features/#compare)
+for more information.
+
As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of
Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers
through to the next one in the chain. If you installed GitLab using Omnibus, or
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 4529bd3bee3..3dcd1593099 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -43,10 +43,11 @@ The following metrics are available:
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
-| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
-| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
-| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
-| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
+| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
+| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
+| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
+| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
+| unicorn_workers | Gauge | 12.0 | The number of Unicorn workers |
## Sidekiq Metrics available for Geo **[PREMIUM]**
@@ -86,8 +87,8 @@ the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`.
| geo_wikis_checksum_mismatch_count | Gauge | 10.7 | Number of wikis that checksum mismatch on secondary | url
| geo_repositories_checked_count | Gauge | 11.1 | Number of repositories that have been checked via `git fsck` | url
| geo_repositories_checked_failed_count | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | url
-| geo_repositories_retrying_verification_count | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url
-| geo_wikis_retrying_verification_count | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url
+| geo_repositories_retrying_verification_count | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url
+| geo_wikis_retrying_verification_count | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url
### Ruby metrics
@@ -100,6 +101,10 @@ Some basic Ruby runtime metrics are available:
| ruby_file_descriptors | Gauge | 11.1 | File descriptors per process |
| ruby_memory_bytes | Gauge | 11.1 | Memory usage by process |
| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats |
+| ruby_process_cpu_seconds_total | Gauge | 12.0 | Total amount of CPU time per process |
+| ruby_process_max_fds | Gauge | 12.0 | Maximum number of open file descriptors per process |
+| ruby_process_resident_memory_bytes | Gauge | 12.0 | Memory usage by process, measured in bytes |
+| ruby_process_start_time_seconds | Gauge | 12.0 | The elapsed time between system boot and the process started, measured in seconds |
[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index b295b7d5dc4..0b4c1ae15d6 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -205,25 +205,6 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
```
-## Create or repair repository hooks symlink
-
-If the GitLab shell hooks directory location changes or another circumstance
-leads to the hooks symlink becoming missing or invalid, run this Rake task
-to create or repair the symlinks.
-
-**Omnibus Installation**
-
-```
-sudo gitlab-rake gitlab:shell:create_hooks
-```
-
-**Source Installation**
-
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production
-```
-
## Check TCP connectivity to a remote site
Sometimes you need to know if your GitLab installation can connect to a TCP
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 92f53c7b5e6..25015fad9e3 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -75,6 +75,7 @@ POST /projects/:id/repository/commits
| `branch` | string | yes | Name of the branch to commit into. To create a new branch, also provide `start_branch`. |
| `commit_message` | string | yes | Commit message |
| `start_branch` | string | no | Name of the branch to start the new commit from |
+| `start_project` | integer/string | no | The project ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) to start the commit from. Defaults to the value of `id`. |
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
| `author_email` | string | no | Specify the commit author's email address |
| `author_name` | string | no | Specify the commit author's name |
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
index 9195ba4cdf1..88e657a5d2f 100644
--- a/doc/api/graphql/index.md
+++ b/doc/api/graphql/index.md
@@ -47,6 +47,7 @@ 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.
+1. `namespace` : Within a namespace it is also possible to fetch `projects`.
### Multiplex queries
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 7992af15448..9529a9ec1f5 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1159,33 +1159,29 @@ Parameters:
}
```
-## Merge to default merge ref path
+## Returns the up to date merge-ref HEAD commit
Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge`
-ref, of the target project repository. This ref will have the state the target branch would have if
+ref, of the target project repository, if possible. This ref will have the state the target branch would have if
a regular merge action was taken.
-This is not a regular merge action given it doesn't change the merge request state in any manner.
+This is not a regular merge action given it doesn't change the merge request target branch state in any manner.
-This ref (`refs/merge-requests/:iid/merge`) is **always** overwritten when submitting
-requests to this API, so none of its state is kept or used in the process.
+This ref (`refs/merge-requests/:iid/merge`) isn't necessarily overwritten when submitting
+requests to this API, though it'll make sure the ref has the latest possible state.
-If the merge request has conflicts, is empty or already merged,
-you'll get a `400` and a descriptive error message. If you don't have permissions to do so,
-you'll get a `403`.
+If the merge request has conflicts, is empty or already merged, you'll get a `400` and a descriptive error message.
-It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in
-case of `200`.
+It returns the HEAD commit of `refs/merge-requests/:iid/merge` in the response body in case of `200`.
```
-PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref
+GET /projects/:id/merge_requests/:merge_request_iid/merge_ref
```
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - Internal ID of MR
-- `merge_commit_message` (optional) - Custom merge commit message
```json
{
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index f90447e124e..1ce0b1e7a62 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -165,15 +165,15 @@ Parameters:
|:--------------|:-------|:---------|:---------------------------------------------------|
| `title` | string | yes | Title of a snippet. |
| `file_name` | string | yes | Name of a snippet file. |
-| `content` | string | yes | Content of a snippet. |
+| `code` | string | yes | Content of a snippet. |
| `description` | string | no | Description of a snippet. |
-| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). |
+| `visibility` | string | yes | Snippet's [visibility](#snippet-visibility-level). |
Example request:
```sh
curl --request POST \
- --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \
+ --data '{"title": "This is a snippet", "code": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \
--header 'Content-Type: application/json' \
--header "PRIVATE-TOKEN: valid_api_token" \
https://gitlab.example.com/api/v4/snippets
@@ -222,14 +222,14 @@ Parameters:
| `title` | string | no | Title of a snippet. |
| `file_name` | string | no | Name of a snippet file. |
| `description` | string | no | Description of a snippet. |
-| `content` | string | no | Content of a snippet. |
+| `code` | string | no | Content of a snippet. |
| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). |
Example request:
```sh
curl --request PUT \
- --data '{"title": "foo", "content": "bar"}' \
+ --data '{"title": "foo", "code": "bar"}' \
--header 'Content-Type: application/json' \
--header "PRIVATE-TOKEN: valid_api_token" \
https://gitlab.example.com/api/v4/snippets/1
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 02370bead00..11bcfd5dc2c 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -4,10 +4,15 @@ type: reference
# Getting started with GitLab CI/CD
->**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
+NOTE: **Note:**
+Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
is fully integrated into GitLab itself and is [enabled] by default on all
projects.
+NOTE: **Note:**
+Please keep in mind that only project Maintainers and Admin users have
+the permissions to access a project's settings.
+
GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
and configure your GitLab project to use a [Runner], then each commit or
@@ -44,6 +49,7 @@ This guide assumes that you have:
- A working GitLab instance of version 8.0+r or are using
[GitLab.com](https://gitlab.com).
- A project in GitLab that you would like to use CI for.
+- Maintainer or owner access to the project
Let's break it down to pieces and work on solving the GitLab CI puzzle.
@@ -77,6 +83,8 @@ You need to create a file named `.gitlab-ci.yml` in the root directory of your
repository. Below is an example for a Ruby on Rails project.
```yaml
+image: "ruby:2.5"
+
before_script:
- apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
- ruby -v
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 22a52a313b3..a0e4020da09 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -59,7 +59,7 @@ graph TB
PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
Prometheus -- TCP 9187 --> PostgreSQLExporter
Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
- Prometheus -- TCP 9168 --> GitLabMonito[GitLab Monitor]
+ Prometheus -- TCP 9168 --> GitLabMonitor[GitLab Monitor]
Prometheus -- TCP 9127 --> PgBouncerExporter
GitLabMonitor --> PostgreSQL
GitLabMonitor --> GitLabShell
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 94dfdbdd073..b50159c2b75 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -95,6 +95,7 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
#### Modules, Imports, and Exports
1. Use ES module syntax to import modules
+
```javascript
// bad
const SomeClass = require('some_class');
@@ -168,6 +169,7 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
Do not use them anymore and feel free to remove them when refactoring legacy code.
1. Avoid adding to the global namespace.
+
```javascript
// bad
window.MyClass = class { /* ... */ };
@@ -176,7 +178,8 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
export default class MyClass { /* ... */ }
```
-1. Side effects are forbidden in any script which contains exports
+1. Side effects are forbidden in any script which contains export
+
```javascript
// bad
export default class MyClass { /* ... */ }
@@ -449,6 +452,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
#### Props
1. Props should be declared as an object
+
```javascript
// bad
props: ['foo']
diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md
index 1b4c89ba2a8..84028b1b342 100644
--- a/doc/development/rolling_out_changes_using_feature_flags.md
+++ b/doc/development/rolling_out_changes_using_feature_flags.md
@@ -200,10 +200,9 @@ isn't gated by a License or Plan.
### Undefined feature flags default to "on"
-An important side-effect of the [implicit feature
-flags][#implicit-feature-flags] mentioned above is that unless the feature is
-explicitly disabled or limited to a percentage of users, the feature flag check
-will default to `true`.
+An important side-effect of the [implicit feature flags](#implicit-feature-flags)
+mentioned above is that unless the feature is explicitly disabled or limited to a
+percentage of users, the feature flag check will default to `true`.
As an example, if you were to ship the backend half of a feature behind a flag,
you'd want to explicitly disable that flag until the frontend half is also ready
diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md
index 01a0044f096..2ef8b3148e4 100644
--- a/doc/development/understanding_explain_plans.md
+++ b/doc/development/understanding_explain_plans.md
@@ -80,8 +80,9 @@ Planning time: 2.861 ms
Execution time: 3428.596 ms
```
-For more information, refer to the official [EXPLAIN
-documentation](https://www.postgresql.org/docs/current/static/sql-explain.html).
+For more information, refer to the official
+[`EXPLAIN` documentation](https://www.postgresql.org/docs/current/sql-explain.html)
+and [using `EXPLAIN` guide](https://www.postgresql.org/docs/current/using-explain.html).
## Nodes
@@ -653,6 +654,35 @@ and related tools such as:
- <https://explain.depesz.com/>
- <http://tatiyants.com/postgres-query-plan-visualization/>
+## Producing query plans
+
+There are a few ways to get the output of a query plan. Of course you
+can directly run the `EXPLAIN` query in the `psql` console, or you can
+follow one of the other options below.
+
+### Rails console
+
+Using the [`activerecord-explain-analyze`](https://github.com/6/activerecord-explain-analyze)
+you can directly generate the query plan from the Rails console:
+
+```ruby
+pry(main)> require 'activerecord-explain-analyze'
+=> true
+pry(main)> Project.where('build_timeout > ?', 3600).explain(analyze: true)
+ Project Load (1.9ms) SELECT "projects".* FROM "projects" WHERE (build_timeout > 3600)
+ ↳ (pry):12
+=> EXPLAIN for: SELECT "projects".* FROM "projects" WHERE (build_timeout > 3600)
+Seq Scan on public.projects (cost=0.00..2.17 rows=1 width=742) (actual time=0.040..0.041 rows=0 loops=1)
+ Output: id, name, path, description, created_at, updated_at, creator_id, namespace_id, ...
+ Filter: (projects.build_timeout > 3600)
+ Rows Removed by Filter: 14
+ Buffers: shared hit=2
+Planning time: 0.411 ms
+Execution time: 0.113 ms
+```
+
+### Chatops
+
GitLab employees can also use our chatops solution, available in Slack using the
`/chatops` slash command. You can use chatops to get a query plan by running the
following:
@@ -674,3 +704,9 @@ For more information about the available options, run:
```
/chatops run explain --help
```
+
+## Further reading
+
+A more extensive guide on understanding query plans can be found in
+the [presentation](https://www.dalibo.org/_media/understanding_explain.pdf)
+from [Dalibo.org](https://www.dalibo.org/en/).
diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md
index 7654023f266..e44eab2556e 100644
--- a/doc/push_rules/push_rules.md
+++ b/doc/push_rules/push_rules.md
@@ -61,7 +61,7 @@ The following options are available.
| --------- | :------------: | ----------- |
| Removal of tags with `git push` | **Starter** 7.10 | Forbid users to remove git tags with `git push`. Tags will still be able to be deleted through the web UI. |
| Check whether author is a GitLab user | **Starter** 7.10 | Restrict commits by author (email) to existing GitLab users. |
-| Check whether committer is the current authenticated user | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
+| Committer restriction | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
| Check whether commit is signed through GPG | **Premium** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. |
| Prevent committing secrets to Git | **Starter** 8.12 | GitLab will reject any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). |
| Restrict by commit message | **Starter** 7.10 | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 56db7b5eb3a..764916ca82d 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -917,9 +917,9 @@ backup beforehand.
1. Clear all the tokens for projects, groups, and the whole instance:
-CAUTION: **Caution:**
-The last UPDATE operation will stop the runners being able to pick up
-new jobs. You must register new runners.
+ CAUTION: **Caution:**
+ The last UPDATE operation will stop the runners being able to pick up
+ new jobs. You must register new runners.
```sql
-- Clear project tokens
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 04938080539..b00a8afa386 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -582,16 +582,17 @@ Note that a post-install hook means that if any deploy succeeds,
If present, `DB_MIGRATE` will be run as a shell command within an application pod as
a helm pre-upgrade hook.
-For example, in a Rails application:
+For example, in a Rails application in an image built with
+[Herokuish](https://github.com/gliderlabs/herokuish):
-- `DB_INITIALIZE` can be set to `cd /app && RAILS_ENV=production
- bin/setup`
-- `DB_MIGRATE` can be set to `cd /app && RAILS_ENV=production bin/update`
+- `DB_INITIALIZE` can be set to `RAILS_ENV=production /bin/herokuish procfile exec bin/rails db:setup`
+- `DB_MIGRATE` can be set to `RAILS_ENV=production /bin/herokuish procfile exec bin/rails db:migrate`
NOTE: **Note:**
-The `/app` path is the directory of your project inside the docker image
-as [configured by
-Herokuish](https://github.com/gliderlabs/herokuish#paths)
+Unless you have a `Dockerfile` in your repo, your image is built with
+Herokuish. You must prefix commands run in these images with `/bin/herokuish
+procfile exec` in order to replicate the the environment your application is
+run in.
### Auto Monitoring
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
index da36a3218e5..c926f0b4888 100644
--- a/doc/university/training/topics/unstage.md
+++ b/doc/university/training/topics/unstage.md
@@ -8,13 +8,13 @@ comments: false
## Unstage
-- To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch.
+- To remove files from stage use reset HEAD where HEAD is the last commit of the current branch. This will unstage the file but maintain the modifications.
```bash
git reset HEAD <file>
```
-- This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+- To revert the file back to the state it was in before the changes we can use:
```bash
git checkout -- <file>
diff --git a/doc/user/admin_area/img/index_runners_search_or_filter.png b/doc/user/admin_area/img/index_runners_search_or_filter.png
new file mode 100755
index 00000000000..5176a1a39bf
--- /dev/null
+++ b/doc/user/admin_area/img/index_runners_search_or_filter.png
Binary files differ
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 0fc6ed349ba..527110d53df 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -20,7 +20,7 @@ The Admin Area is made up of the following sections:
| Section | Description |
|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), [users](#administer-users), groups, jobs, runners, and Gitaly servers. |
+| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), [users](#administer-users), groups, [jobs](#administer-jobs), [Runners](#administer-runners), and [Gitaly servers](#administer-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. |
@@ -109,7 +109,12 @@ created and the date of last activity. To edit a user, click the **Edit** button
row. To delete the user, or delete the user and their contributions, click the cog dropdown in
that user's row, and select the desired option.
-To change the sort order, click the sort dropdown and select the desired order. By default the sort dropdown shows **Name**.
+To change the sort order:
+
+1. Click the sort dropdown.
+1. Select the desired order.
+
+By default the sort dropdown shows **Name**.
To search for users, enter your criteria in the search field. The user search is case
insensitive, and applies partial matching to name and username. To search for an email address,
@@ -138,3 +143,66 @@ For each job, the following details are listed:
| Name | Name of the job specified in a `.gitlab-ci.yml` file. |
| Timing | Duration of the job, and how long ago the job completed. |
| Coverage | Percentage of tests coverage. |
+
+## Administer Runners
+
+You can adminster all Runners in the GitLab instance from the Admin Area's **Runners** page. See
+[GitLab Runner](https://docs.gitlab.com/runner/) for more information on Runner itself.
+
+To access the **Runners** page, go to **Admin Area > Overview > Runners**.
+
+The **Runners** page features:
+
+- A description of Runners, and their possible states.
+- Instructions on installing a Runner.
+- A list of all registered Runners.
+
+Runners are listed in descending order by the date they were created, by default. You can change
+the sort order to *Last Contacted* from the dropdown beside the search field.
+
+To search Runners' descriptions:
+
+1. In the **Search or filter results...** field, type the description of the Runner you want to
+find.
+1. Press Enter.
+
+You can also filter Runners by status, type, and tag. To filter:
+
+1. Click in the **Search or filter results...** field.
+1. Select **status:**, **type:**, or **tag:**
+1. Select or enter your search criteria.
+
+![Attributes of a Runner, with the **Search or filter results...** field active](img/index_runners_search_or_filter.png)
+
+For each Runner, the following attributes are listed:
+
+| Attribute | Description |
+| ------------ | ----------- |
+| Type | One or more of the following states: shared, group, specific, locked, or paused |
+| Runner token | Token used to identify the Runner, and which the Runner uses to communicate with the GitLab instance |
+| Description | Description given to the Runner when it was created |
+| Version | GitLab Runner version |
+| IP address | IP address of the host on which the Runner is registered |
+| Projects | Projects to which the Runner is assigned |
+| Jobs | Total of jobs run by the Runner |
+| Tags | Tags associated with the Runner |
+| Last contact | Timestamp indicating when the GitLab instance last contacted the Runner |
+
+You can also edit, pause, or remove each Runner.
+
+## Administer Gitaly servers
+
+You can list all Gitaly servers in the GitLab instance from the Admin Area's **Gitaly Servers**
+page. For more details, see [Gitaly](../../administration/gitaly/index.md).
+
+To access the **Gitaly Servers** page, go to **Admin Area > Overview > Gitaly Servers**.
+
+For each Gitaly server, the following details are listed:
+
+| Field | Description |
+| -------------- | ----------- |
+| Storage | Repository storage |
+| Address | Network address on which the Gitaly server is listening |
+| Server version | Gitaly version |
+| Git version | Version of Git installed on the Gitaly server |
+| Up to date | Indicates if the Gitaly server version is the latest version available. A green dot indicates the server is up to date. |
diff --git a/doc/user/admin_area/settings/instance_template_repository.md b/doc/user/admin_area/settings/instance_template_repository.md
index 4010008f694..91286a67c31 100644
--- a/doc/user/admin_area/settings/instance_template_repository.md
+++ b/doc/user/admin_area/settings/instance_template_repository.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Instance template repository **[PREMIUM ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5986) in
@@ -61,3 +65,15 @@ top of the list.
If this feature is disabled or no templates are present, there will be
no "Custom" section in the selection dropdown.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index d3ecfd42903..cebf36c7ec1 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Sign-up restrictions
You can block email addresses of specific domains, or whitelist only some
@@ -37,5 +41,17 @@ semicolon, comma, or a new line.
![Domain Blacklist](img/domain_blacklist.png)
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
+
[ce-5259]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5259
[ce-598]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/598
diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md
index 23311801790..d3c9cf7d8ff 100644
--- a/doc/user/admin_area/settings/third_party_offers.md
+++ b/doc/user/admin_area/settings/third_party_offers.md
@@ -1,9 +1,26 @@
+---
+type: reference
+---
+
# Third party offers
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379)
> in [GitLab Core](https://about.gitlab.com/pricing/) 11.1
-Within GitLab, we inform users of available third-party offers they might find valuable in order to enhance the development of their projects.
-An example is the Google Cloud Platform free credit for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/).
+Within GitLab, we inform users of available third-party offers they might find valuable in order
+to enhance the development of their projects. An example is the Google Cloud Platform free credit
+for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/).
+
+The display of third-party offers can be toggled in the **Admin Area > Settings** page.
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
-The display of third-party offers can be toggled in the Admin area on the Settings page.
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 83b268db967..58b7fe33906 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -53,7 +53,7 @@ If you visit the **Registry** link under your project's menu, you can see the
explicit instructions to login to the Container Registry using your GitLab
credentials.
-For example if the Registry's URL is `registry.example.com`, the you should be
+For example if the Registry's URL is `registry.example.com`, then you should be
able to login with:
```
@@ -204,7 +204,7 @@ at the communication between the client and the Registry.
The REST API between the Docker client and Registry is [described
here](https://docs.docker.com/registry/spec/api/). Normally, one would just
use Wireshark or tcpdump to capture the traffic and see where things went
-wrong. However, since all communication between Docker clients and servers
+wrong. However, since all communications between Docker clients and servers
are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
if you know the private key. What can we do instead?
diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md
index 3b37da67a5b..f48a158e2d3 100644
--- a/doc/user/project/import/gitlab_com.md
+++ b/doc/user/project/import/gitlab_com.md
@@ -1,8 +1,9 @@
# Project importing from GitLab.com to your private GitLab instance
You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
-GitLab support is enabled on your GitLab instance.
-You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
+GitLab.com integration is enabled on your GitLab instance.
+[Read more about GitLab.com integration for self-managed GitLab instances](../../../integration/gitlab.md).
+
To get to the importer page you need to go to "New project" page.
>**Note:**
diff --git a/doc/user/project/import/phabricator.md b/doc/user/project/import/phabricator.md
index 4d1d99fd35b..5c624e3aff6 100644
--- a/doc/user/project/import/phabricator.md
+++ b/doc/user/project/import/phabricator.md
@@ -15,18 +15,15 @@ Currently, only the following basic fields are imported:
- Created at
- Closed at
-
## Enabling this feature
While this feature is incomplete, a feature flag is required to enable it so that
we can gain early feedback before releasing it for everyone. To enable it:
-1. Enable Phabricator as an [import source](../../admin_area/settings/visibility_and_access_controls.md#import-sources) in the Admin area.
+1. Run the following command in a Rails console:
- ``` {.ruby}
- Feature.enable(:phabricator_import)
- ```
+ ```ruby
+ Feature.enable(:phabricator_import)
+ ```
-The [import
-source](../../admin_area/settings/visibility_and_access_controls.md#import-sources)
-also needs to be activated by an admin in the admin interface.
+1. Enable Phabricator as an [import source](../../admin_area/settings/visibility_and_access_controls.md#import-sources) in the Admin area.
diff --git a/doc/user/project/issues/multiple_assignees_for_issues.md b/doc/user/project/issues/multiple_assignees_for_issues.md
index 8781ebdd5b0..d1db0790d69 100644
--- a/doc/user/project/issues/multiple_assignees_for_issues.md
+++ b/doc/user/project/issues/multiple_assignees_for_issues.md
@@ -22,7 +22,7 @@ Consider a team formed by frontend developers, backend developers,
UX designers, QA testers, and a product manager working together to bring an idea to
market.
-Multiple Assignees for Issues makes collaboration smother,
+Multiple Assignees for Issues makes collaboration smoother,
and allows shared responsibilities to be clearly displayed.
All assignees are shown across your team's workflows and receive notifications (as they
would as single assignees), simplifying communication and ownership.
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 52b6b56af84..2e9db949890 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -3,7 +3,7 @@
> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only).
NOTE: **Note:**
-If you are running a self-managed instance, the new interface shown on
+Prior to 12.0, if you are running a self-managed instance, the new interface shown on
this page will not be available unless the feature flag
`approval_rules` is enabled, which can be done from the Rails console by
instance administrators.
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 65eb9bfb87e..80913f4ca07 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -96,17 +96,27 @@ module API
end
end
optional :start_branch, type: String, desc: 'Name of the branch to start the new commit from'
+ optional :start_project, types: [Integer, String], desc: 'The ID or path of the project to start the commit from'
optional :author_email, type: String, desc: 'Author email for commit'
optional :author_name, type: String, desc: 'Author name for commit'
optional :stats, type: Boolean, default: true, desc: 'Include commit stats'
optional :force, type: Boolean, default: false, desc: 'When `true` overwrites the target branch with a new commit based on the `start_branch`'
end
post ':id/repository/commits' do
+ if params[:start_project]
+ start_project = find_project!(params[:start_project])
+
+ unless user_project.forked_from?(start_project)
+ forbidden!("Project is not included in the fork network for #{start_project.full_name}")
+ end
+ end
+
authorize_push_to_branch!(params[:branch])
attrs = declared_params
attrs[:branch_name] = attrs.delete(:branch)
attrs[:start_branch] ||= attrs[:branch_name]
+ attrs[:start_project] = start_project if start_project
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 5928ee1657b..693172b7d08 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -206,7 +206,7 @@ module API
delete_note(noteable, params[:note_id])
end
- if Noteable::RESOLVABLE_TYPES.include?(noteable_type.to_s)
+ if Noteable.resolvable_types.include?(noteable_type.to_s)
desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4e99fec02dd..b1b6e7bd7b9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -699,7 +699,7 @@ module API
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
# information.
expose :merge_status do |merge_request|
- merge_request.check_if_can_be_merged
+ merge_request.check_mergeability
merge_request.merge_status
end
expose :diff_head_sha, as: :sha
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index ce85772e4ed..5bbf6df78b0 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -386,9 +386,8 @@ module API
)
if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
+ AutoMergeService.new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
::MergeRequests::MergeService
.new(merge_request.target_project, current_user, merge_params)
@@ -398,28 +397,16 @@ module API
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- desc 'Merge a merge request to its default temporary merge ref path'
- params do
- optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
- end
- put ':id/merge_requests/:merge_request_iid/merge_to_ref' do
+ desc 'Returns the up to date merge-ref HEAD commit'
+ get ':id/merge_requests/:merge_request_iid/merge_ref' do
merge_request = find_project_merge_request(params[:merge_request_iid])
- authorize! :admin_merge_request, user_project
-
- merge_params = {
- commit_message: params[:merge_commit_message]
- }
-
- result = ::MergeRequests::MergeToRefService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
+ result = ::MergeRequests::MergeabilityCheckService.new(merge_request).execute
- if result[:status] == :success
- present result.slice(:commit_id), 200
+ if result.success?
+ present :commit_id, result.payload.dig(:merge_ref_head, :commit_id)
else
- http_status = result[:http_status] || 400
- render_api_error!(result[:message], http_status)
+ render_api_error!(result.message, 400)
end
end
@@ -429,11 +416,9 @@ module API
post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
merge_request = find_project_merge_request(params[:merge_request_iid])
- unauthorized! unless merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
+ unauthorized! unless merge_request.can_cancel_auto_merge?(current_user)
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
+ AutoMergeService.new(merge_request.target_project, current_user).cancel(merge_request)
end
desc 'Rebase the merge request against its target branch' do
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index f4cc8beeb52..77b5053f38c 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -4,6 +4,8 @@ module Banzai
module Filter
class WikiLinkFilter < HTML::Pipeline::Filter
class Rewriter
+ UNSAFE_SLUG_REGEXES = [/\Ajavascript:/i].freeze
+
def initialize(link_string, wiki:, slug:)
@uri = Addressable::URI.parse(link_string)
@wiki_base_path = wiki && wiki.wiki_base_path
@@ -35,6 +37,8 @@ module Banzai
# Of the form `./link`, `../link`, or similar
def apply_hierarchical_link_rules!
+ return if slug_considered_unsafe?
+
@uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.'
end
@@ -54,6 +58,10 @@ module Banzai
def repository_upload?
@uri.relative? && @uri.path.starts_with?(Wikis::CreateAttachmentService::ATTACHMENT_PATH)
end
+
+ def slug_considered_unsafe?
+ UNSAFE_SLUG_REGEXES.any? { |r| r.match?(@slug) }
+ end
end
end
end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index 7db5f5e1f7d..c2da7fec7cc 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -70,8 +70,11 @@ module Banzai
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
- if link_reference == 'true' && href = original_content
- %(<a href="#{href}">#{href}</a>)
+ if link_reference == 'true'
+ href = node.attr('href')
+ content = original_content
+
+ %(<a href="#{href}">#{content}</a>)
end
# The reference should be replaced by the original link's content,
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 3f107fbbf3b..ccaf06c5d6a 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -40,6 +40,7 @@ module Gitlab
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze
VERSION = File.read(root.join("VERSION")).strip.freeze
INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
+ HTTP_PROXY_ENV_VARS = %w(http_proxy https_proxy HTTP_PROXY HTTPS_PROXY).freeze
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
@@ -66,6 +67,10 @@ module Gitlab
end
end
+ def self.http_proxy_env?
+ HTTP_PROXY_ENV_VARS.any? { |name| ENV[name] }
+ end
+
def self.process_name
return 'sidekiq' if Sidekiq.server?
return 'console' if defined?(Rails::Console)
diff --git a/lib/gitlab/background_migration/reset_merge_status.rb b/lib/gitlab/background_migration/reset_merge_status.rb
new file mode 100644
index 00000000000..447fec8903c
--- /dev/null
+++ b/lib/gitlab/background_migration/reset_merge_status.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Updates the range of given MRs to merge_status "unchecked", if they're opened
+ # and mergeable.
+ class ResetMergeStatus
+ def perform(from_id, to_id)
+ relation = MergeRequest.where(id: from_id..to_id,
+ state: 'opened',
+ merge_status: 'can_be_merged')
+
+ relation.update_all(merge_status: 'unchecked')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 0beeb44c272..21c42857895 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -24,7 +24,7 @@ module Gitlab
end
entry :ports, Entry::Ports,
- description: 'Ports used expose the image'
+ description: 'Ports used to expose the image'
attributes :ports
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 084fa4047a4..8d16371e857 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -24,6 +24,9 @@ module Gitlab
validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
end
+ entry :ports, Entry::Ports,
+ description: 'Ports used to expose the service'
+
def alias
value[:alias]
end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 87cd62ef2d4..32ab216dd55 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -51,7 +51,7 @@ variables:
POSTGRES_VERSION: 9.6.2
KUBERNETES_VERSION: 1.11.10
- HELM_VERSION: 2.13.1
+ HELM_VERSION: 2.14.0
DOCKER_DRIVER: overlay2
@@ -89,4 +89,4 @@ include:
dast:
except:
refs:
- - master \ No newline at end of file
+ - master
diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
index c9838c7a7ff..08dc74e041a 100644
--- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
@@ -1,18 +1,14 @@
----
+# This file is a template, and might need editing before it works on your project.
+
# Build JAVA applications using Apache Maven (http://maven.apache.org)
# For docker image tags see https://hub.docker.com/_/maven/
#
# For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
-#
-# This template will build and test your projects as well as create the documentation.
-#
+
+# This template will build and test your projects
# * Caches downloaded dependencies and plugins between invocation.
# * Verify but don't deploy merge requests.
# * Deploy built artifacts from master branch only.
-# * Shows how to use multiple jobs in test stage for verifying functionality
-# with multiple JDKs.
-# * Uses site:stage to collect the documentation for multi-module projects.
-# * Publishes the documentation for `master` branch.
variables:
# This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
@@ -23,78 +19,38 @@ variables:
# `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
+# This template uses jdk8 for verifying and deploying images
+image: maven:3.3.9-jdk-8
+
# Cache downloaded dependencies and plugins between builds.
# To keep cache across branches add 'key: "$CI_JOB_NAME"'
cache:
paths:
- .m2/repository
-# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
-# Because some enforcer rules might check dependency convergence and class duplications
-# we use `test-compile` here instead of `validate`, so the correct classpath is picked up.
-.validate: &validate
- stage: build
- script:
- - 'mvn $MAVEN_CLI_OPTS test-compile'
-
# For merge requests do not `deploy` but only run `verify`.
# See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
.verify: &verify
stage: test
script:
- - 'mvn $MAVEN_CLI_OPTS verify site site:stage'
+ - 'mvn $MAVEN_CLI_OPTS verify'
except:
- master
-# Validate merge requests using JDK7
-validate:jdk7:
- <<: *validate
- image: maven:3.3.9-jdk-7
-
-# Validate merge requests using JDK8
-validate:jdk8:
- <<: *validate
- image: maven:3.3.9-jdk-8
-
-# Verify merge requests using JDK7
-verify:jdk7:
- <<: *verify
- image: maven:3.3.9-jdk-7
-
# Verify merge requests using JDK8
verify:jdk8:
<<: *verify
- image: maven:3.3.9-jdk-8
+# To deploy packages from CI, create a ci_settings.xml file
+# For deploying packages to GitLab's Maven Repository: See https://gitlab.com/help/user/project/packages/maven_repository.md#creating-maven-packages-with-gitlab-cicd for more details.
+# Please note: The GitLab Maven Repository is currently only available in GitLab Premium / Ultimate.
# For `master` branch run `mvn deploy` automatically.
-# Here you need to decide whether you want to use JDK7 or 8.
-# To get this working you need to define a volume while configuring your gitlab-ci-multi-runner.
-# Mount your `settings.xml` as `/root/.m2/settings.xml` which holds your secrets.
-# See https://maven.apache.org/settings.html
deploy:jdk8:
- # Use stage test here, so the pages job may later pickup the created site.
- stage: test
- script:
- - 'mvn $MAVEN_CLI_OPTS deploy site site:stage'
- only:
- - master
- # Archive up the built documentation site.
- artifacts:
- paths:
- - target/staging
- image: maven:3.3.9-jdk-8
-
-pages:
- image: busybox:latest
stage: deploy
script:
- # Because Maven appends the artifactId automatically to the staging path if you did define a parent pom,
- # you might need to use `mv target/staging/YOUR_ARTIFACT_ID public` instead.
- - mv target/staging public
- dependencies:
- - deploy:jdk8
- artifacts:
- paths:
- - public
+ - if [ ! -f ci_settings.xml ];
+ then echo "CI settings missing\! If deploying to GitLab Maven Repository, please see https://gitlab.com/help/user/project/packages/maven_repository.md#creating-maven-packages-with-gitlab-cicd for instructions.";
+ fi
+ - 'mvn $MAVEN_CLI_OPTS deploy -s ci_settings.xml'
only:
- master
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 455588f3c66..dcdd3581d92 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -34,34 +34,6 @@ module Gitlab
TagExistsError = Class.new(StandardError)
ChecksumError = Class.new(StandardError)
- class << self
- def create_hooks(repo_path, global_hooks_path)
- local_hooks_path = File.join(repo_path, 'hooks')
- real_local_hooks_path = :not_found
-
- begin
- real_local_hooks_path = File.realpath(local_hooks_path)
- rescue Errno::ENOENT
- # real_local_hooks_path == :not_found
- end
-
- # Do nothing if hooks already exist
- unless real_local_hooks_path == File.realpath(global_hooks_path)
- if File.exist?(local_hooks_path)
- # Move the existing hooks somewhere safe
- FileUtils.mv(
- local_hooks_path,
- "#{local_hooks_path}.old.#{Time.now.to_i}")
- end
-
- # Create the hooks symlink
- FileUtils.ln_sf(global_hooks_path, local_hooks_path)
- end
-
- true
- end
- end
-
# Directory name of repo
attr_reader :name
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 3f13ebeb9d0..dfff6823689 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,12 +5,15 @@
module Gitlab
module GitRefValidator
extend self
+
+ EXPANDED_PREFIXES = %w[refs/heads/ refs/remotes/].freeze
+ DISALLOWED_PREFIXES = %w[-].freeze
+
# Validates a given name against the git reference specification
#
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
- not_allowed_prefixes = %w(refs/heads/ refs/remotes/ -)
- return false if ref_name.start_with?(*not_allowed_prefixes)
+ return false if ref_name.start_with?(*(EXPANDED_PREFIXES + DISALLOWED_PREFIXES))
return false if ref_name == 'HEAD'
begin
@@ -19,5 +22,21 @@ module Gitlab
return false
end
end
+
+ def validate_merge_request_branch(ref_name)
+ return false if ref_name.start_with?(*DISALLOWED_PREFIXES)
+
+ expanded_name = if ref_name.start_with?(*EXPANDED_PREFIXES)
+ ref_name
+ else
+ "refs/heads/#{ref_name}"
+ end
+
+ begin
+ Rugged::Reference.valid_name?(expanded_name)
+ rescue ArgumentError
+ return false
+ end
+ end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 05e06eec012..e683d4e5bbe 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -33,7 +33,7 @@ module Gitlab
SERVER_FEATURE_CATFILE_CACHE = 'catfile-cache'.freeze
# Server feature flags should use '_' to separate words.
- SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE, 'delta_islands'].freeze
+ SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE].freeze
MUTEX = Mutex.new
diff --git a/lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb b/lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb
new file mode 100644
index 00000000000..5e151f4dbd7
--- /dev/null
+++ b/lib/gitlab/graphql/loaders/batch_project_statistics_loader.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Loaders
+ class BatchProjectStatisticsLoader
+ attr_reader :project_id
+
+ def initialize(project_id)
+ @project_id = project_id
+ end
+
+ def find
+ BatchLoader.for(project_id).batch do |project_ids, loader|
+ ProjectStatistics.for_project_ids(project_ids).each do |statistics|
+ loader.call(statistics.project_id, statistics)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index 313b5df51d4..db2b4dde244 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -18,7 +18,7 @@ module Gitlab
include HTTParty # rubocop:disable Gitlab/HTTParty
- connection_adapter ProxyHTTPConnectionAdapter
+ connection_adapter HTTPConnectionAdapter
def self.perform_request(http_method, path, options, &block)
super
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/http_connection_adapter.rb
index a64cb47e77e..41eab3658bc 100644
--- a/lib/gitlab/proxy_http_connection_adapter.rb
+++ b/lib/gitlab/http_connection_adapter.rb
@@ -10,17 +10,19 @@
#
# This option will take precedence over the global setting.
module Gitlab
- class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
+ class HTTPConnectionAdapter < HTTParty::ConnectionAdapter
def connection
- unless allow_local_requests?
- begin
- Gitlab::UrlBlocker.validate!(uri, allow_local_network: false)
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
- raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
- end
+ begin
+ @uri, hostname = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_local_requests?,
+ allow_localhost: allow_local_requests?,
+ dns_rebind_protection: dns_rebind_protection?)
+ rescue Gitlab::UrlBlocker::BlockedUrlError => e
+ raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
end
- super
+ super.tap do |http|
+ http.hostname_override = hostname if hostname
+ end
end
private
@@ -29,6 +31,12 @@ module Gitlab
options.fetch(:allow_local_requests, allow_settings_local_requests?)
end
+ def dns_rebind_protection?
+ return false if Gitlab.http_proxy_env?
+
+ Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
+ end
+
def allow_settings_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services?
end
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
index 93b37b7bc5f..c28a1674018 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -4,6 +4,7 @@ module Gitlab
module ImportExport
class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
+ PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_html\Z/).freeze
def self.clean(*args)
new(*args).clean
@@ -24,7 +25,11 @@ module Gitlab
private
def prohibited_key?(key)
- key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
+ key =~ PROHIBITED_REFERENCES && !permitted_key?(key)
+ end
+
+ def permitted_key?(key)
+ ALLOWED_REFERENCES.include?(key)
end
def excluded_key?(key)
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index 87669b253bc..25e40c70230 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -51,10 +51,11 @@ module Gitlab
set_master_metrics(stats)
stats['worker_status'].each do |worker|
+ last_status = worker['last_status']
labels = { worker: "worker_#{worker['index']}" }
metrics[:puma_phase].set(labels, worker['phase'])
- set_worker_metrics(worker['last_status'], labels)
+ set_worker_metrics(last_status, labels) if last_status.present?
end
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 18a69321905..4d9c43f37e7 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -23,25 +23,32 @@ module Gitlab
end
def init_metrics
- metrics = {}
- metrics[:sampler_duration] = ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels)
- metrics[:total_time] = ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
+ metrics = {
+ file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum),
+ memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum),
+ process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
+ process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
+ process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels, :livesum),
+ process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
+ sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
+ total_time: ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
+ }
+
GC.stat.keys.each do |key|
metrics[key] = ::Gitlab::Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum)
end
- metrics[:memory_usage] = ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum)
- metrics[:file_descriptors] = ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum)
-
metrics
end
def sample
start_time = System.monotonic_time
- metrics[:memory_usage].set(labels.merge(worker_label), System.memory_usage)
metrics[:file_descriptors].set(labels.merge(worker_label), System.file_descriptor_count)
-
+ metrics[:process_cpu_seconds_total].set(labels.merge(worker_label), ::Gitlab::Metrics::System.cpu_time)
+ metrics[:process_max_fds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.max_open_file_descriptors)
+ metrics[:process_start_time_seconds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.process_start_time)
+ set_memory_usage_metrics
sample_gc
metrics[:sampler_duration].increment(labels, System.monotonic_time - start_time)
@@ -61,6 +68,14 @@ module Gitlab
metrics[:total_time].increment(labels, GC::Profiler.total_time)
end
+ def set_memory_usage_metrics
+ memory_usage = System.memory_usage
+ memory_labels = labels.merge(worker_label)
+
+ metrics[:memory_bytes].set(memory_labels, memory_usage)
+ metrics[:process_resident_memory_bytes].set(memory_labels, memory_usage)
+ end
+
def worker_label
return {} unless defined?(Unicorn::Worker)
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index bec64e864b3..9af7e0afed4 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -4,16 +4,16 @@ module Gitlab
module Metrics
module Samplers
class UnicornSampler < BaseSampler
- def initialize(interval)
- super(interval)
+ def metrics
+ @metrics ||= init_metrics
end
- def unicorn_active_connections
- @unicorn_active_connections ||= ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
- end
-
- def unicorn_queued_connections
- @unicorn_queued_connections ||= ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
+ def init_metrics
+ {
+ unicorn_active_connections: ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max),
+ unicorn_queued_connections: ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max),
+ unicorn_workers: ::Gitlab::Metrics.gauge(:unicorn_workers, 'Unicorn workers')
+ }
end
def enabled?
@@ -23,14 +23,13 @@ module Gitlab
def sample
Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- unicorn_active_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.active)
- unicorn_queued_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.queued)
+ set_unicorn_connection_metrics('tcp', addr, stats)
end
-
Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- unicorn_active_connections.set({ socket_type: 'unix', socket_address: addr }, stats.active)
- unicorn_queued_connections.set({ socket_type: 'unix', socket_address: addr }, stats.queued)
+ set_unicorn_connection_metrics('unix', addr, stats)
end
+
+ metrics[:unicorn_workers].set({}, unicorn_workers_count)
end
private
@@ -39,6 +38,13 @@ module Gitlab
@tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
end
+ def set_unicorn_connection_metrics(type, addr, stats)
+ labels = { socket_type: type, socket_address: addr }
+
+ metrics[:unicorn_active_connections].set(labels, stats.active)
+ metrics[:unicorn_queued_connections].set(labels, stats.queued)
+ end
+
def unix_listeners
@unix_listeners ||= Unicorn.listener_names - tcp_listeners
end
@@ -46,6 +52,10 @@ module Gitlab
def unicorn_with_listeners?
defined?(Unicorn) && Unicorn.listener_names.any?
end
+
+ def unicorn_workers_count
+ `pgrep -f '[u]nicorn_rails worker.+ #{Rails.root.to_s}'`.split.count
+ end
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 426496855e3..33c0de91c11 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -23,6 +23,22 @@ module Gitlab
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
+
+ def self.max_open_file_descriptors
+ match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
+
+ return unless match && match[1]
+
+ match[1].to_i
+ end
+
+ def self.process_start_time
+ fields = File.read('/proc/self/stat').split
+
+ # fields[21] is linux proc stat field "(22) starttime".
+ # The value is expressed in clock ticks, divide by clock ticks for seconds.
+ ( fields[21].to_i || 0 ) / clk_tck
+ end
else
def self.memory_usage
0.0
@@ -31,6 +47,14 @@ module Gitlab
def self.file_descriptor_count
0
end
+
+ def self.max_open_file_descriptors
+ 0
+ end
+
+ def self.process_start_time
+ 0
+ end
end
# THREAD_CPUTIME is not supported on OS X
@@ -59,6 +83,10 @@ module Gitlab
def self.monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
end
+
+ def self.clk_tck
+ @clk_tck ||= `getconf CLK_TCK`.to_i
+ end
end
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 78337518988..0f3b97e2317 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -138,6 +138,12 @@ module Gitlab
project
end
+ def filter_milestones_by_project(milestones)
+ return Milestone.none unless Ability.allowed?(@current_user, :read_milestone, @project)
+
+ milestones.where(project_id: project.id) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
def repository_project_ref
@repository_project_ref ||= repository_ref || project.default_branch
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 4a097a00101..7c1e6b1baff 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -103,9 +103,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def milestones
- milestones = Milestone.where(project_id: project_ids_relation)
- milestones = milestones.search(query)
- milestones.reorder('milestones.updated_at DESC')
+ milestones = Milestone.search(query)
+
+ milestones = filter_milestones_by_project(milestones)
+
+ milestones.reorder('updated_at DESC')
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -123,6 +125,26 @@ module Gitlab
'projects'
end
+ # Filter milestones by authorized projects.
+ # For performance reasons project_id is being plucked
+ # to be used on a smaller query.
+ #
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_milestones_by_project(milestones)
+ project_ids =
+ milestones.where(project_id: project_ids_relation)
+ .select(:project_id).distinct
+ .pluck(:project_id)
+
+ return Milestone.none if project_ids.nil?
+
+ authorized_project_ids_relation =
+ Project.where(id: project_ids).ids_with_milestone_available_for(current_user)
+
+ milestones.where(project_id: authorized_project_ids_relation)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
# rubocop: disable CodeReuse/ActiveRecord
def project_ids_relation
limit_projects.select(:id).reorder(nil)
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 641ba70ef83..9a8df719827 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -8,38 +8,68 @@ module Gitlab
BlockedUrlError = Class.new(StandardError)
class << self
- def validate!(url, ports: [], schemes: [], allow_localhost: false, allow_local_network: true, ascii_only: false, enforce_user: false, enforce_sanitization: false)
- return true if url.nil?
+ # Validates the given url according to the constraints specified by arguments.
+ #
+ # ports - Raises error if the given URL port does is not between given ports.
+ # allow_localhost - Raises error if URL resolves to a localhost IP address and argument is true.
+ # allow_local_network - Raises error if URL resolves to a link-local address and argument is true.
+ # ascii_only - Raises error if URL has unicode characters and argument is true.
+ # enforce_user - Raises error if URL user doesn't start with alphanumeric characters and argument is true.
+ # enforce_sanitization - Raises error if URL includes any HTML/CSS/JS tags and argument is true.
+ #
+ # Returns an array with [<uri>, <original-hostname>].
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/ParameterLists
+ def validate!(
+ url,
+ ports: [],
+ schemes: [],
+ allow_localhost: false,
+ allow_local_network: true,
+ ascii_only: false,
+ enforce_user: false,
+ enforce_sanitization: false,
+ dns_rebind_protection: true)
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/ParameterLists
+
+ return [nil, nil] if url.nil?
# Param url can be a string, URI or Addressable::URI
uri = parse_url(url)
validate_html_tags!(uri) if enforce_sanitization
- # Allow imports from the GitLab instance itself but only from the configured ports
- return true if internal?(uri)
-
+ hostname = uri.hostname
port = get_port(uri)
- validate_scheme!(uri.scheme, schemes)
- validate_port!(port, ports) if ports.any?
- validate_user!(uri.user) if enforce_user
- validate_hostname!(uri.hostname)
- validate_unicode_restriction!(uri) if ascii_only
+
+ unless internal?(uri)
+ validate_scheme!(uri.scheme, schemes)
+ validate_port!(port, ports) if ports.any?
+ validate_user!(uri.user) if enforce_user
+ validate_hostname!(hostname)
+ validate_unicode_restriction!(uri) if ascii_only
+ end
begin
- addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM).map do |addr|
+ addrs_info = Addrinfo.getaddrinfo(hostname, port, nil, :STREAM).map do |addr|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end
rescue SocketError
- return true
+ return [uri, nil]
end
+ protected_uri_with_hostname = enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
+
+ # Allow url from the GitLab instance itself but only for the configured hostname and ports
+ return protected_uri_with_hostname if internal?(uri)
+
validate_localhost!(addrs_info) unless allow_localhost
validate_loopback!(addrs_info) unless allow_localhost
validate_local_network!(addrs_info) unless allow_local_network
validate_link_local!(addrs_info) unless allow_local_network
- true
+ protected_uri_with_hostname
end
def blocked_url?(*args)
@@ -52,6 +82,25 @@ module Gitlab
private
+ # Returns the given URI with IP address as hostname and the original hostname respectively
+ # in an Array.
+ #
+ # It checks whether the resolved IP address matches with the hostname. If not, it changes
+ # the hostname to the resolved IP address.
+ #
+ # The original hostname is used to validate the SSL, given in that scenario
+ # we'll be making the request to the IP address, instead of using the hostname.
+ def enforce_uri_hostname(addrs_info, uri, hostname, dns_rebind_protection)
+ address = addrs_info.first
+ ip_address = address&.ip_address
+
+ return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != hostname
+
+ uri = uri.dup
+ uri.hostname = ip_address
+ [uri, hostname]
+ end
+
def get_port(uri)
uri.port || uri.default_port
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index 880712de5fe..215454fe63c 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -47,6 +47,10 @@ module Gitlab
@credentials ||= { user: @url.user.presence, password: @url.password.presence }
end
+ def user
+ credentials[:user]
+ end
+
def full_url
@full_url ||= generate_full_url.to_s
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index ee3ef9dad6e..487808a7baa 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -51,9 +51,6 @@ namespace :gitlab do
end
end
- # (Re)create hooks
- Rake::Task['gitlab:shell:create_hooks'].invoke
-
Gitlab::Shell.ensure_secret_token!
end
@@ -78,15 +75,6 @@ namespace :gitlab do
end
end
end
-
- desc 'Create or repair repository hooks symlink'
- task create_hooks: :gitlab_environment do
- warn_user_is_not_gitlab
-
- puts 'Creating/Repairing hooks symlinks for all repositories'
- system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
- puts 'done'.color(:green)
- end
end
def setup
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f737d75ca95..294c938d87d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2337,9 +2337,6 @@ msgstr ""
msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}."
msgstr ""
-msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}."
-msgstr ""
-
msgid "ClusterIntegration|Instance cluster"
msgstr ""
@@ -2373,6 +2370,9 @@ msgstr ""
msgid "ClusterIntegration|Knative Endpoint:"
msgstr ""
+msgid "ClusterIntegration|Knative domain name was updated successfully."
+msgstr ""
+
msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
msgstr ""
@@ -2547,6 +2547,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
+msgstr ""
+
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
msgstr ""
@@ -3850,6 +3853,9 @@ msgstr ""
msgid "Ends at (UTC)"
msgstr ""
+msgid "Enforce DNS rebinding attack protection"
+msgstr ""
+
msgid "Enter at least three characters to search"
msgstr ""
@@ -5147,7 +5153,7 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
-msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
msgid "ImageDiffViewer|2-up"
@@ -5438,6 +5444,9 @@ msgstr ""
msgid "Issue update failed"
msgstr ""
+msgid "Issue was closed by %{name} %{reason}"
+msgstr ""
+
msgid "IssueBoards|Board"
msgstr ""
@@ -5853,6 +5862,9 @@ msgstr ""
msgid "Live preview"
msgstr ""
+msgid "Loading functions timed out. Please reload the page to try again."
+msgstr ""
+
msgid "Loading the GitLab IDE..."
msgstr ""
@@ -6638,9 +6650,6 @@ msgstr ""
msgid "Not found."
msgstr ""
-msgid "Not implemented!"
-msgstr ""
-
msgid "Not now"
msgstr ""
@@ -6928,6 +6937,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Password (optional)"
+msgstr ""
+
msgid "Password authentication is unavailable."
msgstr ""
@@ -8475,6 +8487,9 @@ msgstr ""
msgid "Resolved by %{resolvedByName}"
msgstr ""
+msgid "Resolves IP addresses once and uses them to submit requests"
+msgstr ""
+
msgid "Response metrics (AWS ELB)"
msgstr ""
@@ -11155,6 +11170,9 @@ msgstr ""
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
msgstr ""
+msgid "Username (optional)"
+msgstr ""
+
msgid "Username is already taken."
msgstr ""
@@ -11976,6 +11994,9 @@ msgstr ""
msgid "commented on %{link_to_project}"
msgstr ""
+msgid "commit %{commit_id}"
+msgstr ""
+
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr ""
@@ -12237,6 +12258,9 @@ msgstr ""
msgid "mrWidget|Merge failed."
msgstr ""
+msgid "mrWidget|Merge failed: %{mergeError}. Please try again."
+msgstr ""
+
msgid "mrWidget|Merge locally"
msgstr ""
@@ -12501,6 +12525,12 @@ msgstr ""
msgid "verify ownership"
msgstr ""
+msgid "via %{closed_via}"
+msgstr ""
+
+msgid "via merge request %{link}"
+msgstr ""
+
msgid "view it on GitLab"
msgstr ""
diff --git a/package.json b/package.json
index b4e83475802..3599ce2c279 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"@babel/preset-env": "^7.4.4",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.63.0",
- "@gitlab/ui": "^3.10.1",
+ "@gitlab/ui": "^3.10.3",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-link": "^1.2.11",
@@ -146,7 +146,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
- "@gitlab/eslint-config": "^1.5.0",
+ "@gitlab/eslint-config": "^1.6.0",
"@vue/test-utils": "^1.0.0-beta.25",
"axios-mock-adapter": "^1.15.0",
"babel-jest": "^24.1.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index 64215b24cf1..12994b85322 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -10,3 +10,4 @@ gem 'airborne', '~> 0.2.13'
gem 'nokogiri', '~> 1.10.3'
gem 'rspec-retry', '~> 0.6.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
+gem 'knapsack', '~> 1.17'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index a06c88b6f0a..6b0635ed0e2 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -39,6 +39,8 @@ GEM
domain_name (~> 0.5)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
+ knapsack (1.17.1)
+ rake
launchy (2.4.3)
addressable (~> 2.3)
method_source (0.9.0)
@@ -102,6 +104,7 @@ DEPENDENCIES
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
faker (~> 1.6, >= 1.6.6)
+ knapsack (~> 1.17)
nokogiri (~> 1.10.3)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
diff --git a/qa/knapsack/gitlab-ce/review-qa-all_master_report.json b/qa/knapsack/gitlab-ce/review-qa-all_master_report.json
new file mode 100644
index 00000000000..f147346ba0f
--- /dev/null
+++ b/qa/knapsack/gitlab-ce/review-qa-all_master_report.json
@@ -0,0 +1,42 @@
+{
+ "qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb": 9.697327613830566,
+ "qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb": 46.54227638244629,
+ "qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb": 10.214765310287476,
+ "qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb": 7.882027864456177,
+ "qa/specs/features/api/3_create/repository/files_spec.rb": 5.015859127044678,
+ "qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb": 12.772682905197144,
+ "qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb": 29.76174831390381,
+ "qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb": 22.800872802734375,
+ "qa/specs/features/browser_ui/1_manage/login/register_spec.rb": 22.320587396621704,
+ "qa/specs/features/api/1_manage/users_spec.rb": 0.6089541912078857,
+ "qa/specs/features/browser_ui/3_create/repository/clone_spec.rb": 0.9618203639984131,
+ "qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb": 13.403101205825806,
+ "qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb": 8.810423135757446,
+ "qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb": 7.730542182922363,
+ "qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb": 16.18057894706726,
+ "qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb": 8.31815505027771,
+ "qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb": 9.48607873916626,
+ "qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb": 19.552733182907104,
+ "qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb": 17.273863554000854,
+ "qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb": 8.281434059143066,
+ "qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb": 18.047621726989746,
+ "qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb": 7.422840595245361,
+ "qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb": 3.438166856765747,
+ "qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb": 18.679633855819702,
+ "qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb": 27.943300485610962,
+ "qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb": 39.17585229873657,
+ "qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb": 40.09336972236633,
+ "qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb": 3.705310821533203,
+ "qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb": 5.812374591827393,
+ "qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb": 92.46774697303772,
+ "qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb": 100.28881478309631,
+ "qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb": 23.710937023162842,
+ "qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb": 20.58603596687317,
+ "qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb": 25.460349321365356,
+ "qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb": 19.459370374679565,
+ "qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb": 6.731764793395996,
+ "qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb": 15.342933893203735,
+ "qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb": 11.280649185180664,
+ "qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb": 57.48992609977722,
+ "qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb": 32.5517954826355
+} \ No newline at end of file
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 03cae3c1fe6..82510dfa03c 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -136,6 +136,10 @@ module QA
ENV['GITLAB_QA_PASSWORD_2']
end
+ def knapsack?
+ !!(ENV['KNAPSACK_GENERATE_REPORT'] || ENV['KNAPSACK_REPORT_PATH'] || ENV['KNAPSACK_TEST_FILE_PATTERN'])
+ end
+
def ldap_username
@ldap_username ||= ENV['GITLAB_LDAP_USERNAME']
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
index f97b0e56ca2..530fc684437 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/53
- context 'Plan', :quarantine do
+ context 'Plan' do
describe 'issue suggestions' do
let(:issue_title) { 'Issue Lists are awesome' }
@@ -10,12 +9,12 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- project = Resource::Project.fabricate! do |resource|
+ project = Resource::Project.fabricate_via_api! do |resource|
resource.name = 'project-for-issue-suggestions'
resource.description = 'project for issue suggestions'
end
- Resource::Issue.fabricate! do |issue|
+ Resource::Issue.fabricate_via_browser_ui! do |issue|
issue.title = issue_title
issue.project = project
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
index a544efb35ee..5bfafdfa041 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
@@ -26,6 +26,8 @@ module QA
Page::Main::Login.perform(&:sign_in_using_credentials)
set_file_size_limit('')
+
+ Page::Main::Menu.perform(&:sign_out)
end
it 'push successful when the file size is under the limit' do
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 306913dafa6..f1cb9378de8 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
require 'rspec/core'
+require 'rspec/expectations'
+require 'knapsack'
module QA
module Specs
@@ -32,10 +34,25 @@ module QA
end
args.push(options)
- args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
Runtime::Browser.configure!
+ if Runtime::Env.knapsack?
+ allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator
+
+ QA::Runtime::Logger.info ''
+ QA::Runtime::Logger.info 'Report specs:'
+ QA::Runtime::Logger.info allocator.report_node_tests.join(', ')
+ QA::Runtime::Logger.info ''
+ QA::Runtime::Logger.info 'Leftover specs:'
+ QA::Runtime::Logger.info allocator.leftover_node_tests.join(', ')
+ QA::Runtime::Logger.info ''
+
+ args.push(['--', allocator.node_tests])
+ else
+ args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
+ end
+
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 04085efe2ce..2560695ef2e 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -168,6 +168,30 @@ describe QA::Runtime::Env do
end
end
+ describe '.knapsack?' do
+ it 'returns true if KNAPSACK_GENERATE_REPORT is defined' do
+ stub_env('KNAPSACK_GENERATE_REPORT', 'true')
+
+ expect(described_class.knapsack?).to be_truthy
+ end
+
+ it 'returns true if KNAPSACK_REPORT_PATH is defined' do
+ stub_env('KNAPSACK_REPORT_PATH', '/a/path')
+
+ expect(described_class.knapsack?).to be_truthy
+ end
+
+ it 'returns true if KNAPSACK_TEST_FILE_PATTERN is defined' do
+ stub_env('KNAPSACK_TEST_FILE_PATTERN', '/a/**/pattern')
+
+ expect(described_class.knapsack?).to be_truthy
+ end
+
+ it 'returns false if neither KNAPSACK_GENERATE_REPORT nor KNAPSACK_REPORT_PATH nor KNAPSACK_TEST_FILE_PATTERN are defined' do
+ expect(described_class.knapsack?).to be_falsey
+ end
+ end
+
describe '.require_github_access_token!' do
it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do
stub_env('GITHUB_ACCESS_TOKEN', nil)
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index a368ffba711..f25dbf3a8ab 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -3,6 +3,11 @@
require_relative '../qa'
require 'rspec/retry'
+if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK']
+ require 'knapsack'
+ Knapsack::Adapters::RSpecAdapter.bind
+end
+
%w[helpers shared_examples].each do |d|
Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f }
end
diff --git a/spec/controllers/concerns/import_url_params_spec.rb b/spec/controllers/concerns/import_url_params_spec.rb
new file mode 100644
index 00000000000..adbe6e5d3bf
--- /dev/null
+++ b/spec/controllers/concerns/import_url_params_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ImportUrlParams do
+ let(:import_url_params) do
+ controller = OpenStruct.new(params: params).extend(described_class)
+ controller.import_url_params
+ end
+
+ context 'empty URL' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ title: 'Test'
+ })
+ end
+
+ it 'returns empty hash' do
+ expect(import_url_params).to eq({})
+ end
+ end
+
+ context 'url and password separately provided' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ import_url: 'https://url.com',
+ import_url_user: 'user', import_url_password: 'password'
+ })
+ end
+
+ describe '#import_url_params' do
+ it 'returns hash with import_url' do
+ expect(import_url_params).to eq(
+ import_url: "https://user:password@url.com"
+ )
+ end
+ end
+ end
+
+ context 'url with provided empty credentials' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ import_url: 'https://user:password@url.com',
+ import_url_user: '', import_url_password: ''
+ })
+ end
+
+ describe '#import_url_params' do
+ it 'does not change the url' do
+ expect(import_url_params).to eq(
+ import_url: "https://user:password@url.com"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index 323a32575af..cc6ac83ca38 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Projects::Ci::LintsController do
+ include StubRequests
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -70,7 +72,7 @@ describe Projects::Ci::LintsController do
context 'with a valid gitlab-ci.yml' do
before do
- WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
+ stub_full_request(remote_file_path).to_return(body: remote_file_content)
project.add_developer(user)
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 8d88ee7dfd6..bdc81efe3bc 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -122,4 +122,19 @@ describe Projects::ImportsController do
end
end
end
+
+ describe 'POST #create' do
+ let(:params) { { import_url: 'https://github.com/vim/vim.git', import_url_user: 'user', import_url_password: 'password' } }
+ let(:project) { create(:project) }
+
+ before do
+ allow(RepositoryImportWorker).to receive(:perform_async)
+
+ post :create, params: { project: params, namespace_id: project.namespace.to_param, project_id: project }
+ end
+
+ it 'sets import_url to the project' do
+ expect(project.reload.import_url).to eq('https://user:password@github.com/vim/vim.git')
+ end
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index f4a18dcba51..f8c0ab55eb4 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -429,8 +429,9 @@ describe Projects::MergeRequestsController do
it 'sets the MR to merge when the pipeline succeeds' do
service = double(:merge_when_pipeline_succeeds_service)
+ allow(service).to receive(:available_for?) { true }
- expect(MergeRequests::MergeWhenPipelineSucceedsService)
+ expect(AutoMerge::MergeWhenPipelineSucceedsService)
.to receive(:new).with(project, anything, anything)
.and_return(service)
expect(service).to receive(:execute).with(merge_request)
@@ -713,9 +714,9 @@ describe Projects::MergeRequestsController do
end
end
- describe 'POST cancel_merge_when_pipeline_succeeds' do
+ describe 'POST cancel_auto_merge' do
subject do
- post :cancel_merge_when_pipeline_succeeds,
+ post :cancel_auto_merge,
params: {
format: :json,
namespace_id: merge_request.project.namespace.to_param,
@@ -725,14 +726,15 @@ describe Projects::MergeRequestsController do
xhr: true
end
- it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
- mwps_service = double
+ it 'calls AutoMergeService' do
+ auto_merge_service = double
- allow(MergeRequests::MergeWhenPipelineSucceedsService)
+ allow(AutoMergeService)
.to receive(:new)
- .and_return(mwps_service)
+ .and_return(auto_merge_service)
- expect(mwps_service).to receive(:cancel).with(merge_request)
+ allow(auto_merge_service).to receive(:available_strategies).with(merge_request)
+ expect(auto_merge_service).to receive(:cancel).with(merge_request)
subject
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index f8470a94f98..767cee7d54a 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -175,6 +175,40 @@ describe Projects::MilestonesController do
end
end
+ describe '#labels' do
+ render_views
+
+ context 'as json' do
+ let!(:guest) { create(:user, username: 'guest1') }
+ let!(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :public, group: group) }
+ let!(:label) { create(:label, title: 'test_label_on_private_issue', project: project) }
+ let!(:confidential_issue) { create(:labeled_issue, confidential: true, project: project, milestone: milestone, labels: [label]) }
+
+ it 'does not render labels of private issues if user has no access' do
+ sign_in(guest)
+
+ get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+
+ expect(json_response['html']).not_to include(label.title)
+ end
+
+ it 'does render labels of private issues if user has access' do
+ sign_in(user)
+
+ get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+
+ expect(json_response['html']).to include(label.title)
+ end
+ end
+ end
+
context 'promotion succeeds' do
before do
group.add_developer(user)
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 782f5f272d9..18c594acae0 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:service) { cluster.platform_kubernetes }
- let(:project) { cluster.project}
+ let(:project) { cluster.project }
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -30,17 +29,69 @@ describe Projects::Serverless::FunctionsController do
end
describe 'GET #index' do
- context 'empty cache' do
- it 'has no data' do
+ let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
+
+ context 'when cache is being read' do
+ let(:knative_state) { 'checking' }
+ let(:functions) { [] }
+
+ before do
get :index, params: params({ format: :json })
+ end
- expect(response).to have_gitlab_http_status(204)
+ it 'returns checking' do
+ expect(json_response).to eq expected_json
end
- it 'renders an html page' do
- get :index, params: params
+ it { expect(response).to have_gitlab_http_status(200) }
+ end
+
+ context 'when cache is ready' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+ let(:knative_state) { true }
- expect(response).to have_gitlab_http_status(200)
+ before do
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ stub_kubeclient_service_pods(
+ kube_response({ "kind" => "PodList", "items" => [] }),
+ namespace: namespace.namespace
+ )
+ end
+
+ context 'when no functions were found' do
+ let(:functions) { [] }
+
+ before do
+ stub_kubeclient_knative_services(
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ )
+ get :index, params: params({ format: :json })
+ end
+
+ it 'returns checking' do
+ expect(json_response).to eq expected_json
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
+ end
+
+ context 'when functions were found' do
+ let(:functions) { ["asdf"] }
+
+ before do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ get :index, params: params({ format: :json })
+ end
+
+ it 'returns functions' do
+ expect(json_response["functions"]).not_to be_empty
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
end
end
end
@@ -56,11 +107,12 @@ describe Projects::Serverless::FunctionsController do
context 'valid data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
end
it 'has a valid function name' do
@@ -88,11 +140,12 @@ describe Projects::Serverless::FunctionsController do
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
end
it 'has data' do
@@ -100,11 +153,16 @@ describe Projects::Serverless::FunctionsController do
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to contain_exactly(
- a_hash_including(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
- )
+ expect(json_response).to match(
+ {
+ "knative_installed" => "checking",
+ "functions" => [
+ a_hash_including(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ )
+ ]
+ }
)
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index db53e5bc8a4..b91a4df40a5 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -109,7 +109,7 @@ describe Projects::Settings::CiCdController do
end
context 'when updating the auto_devops settings' do
- let(:params) { { auto_devops_attributes: { enabled: '', domain: 'mepmep.md' } } }
+ let(:params) { { auto_devops_attributes: { enabled: '' } } }
context 'following the instance default' do
let(:params) { { auto_devops_attributes: { enabled: '' } } }
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 2b9df71aa3a..89857a9d21b 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -4,15 +4,31 @@ require 'rails_helper'
describe SentNotificationsController do
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:sent_notification) { create(:sent_notification, project: project, noteable: issue, recipient: user) }
+ let(:project) { create(:project, :public) }
+ let(:private_project) { create(:project, :private) }
+ let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: user) }
let(:issue) do
- create(:issue, project: project, author: user) do |issue|
- issue.subscriptions.create(user: user, project: project, subscribed: true)
+ create(:issue, project: target_project) do |issue|
+ issue.subscriptions.create(user: user, project: target_project, subscribed: true)
end
end
+ let(:confidential_issue) do
+ create(:issue, project: target_project, confidential: true) do |issue|
+ issue.subscriptions.create(user: user, project: target_project, subscribed: true)
+ end
+ end
+
+ let(:merge_request) do
+ create(:merge_request, source_project: target_project, target_project: target_project) do |mr|
+ mr.subscriptions.create(user: user, project: target_project, subscribed: true)
+ end
+ end
+
+ let(:noteable) { issue }
+ let(:target_project) { project }
+
describe 'GET unsubscribe' do
context 'when the user is not logged in' do
context 'when the force param is passed' do
@@ -34,20 +50,93 @@ describe SentNotificationsController do
end
context 'when the force param is not passed' do
+ render_views
+
before do
get(:unsubscribe, params: { id: sent_notification.reply_key })
end
- it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user, project)).to be_truthy
+ shared_examples 'unsubscribing as anonymous' do
+ it 'does not unsubscribe the user' do
+ expect(noteable.subscribed?(user, target_project)).to be_truthy
+ end
+
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'renders unsubscribe page' do
+ expect(response.status).to eq(200)
+ expect(response).to render_template :unsubscribe
+ end
end
- it 'does not set the flash message' do
- expect(controller).not_to set_flash[:notice]
+ context 'when project is public' do
+ context 'when unsubscribing from issue' do
+ let(:noteable) { issue }
+
+ it 'shows issue title' do
+ expect(response.body).to include(issue.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from confidential issue' do
+ let(:noteable) { confidential_issue }
+
+ it 'does not show issue title' do
+ expect(response.body).not_to include(confidential_issue.title)
+ expect(response.body).to include(confidential_issue.to_reference)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from merge request' do
+ let(:noteable) { merge_request }
+
+ it 'shows merge request title' do
+ expect(response.body).to include(merge_request.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
end
- it 'redirects to the login page' do
- expect(response).to render_template :unsubscribe
+ context 'when project is not public' do
+ let(:target_project) { private_project }
+
+ context 'when unsubscribing from issue' do
+ let(:noteable) { issue }
+
+ it 'shows issue title' do
+ expect(response.body).not_to include(issue.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from confidential issue' do
+ let(:noteable) { confidential_issue }
+
+ it 'does not show issue title' do
+ expect(response.body).not_to include(confidential_issue.title)
+ expect(response.body).to include(confidential_issue.to_reference)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from merge request' do
+ let(:noteable) { merge_request }
+
+ it 'shows merge request title' do
+ expect(response.body).not_to include(merge_request.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
end
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 6bcff7f975c..9c4ddce5409 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -58,7 +58,26 @@ describe SessionsController do
it 'authenticates user correctly' do
post(:create, params: { user: user_params })
- expect(subject.current_user). to eq user
+ expect(subject.current_user).to eq user
+ end
+
+ context 'with password authentication disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'does not sign in the user' do
+ post(:create, params: { user: user_params })
+
+ expect(@request.env['warden']).not_to be_authenticated
+ expect(subject.current_user).to be_nil
+ end
+
+ it 'returns status 403' do
+ post(:create, params: { user: user_params })
+
+ expect(response.status).to eq 403
+ end
end
it 'creates an audit log record' do
@@ -153,6 +172,19 @@ describe SessionsController do
end
end
+ context 'with password authentication disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'allows 2FA stage of non-password login' do
+ authenticate_2fa(otp_attempt: user.current_otp)
+
+ expect(@request.env['warden']).to be_authenticated
+ expect(subject.current_user).to eq user
+ end
+ end
+
##
# See #14900 issue
#
diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb
index b2b79807429..4b83ba2ac1b 100644
--- a/spec/factories/ci/pipeline_schedule.rb
+++ b/spec/factories/ci/pipeline_schedule.rb
@@ -7,6 +7,16 @@ FactoryBot.define do
description "pipeline schedule"
project
+ trait :every_minute do
+ cron '*/1 * * * *'
+ cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ end
+
+ trait :hourly do
+ cron '* */1 * * *'
+ cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ end
+
trait :nightly do
cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index e8df5094b83..0b6a43b13a9 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -95,7 +95,8 @@ FactoryBot.define do
end
trait :merge_when_pipeline_succeeds do
- merge_when_pipeline_succeeds true
+ auto_merge_enabled true
+ auto_merge_strategy AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
merge_user { author }
end
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 75ac7cc7687..1de42512402 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -2,7 +2,6 @@ FactoryBot.define do
factory :project_auto_devops do
project
enabled true
- domain "example.com"
deploy_strategy :continuous
trait :continuous_deployment do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index f9950b5b03f..c4dbe23f6b4 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -332,16 +332,19 @@ describe 'Admin updates settings' do
end
context 'Network page' do
- it 'Enable outbound requests' do
+ it 'Changes Outbound requests settings' do
visit network_admin_application_settings_path
page.within('.as-outbound') do
check 'Allow requests to the local network from hooks and services'
+ # Enabled by default
+ uncheck 'Enforce DNS rebinding attack protection'
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
+ expect(Gitlab::CurrentSettings.dns_rebinding_protection_enabled).to be false
end
end
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index d7692181453..f2ab5373d3d 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -25,4 +25,18 @@ describe 'Global search' do
expect(page).to have_selector('.gl-pagination .next')
end
end
+
+ it 'closes the dropdown on blur', :js do
+ visit dashboard_projects_path
+
+ fill_in 'search', with: "a"
+ dropdown = find('.js-dashboard-search-options')
+
+ expect(dropdown[:class]).to include 'show'
+
+ find('#search').send_keys(:backspace)
+ find('body').click
+
+ expect(dropdown[:class]).not_to include 'show'
+ end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 7b6e9cd66b2..225b858742d 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -76,7 +76,7 @@ describe 'issuable list' do
create(:issue, project: project, author: user)
else
create(:merge_request, source_project: project, source_branch: generate(:branch))
- source_branch = FFaker::Name.name
+ source_branch = FFaker::Lorem.characters(8)
pipeline = create(:ci_empty_pipeline, project: project, ref: source_branch, status: %w(running failed success).sample, sha: 'any')
create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: source_branch, head_pipeline: pipeline)
end
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index bcc11217389..d05ef2a8f12 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -8,8 +8,6 @@ describe "User creates a merge request", :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(approval_rules: false)
-
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index d4ad11b3585..586b3ba170d 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -74,11 +74,12 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
source_project: project,
title: 'Bug NS-04',
author: user,
- merge_user: user,
- merge_params: { force_remove_source_branch: '1' })
+ merge_user: user)
end
before do
+ merge_request.merge_params['force_remove_source_branch'] = '0'
+ merge_request.save!
click_link "Cancel automatic merge"
end
@@ -102,11 +103,11 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
context 'when merge when pipeline succeeds is enabled' do
let(:merge_request) do
- create(:merge_request_with_diffs, :simple, source_project: project,
- author: user,
- merge_user: user,
- title: 'MepMep',
- merge_when_pipeline_succeeds: true)
+ create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
+ source_project: project,
+ author: user,
+ merge_user: user,
+ title: 'MepMep')
end
let!(:build) do
create(:ci_build, pipeline: pipeline)
@@ -158,8 +159,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -177,8 +178,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 93ddde623fe..0066e985fbb 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -314,7 +314,8 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: merge_request.author,
merge_error: 'Something went wrong'
)
@@ -326,8 +327,8 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -347,8 +348,8 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index f76f9ba7577..9d74a96ab3d 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -12,7 +12,7 @@ describe 'Import/Export - project export integration test', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
- let(:sensitive_words) { %w[pass secret token key encrypted] }
+ let(:sensitive_words) { %w[pass secret token key encrypted html] }
let(:safe_list) do
{
token: [ProjectHook, Ci::Trigger, CommitStatus],
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 28ae90bc0de..8d2b1fc7e30 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -47,7 +47,6 @@ describe 'Import/Export - project import integration test', :js do
expect(project.description).to eq("Foo Bar")
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
- expect(project_hook_exists?(project)).to be true
expect(wiki_exists?(project)).to be true
expect(project.import_state.status).to eq('finished')
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index b1a705f09ce..24041a51383 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -225,7 +225,7 @@ describe 'Pipeline Schedules', :js do
context 'when active is true and next_run_at is NULL' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
- pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil
+ pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
end
end
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index e14934b1672..9865dbbfb3c 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe 'Functions', :js do
include KubernetesHelpers
+ include ReactiveCachingHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,44 +14,70 @@ describe 'Functions', :js do
gitlab_sign_in(user)
end
- context 'when user does not have a cluster and visits the serverless page' do
+ shared_examples "it's missing knative installation" do
before do
visit project_serverless_functions_path(project)
end
- it 'sees an empty state' do
+ it 'sees an empty state require Knative installation' do
expect(page).to have_link('Install Knative')
expect(page).to have_selector('.empty-state')
end
end
+ context 'when user does not have a cluster and visits the serverless page' do
+ it_behaves_like "it's missing knative installation"
+ end
+
context 'when the user does have a cluster and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- before do
- visit project_serverless_functions_path(project)
- end
-
- it 'sees an empty state' do
- expect(page).to have_link('Install Knative')
- expect(page).to have_selector('.empty-state')
- end
+ it_behaves_like "it's missing knative installation"
end
context 'when the user has a cluster and knative installed and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let(:project) { knative.cluster.project }
+ let(:project) { cluster.project }
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
before do
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ stub_kubeclient_knative_services(stub_get_services_options)
+ stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
visit project_serverless_functions_path(project)
end
- it 'sees an empty listing of serverless functions' do
- expect(page).to have_selector('.empty-state')
+ context 'when there are no functions' do
+ let(:stub_get_services_options) do
+ {
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ }
+ end
+
+ it 'sees an empty listing of serverless functions' do
+ expect(page).to have_selector('.empty-state')
+ expect(page).not_to have_selector('.content-list')
+ end
+ end
+
+ context 'when there are functions' do
+ let(:stub_get_services_options) { { namespace: namespace.namespace } }
+
+ it 'does not see an empty listing of serverless functions' do
+ expect(page).not_to have_selector('.empty-state')
+ expect(page).to have_selector('.content-list')
+ end
end
end
end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
index dc0278370aa..df33d215602 100644
--- a/spec/features/projects/settings/forked_project_settings_spec.rb
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -7,7 +7,6 @@ describe 'Projects > Settings > For a forked project', :js do
let(:forked_project) { fork_project(original_project, user) }
before do
- stub_feature_flags(approval_rules: false)
original_project.add_maintainer(user)
forked_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 4fe45311b2d..27f6ed56283 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -6,7 +6,6 @@ describe 'Project' do
before do
stub_feature_flags(vue_file_list: false)
- stub_feature_flags(approval_rules: false)
end
describe 'creating from template' do
diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb
new file mode 100644
index 00000000000..b731c2bd6bf
--- /dev/null
+++ b/spec/finders/clusters/knative_services_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::KnativeServicesFinder do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.cluster_project.project }
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: project)
+ end
+
+ before do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ stub_kubeclient_service_pods(
+ kube_response(
+ kube_knative_pods_body(
+ project.name, namespace.namespace
+ )
+ ),
+ namespace: namespace.namespace
+ )
+ end
+
+ shared_examples 'a cached data' do
+ it 'has an unintialized cache' do
+ is_expected.to be_blank
+ end
+
+ context 'when using synchronous reactive cache' do
+ before do
+ synchronous_reactive_cache(cluster.knative_services_finder(project))
+ end
+
+ context 'when there are functions for cluster namespace' do
+ it { is_expected.not_to be_blank }
+ end
+
+ context 'when there are no functions for cluster namespace' do
+ before do
+ stub_kubeclient_knative_services(
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ )
+ stub_kubeclient_service_pods(
+ kube_response({ "kind" => "PodList", "items" => [] }),
+ namespace: namespace.namespace
+ )
+ end
+
+ it { is_expected.to be_blank }
+ end
+ end
+ end
+
+ describe '#service_pod_details' do
+ subject { cluster.knative_services_finder(project).service_pod_details(project.name) }
+
+ it_behaves_like 'a cached data'
+ end
+
+ describe '#services' do
+ subject { cluster.knative_services_finder(project).services }
+
+ it_behaves_like 'a cached data'
+ end
+
+ describe '#knative_detected' do
+ subject { cluster.knative_services_finder(project).knative_detected }
+ before do
+ synchronous_reactive_cache(cluster.knative_services_finder(project))
+ end
+
+ context 'when knative is installed' do
+ before do
+ stub_kubeclient_discover(service.api_url)
+ end
+
+ it { is_expected.to be_truthy }
+ it "discovers knative installation" do
+ expect { subject }
+ .to change { cluster.kubeclient.knative_client.discovered }
+ .from(false)
+ .to(true)
+ end
+ end
+
+ context 'when knative is not installed' do
+ before do
+ stub_kubeclient_discover_knative_not_found(service.api_url)
+ end
+
+ it { is_expected.to be_falsy }
+ it "does not discover knative installation" do
+ expect { subject }.not_to change { cluster.kubeclient.knative_client.discovered }
+ end
+ end
+ end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 3ad38207da4..8aea45b457c 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -10,7 +10,7 @@ describe Projects::Serverless::FunctionsFinder do
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
- let(:project) { cluster.project}
+ let(:project) { cluster.project }
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -23,9 +23,45 @@ describe Projects::Serverless::FunctionsFinder do
project.add_maintainer(user)
end
+ describe '#installed' do
+ it 'when reactive_caching is still fetching data' do
+ expect(described_class.new(project).knative_installed).to eq 'checking'
+ end
+
+ context 'when reactive_caching has finished' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+ before do
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ end
+
+ context 'when knative is not installed' do
+ it 'returns false' do
+ stub_kubeclient_discover_knative_not_found(service.api_url)
+
+ expect(described_class.new(project).knative_installed).to eq false
+ end
+ end
+
+ context 'reactive_caching is finished and knative is installed' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+ it 'returns true' do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
+
+ expect(described_class.new(project).knative_installed).to be true
+ end
+ end
+ end
+ end
+
describe 'retrieve data from knative' do
- it 'does not have knative installed' do
- expect(described_class.new(project).execute).to be_empty
+ context 'does not have knative installed' do
+ it { expect(described_class.new(project).execute).to be_empty }
end
context 'has knative installed' do
@@ -38,22 +74,24 @@ describe Projects::Serverless::FunctionsFinder do
it 'there are functions', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
expect(finder.execute).not_to be_empty
end
it 'has a function', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty
@@ -84,20 +122,4 @@ describe Projects::Serverless::FunctionsFinder do
end
end
end
-
- describe 'verify if knative is installed' do
- context 'knative is not installed' do
- it 'does not have knative installed' do
- expect(described_class.new(project).installed?).to be false
- end
- end
-
- context 'knative is installed' do
- let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
-
- it 'does have knative installed' do
- expect(described_class.new(project).installed?).to be true
- end
- end
- end
end
diff --git a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
new file mode 100644
index 00000000000..1e62d020026
--- /dev/null
+++ b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
@@ -0,0 +1,422 @@
+{
+ "version": "2.1",
+ "vulnerabilities": [
+ {
+ "category": "dependency_scanning",
+ "name": "Vulnerabilities in libxml2",
+ "message": "Vulnerabilities in libxml2 in nokogiri",
+ "description": " The version of libxml2 packaged with Nokogiri contains several vulnerabilities.\r\n Nokogiri has mitigated these issues by upgrading to libxml 2.9.5.\r\n\r\n It was discovered that a type confusion error existed in libxml2. An\r\n attacker could use this to specially construct XML data that\r\n could cause a denial of service or possibly execute arbitrary\r\n code. (CVE-2017-0663)\r\n\r\n It was discovered that libxml2 did not properly validate parsed entity\r\n references. An attacker could use this to specially construct XML\r\n data that could expose sensitive information. (CVE-2017-7375)\r\n\r\n It was discovered that a buffer overflow existed in libxml2 when\r\n handling HTTP redirects. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-7376)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overflow in\r\n libxml2 when handling elements. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-9047)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overread\r\n in libxml2 when handling elements. An attacker could use this\r\n to specially construct XML data that could cause a denial of\r\n service. (CVE-2017-9048)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered multiple buffer overreads\r\n in libxml2 when handling parameter-entity references. An attacker\r\n could use these to specially construct XML data that could cause a\r\n denial of service. (CVE-2017-9049, CVE-2017-9050)",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:06565b64-486d-4326-b906-890d9915804d",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
+ "value": "06565b64-486d-4326-b906-890d9915804d",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "usn",
+ "name": "USN-3424-1",
+ "value": "USN-3424-1",
+ "url": "https://usn.ubuntu.com/3424-1/"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Infinite recursion in parameter entities",
+ "message": "Infinite recursion in parameter entities in nokogiri",
+ "description": "libxml2 incorrectly handles certain parameter entities. An attacker can leverage this with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "value": "6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-16932",
+ "value": "CVE-2017-16932",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-16932.html"
+ },
+ {
+ "url": "https://usn.ubuntu.com/usn/usn-3504-1/"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Denial of Service",
+ "message": "Denial of Service in nokogiri",
+ "description": "libxml2 incorrectly handles certain files. An attacker can use this issue with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.\r\n\r\n",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:78658378-bd8f-4d79-81c8-07c419302426",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-78658378-bd8f-4d79-81c8-07c419302426",
+ "value": "78658378-bd8f-4d79-81c8-07c419302426",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-15412",
+ "value": "CVE-2017-15412",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-15412.html"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Bypass of a protection mechanism in libxslt",
+ "message": "Bypass of a protection mechanism in libxslt in nokogiri",
+ "description": "libxslt through 1.1.33 allows bypass of a protection mechanism because callers of xsltCheckRead and xsltCheckWrite permit access even upon receiving a -1 error code. xsltCheckRead can return -1 for a crafted URL that is not actually invalid and is subsequently loaded. Vendored version of libxslt has been patched to remediate this vulnerability. Note that this patch is not yet (as of 2019-04-22) in an upstream release of libxslt.",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version if using vendored version of libxslt OR update the system library libxslt to a fixed version",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "value": "1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2019-11068",
+ "value": "CVE-2019-11068",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1892"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/CVE-2019-11068"
+ },
+ {
+ "url": "https://security-tracker.debian.org/tracker/CVE-2019-11068"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Regular Expression Denial of Service",
+ "message": "Regular Expression Denial of Service in debug",
+ "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
+ "cve": "yarn/yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest versions.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn/yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "value": "37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/visionmedia/debug/issues/501"
+ },
+ {
+ "url": "https://github.com/visionmedia/debug/pull/504"
+ },
+ {
+ "url": "https://nodesecurity.io/advisories/534"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Authentication bypass via incorrect DOM traversal and canonicalization",
+ "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
+ "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
+ "cve": "yarn/yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
+ "severity": "Unknown",
+ "solution": "Upgrade to fixed version.\r\n",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn/yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
+ "value": "9952e574-7b5b-46fa-a270-aeb694198a98",
+ "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-11429",
+ "value": "CVE-2017-11429",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
+ },
+ {
+ "url": "https://github.com/Clever/saml2/issues/127"
+ },
+ {
+ "url": "https://www.kb.cert.org/vuls/id/475445"
+ }
+ ]
+ }
+ ],
+ "remediations": [],
+ "dependency_files": [
+ {
+ "path": "rails/Gemfile.lock",
+ "package_manager": "bundler",
+ "dependencies": [
+ {
+ "package": {
+ "name": "mini_portile2"
+ },
+ "version": "2.2.0"
+ },
+ {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ ]
+ },
+ {
+ "path": "yarn/yarn.lock",
+ "package_manager": "yarn",
+ "dependencies": [
+ {
+ "package": {
+ "name": "async"
+ },
+ "version": "0.2.10"
+ },
+ {
+ "package": {
+ "name": "async"
+ },
+ "version": "1.5.2"
+ },
+ {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ },
+ {
+ "package": {
+ "name": "ejs"
+ },
+ "version": "0.8.8"
+ },
+ {
+ "package": {
+ "name": "ms"
+ },
+ "version": "2.0.0"
+ },
+ {
+ "package": {
+ "name": "node-forge"
+ },
+ "version": "0.2.24"
+ },
+ {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ },
+ {
+ "package": {
+ "name": "sax"
+ },
+ "version": "1.2.4"
+ },
+ {
+ "package": {
+ "name": "underscore"
+ },
+ "version": "1.9.1"
+ },
+ {
+ "package": {
+ "name": "underscore"
+ },
+ "version": "1.6.0"
+ },
+ {
+ "package": {
+ "name": "xml-crypto"
+ },
+ "version": "0.8.5"
+ },
+ {
+ "package": {
+ "name": "xml-encryption"
+ },
+ "version": "0.7.4"
+ },
+ {
+ "package": {
+ "name": "xml2js"
+ },
+ "version": "0.4.19"
+ },
+ {
+ "package": {
+ "name": "xmlbuilder"
+ },
+ "version": "2.1.0"
+ },
+ {
+ "package": {
+ "name": "xmlbuilder"
+ },
+ "version": "9.0.7"
+ },
+ {
+ "package": {
+ "name": "xmldom"
+ },
+ "version": "0.1.19"
+ },
+ {
+ "package": {
+ "name": "xmldom"
+ },
+ "version": "0.1.27"
+ },
+ {
+ "package": {
+ "name": "xpath.js"
+ },
+ "version": "1.1.0"
+ },
+ {
+ "package": {
+ "name": "xpath"
+ },
+ "version": "0.0.5"
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/fixtures/security-reports/master/gl-dast-report.json b/spec/fixtures/security-reports/master/gl-dast-report.json
index 3a308bf047e..df459d9419d 100644
--- a/spec/fixtures/security-reports/master/gl-dast-report.json
+++ b/spec/fixtures/security-reports/master/gl-dast-report.json
@@ -1,40 +1,42 @@
{
- "site": {
- "alerts": [
- {
- "sourceid": "3",
- "wascid": "15",
- "cweid": "16",
- "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
- "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
- "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
- "count": "2",
- "pluginid": "10021",
- "alert": "X-Content-Type-Options Header Missing",
- "name": "X-Content-Type-Options Header Missing",
- "riskcode": "1",
- "confidence": "2",
- "riskdesc": "Low (Medium)",
- "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
- "instances": [
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
- }
- ]
- }
- ],
- "@ssl": "false",
- "@port": "80",
- "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
- "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
+ "site": [
+ {
+ "alerts": [
+ {
+ "sourceid": "3",
+ "wascid": "15",
+ "cweid": "16",
+ "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
+ "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
+ "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
+ "count": "2",
+ "pluginid": "10021",
+ "alert": "X-Content-Type-Options Header Missing",
+ "name": "X-Content-Type-Options Header Missing",
+ "riskcode": "1",
+ "confidence": "2",
+ "riskdesc": "Low (Medium)",
+ "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
+ "instances": [
+ {
+ "param": "X-Content-Type-Options",
+ "method": "GET",
+ "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+ },
+ {
+ "param": "X-Content-Type-Options",
+ "method": "GET",
+ "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
+ }
+ ]
+ }
+ ],
+ "@ssl": "false",
+ "@port": "80",
+ "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
+ "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+ }
+ ],
"@generated": "Fri, 13 Apr 2018 09:22:01",
"@version": "2.7.0"
}
diff --git a/spec/javascripts/activities_spec.js b/spec/frontend/activities_spec.js
index 23b6de7e4e0..d14be3a1f26 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/frontend/activities_spec.js
@@ -1,7 +1,6 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */
import $ from 'jquery';
-import 'vendor/jquery.endless-scroll';
import Activities from '~/activities';
import Pager from '~/pager';
@@ -40,7 +39,7 @@ describe('Activities', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- spyOn(Pager, 'init').and.stub();
+ jest.spyOn(Pager, 'init').mockImplementation(() => {});
new Activities();
});
diff --git a/spec/javascripts/api_spec.js b/spec/frontend/api_spec.js
index 805bb10bda6..6010488d9e0 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -264,7 +264,7 @@ describe('Api', () => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
- const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/labels`;
+ const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/-/labels`;
const expectedData = {
label: labelData,
};
@@ -459,7 +459,7 @@ describe('Api', () => {
dummyProjectPath,
)}/repository/branches`;
- spyOn(axios, 'post').and.callThrough();
+ jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(200, {
name: branch,
diff --git a/spec/javascripts/autosave_spec.js b/spec/frontend/autosave_spec.js
index dcb1c781591..4d9c8f96d62 100644
--- a/spec/javascripts/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -1,16 +1,19 @@
import $ from 'jquery';
import Autosave from '~/autosave';
import AccessorUtilities from '~/lib/utils/accessor';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
describe('Autosave', () => {
+ useLocalStorageSpy();
+
let autosave;
const field = $('<textarea></textarea>');
const key = 'key';
describe('class constructor', () => {
beforeEach(() => {
- spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
- spyOn(Autosave.prototype, 'restore');
+ jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true);
+ jest.spyOn(Autosave.prototype, 'restore').mockImplementation(() => {});
});
it('should set .isLocalStorageAvailable', () => {
@@ -27,8 +30,6 @@ describe('Autosave', () => {
field,
key,
};
-
- spyOn(window.localStorage, 'getItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -55,7 +56,7 @@ describe('Autosave', () => {
});
it('triggers jquery event', () => {
- spyOn(autosave.field, 'trigger').and.callThrough();
+ jest.spyOn(autosave.field, 'trigger').mockImplementation(() => {});
Autosave.prototype.restore.call(autosave);
@@ -77,7 +78,7 @@ describe('Autosave', () => {
});
it('does not trigger event', () => {
- spyOn(field, 'trigger').and.callThrough();
+ jest.spyOn(field, 'trigger');
expect(field.trigger).not.toHaveBeenCalled();
});
@@ -86,11 +87,9 @@ describe('Autosave', () => {
describe('save', () => {
beforeEach(() => {
- autosave = jasmine.createSpyObj('autosave', ['reset']);
+ autosave = { reset: jest.fn() };
autosave.field = field;
field.val('value');
-
- spyOn(window.localStorage, 'setItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -123,8 +122,6 @@ describe('Autosave', () => {
autosave = {
key,
};
-
- spyOn(window.localStorage, 'removeItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 8bcf02f0a34..221ebb143be 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -1,9 +1,11 @@
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants';
-import eventHub from '~/clusters/event_hub';
import mountComponent from 'helpers/vue_mount_component_helper';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
+import eventHub from '~/clusters/event_hub';
+import { shallowMount } from '@vue/test-utils';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
describe('Applications', () => {
let vm;
@@ -277,73 +279,48 @@ describe('Applications', () => {
});
describe('Knative application', () => {
- describe('when installed', () => {
- describe('with ip address', () => {
- const props = {
- applications: {
- ...APPLICATIONS_MOCK_STATE,
- knative: {
- title: 'Knative',
- hostname: 'example.com',
- status: 'installed',
- externalIp: '1.1.1.1',
- },
- },
- };
- it('renders ip address with a clipboard button', () => {
- vm = mountComponent(Applications, props);
+ const propsData = {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ knative: {
+ title: 'Knative',
+ hostname: 'example.com',
+ status: 'installed',
+ externalIp: '1.1.1.1',
+ installed: true,
+ },
+ },
+ };
+ const newHostname = 'newhostname.com';
+ let wrapper;
+ let knativeDomainEditor;
- expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('1.1.1.1');
-
- expect(
- vm.$el
- .querySelector('.js-knative-endpoint-clipboard-btn')
- .getAttribute('data-clipboard-text'),
- ).toEqual('1.1.1.1');
- });
-
- it('renders domain & allows editing', () => {
- expect(vm.$el.querySelector('.js-knative-domainname').value).toEqual('example.com');
- expect(vm.$el.querySelector('.js-knative-domainname').getAttribute('readonly')).toBe(
- null,
- );
- });
-
- it('renders an update/save Knative domain button', () => {
- expect(vm.$el.querySelector('.js-knative-save-domain-button')).not.toBe(null);
- });
+ beforeEach(() => {
+ wrapper = shallowMount(Applications, { propsData });
+ jest.spyOn(eventHub, '$emit');
- it('emits event when clicking Save changes button', () => {
- jest.spyOn(eventHub, '$emit');
- vm = mountComponent(Applications, props);
+ knativeDomainEditor = wrapper.find(KnativeDomainEditor);
+ });
- const saveButton = vm.$el.querySelector('.js-knative-save-domain-button');
+ afterEach(() => {
+ wrapper.destroy();
+ });
- saveButton.click();
+ it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
+ knativeDomainEditor.vm.$emit('save', newHostname);
- expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
- id: 'knative',
- params: { hostname: 'example.com' },
- });
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
+ id: 'knative',
+ params: { hostname: newHostname },
});
+ });
- describe('without ip address', () => {
- it('renders an input text with a loading icon and an alert text', () => {
- vm = mountComponent(Applications, {
- applications: {
- ...APPLICATIONS_MOCK_STATE,
- knative: {
- title: 'Knative',
- hostname: 'example.com',
- status: 'installed',
- },
- },
- });
+ it('emits setKnativeHostname event when knative domain editor emits change event', () => {
+ wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname);
- expect(vm.$el.querySelector('.js-knative-ip-loading-icon')).not.toBe(null);
- expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null);
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', {
+ id: 'knative',
+ hostname: newHostname,
});
});
});
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
new file mode 100644
index 00000000000..242b5701f8b
--- /dev/null
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING } = APPLICATION_STATUS;
+
+describe('KnativeDomainEditor', () => {
+ let wrapper;
+ let knative;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(KnativeDomainEditor, {
+ propsData: { ...props },
+ });
+ };
+
+ beforeEach(() => {
+ knative = {
+ title: 'Knative',
+ hostname: 'example.com',
+ installed: true,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('knative has an assigned IP address', () => {
+ beforeEach(() => {
+ knative.externalIp = '1.1.1.1';
+ createComponent({ knative });
+ });
+
+ it('renders ip address with a clipboard button', () => {
+ expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true);
+ expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp);
+ });
+
+ it('displays ip address clipboard button', () => {
+ expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual(
+ knative.externalIp,
+ );
+ });
+
+ it('renders domain & allows editing', () => {
+ const domainNameInput = wrapper.find('.js-knative-domainname');
+
+ expect(domainNameInput.element.value).toEqual(knative.hostname);
+ expect(domainNameInput.attributes('readonly')).toBeFalsy();
+ });
+
+ it('renders an update/save Knative domain button', () => {
+ expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true);
+ });
+ });
+
+ describe('knative without ip address', () => {
+ beforeEach(() => {
+ knative.externalIp = null;
+ createComponent({ knative });
+ });
+
+ it('renders an input text with a loading icon', () => {
+ expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true);
+ });
+
+ it('renders message indicating there is not IP address assigned', () => {
+ expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true);
+ });
+ });
+
+ describe('clicking save changes button', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('triggers save event and pass current knative hostname', () => {
+ wrapper.find(LoadingButton).vm.$emit('click');
+ expect(wrapper.emitted('save')[0]).toEqual([knative.hostname]);
+ });
+ });
+
+ describe('when knative domain name was saved successfully', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('displays toast indicating a successful update', () => {
+ wrapper.vm.$toast = { show: jest.fn() };
+ wrapper.setProps({ knative: Object.assign({ updateSuccessful: true }, knative) });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
+ 'Knative domain name was updated successfully.',
+ );
+ });
+ });
+ });
+
+ describe('when knative domain name input changes', () => {
+ it('emits "set" event with updated domain name', () => {
+ const newHostname = 'newhostname.com';
+
+ wrapper.setData({ knativeHostname: newHostname });
+
+ expect(wrapper.emitted('set')[0]).toEqual([newHostname]);
+ });
+ });
+
+ describe('when updating knative domain name failed', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('displays an error banner indicating the operation failure', () => {
+ wrapper.setProps({ knative: { updateFailed: true, ...knative } });
+
+ expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true);
+ });
+ });
+
+ describe(`when knative status is ${UPDATING}`, () => {
+ beforeEach(() => {
+ createComponent({ knative: { status: UPDATING, ...knative } });
+ });
+
+ it('renders loading spinner in save button', () => {
+ expect(wrapper.find(LoadingButton).props('loading')).toBe(true);
+ });
+
+ it('renders disabled save button', () => {
+ expect(wrapper.find(LoadingButton).props('disabled')).toBe(true);
+ });
+
+ it('renders save button with "Saving" label', () => {
+ expect(wrapper.find(LoadingButton).props('label')).toBe('Saving');
+ });
+ });
+});
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index aa926bb36d7..0d129349799 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -133,6 +133,8 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ updateSuccessful: false,
+ updateFailed: false,
},
cert_manager: {
title: 'Cert-Manager',
diff --git a/spec/frontend/helpers/jquery.js b/spec/frontend/helpers/jquery.js
new file mode 100644
index 00000000000..6421a592c0c
--- /dev/null
+++ b/spec/frontend/helpers/jquery.js
@@ -0,0 +1,6 @@
+import $ from 'jquery';
+
+global.$ = $;
+global.jQuery = $;
+
+export default $;
diff --git a/spec/frontend/helpers/local_storage_helper.js b/spec/frontend/helpers/local_storage_helper.js
new file mode 100644
index 00000000000..48e66b11767
--- /dev/null
+++ b/spec/frontend/helpers/local_storage_helper.js
@@ -0,0 +1,41 @@
+/**
+ * Manage the instance of a custom `window.localStorage`
+ *
+ * This only encapsulates the setup / teardown logic so that it can easily be
+ * reused with different implementations (i.e. a spy or a [fake][1])
+ *
+ * [1]: https://stackoverflow.com/a/41434763/1708147
+ *
+ * @param {() => any} fn Function that returns the object to use for localStorage
+ */
+const useLocalStorage = fn => {
+ const origLocalStorage = window.localStorage;
+ let currentLocalStorage;
+
+ Object.defineProperty(window, 'localStorage', {
+ get: () => currentLocalStorage,
+ });
+
+ beforeEach(() => {
+ currentLocalStorage = fn();
+ });
+
+ afterEach(() => {
+ currentLocalStorage = origLocalStorage;
+ });
+};
+
+/**
+ * Create an object with the localStorage interface but `jest.fn()` implementations.
+ */
+export const createLocalStorageSpy = () => ({
+ clear: jest.fn(),
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ removeItem: jest.fn(),
+});
+
+/**
+ * Before each test, overwrite `window.localStorage` with a spy implementation.
+ */
+export const useLocalStorageSpy = () => useLocalStorage(createLocalStorageSpy);
diff --git a/spec/frontend/helpers/vue_test_utils_helper.js b/spec/frontend/helpers/vue_test_utils_helper.js
index 19e27388eeb..121e99c9783 100644
--- a/spec/frontend/helpers/vue_test_utils_helper.js
+++ b/spec/frontend/helpers/vue_test_utils_helper.js
@@ -16,4 +16,6 @@ const vNodeContainsText = (vnode, text) =>
* @param {String} text
*/
export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
- !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length;
+ Boolean(
+ shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
+ );
diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 381c7b2d0a6..eca240c9c18 100644
--- a/spec/javascripts/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -107,4 +107,88 @@ describe('URL utility', () => {
expect(url).toBe('/home/feature#install');
});
});
+
+ describe('getBaseURL', () => {
+ beforeEach(() => {
+ global.window = Object.create(window);
+ Object.defineProperty(window, 'location', {
+ value: {
+ host: 'gitlab.com',
+ protocol: 'https:',
+ },
+ });
+ });
+
+ it('returns correct base URL', () => {
+ expect(urlUtils.getBaseURL()).toBe('https://gitlab.com');
+ });
+ });
+
+ describe('isAbsoluteOrRootRelative', () => {
+ const validUrls = ['https://gitlab.com/', 'http://gitlab.com/', '/users/sign_in'];
+
+ const invalidUrls = [' https://gitlab.com/', './file/path', 'notanurl', '<a></a>'];
+
+ it.each(validUrls)(`returns true for %s`, url => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(true);
+ });
+
+ it.each(invalidUrls)(`returns false for %s`, url => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(false);
+ });
+ });
+
+ describe('isSafeUrl', () => {
+ const absoluteUrls = [
+ 'http://example.org',
+ 'http://example.org:8080',
+ 'https://example.org',
+ 'https://example.org:8080',
+ 'https://192.168.1.1',
+ ];
+
+ const rootRelativeUrls = ['/relative/link'];
+
+ const relativeUrls = ['./relative/link', '../relative/link'];
+
+ const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
+
+ /* eslint-disable no-script-url */
+ const nonHttpUrls = [
+ 'javascript:',
+ 'javascript:alert("XSS")',
+ 'jav\tascript:alert("XSS");',
+ ' &#14; javascript:alert("XSS");',
+ 'ftp://192.168.1.1',
+ 'file:///',
+ 'file:///etc/hosts',
+ ];
+ /* eslint-enable no-script-url */
+
+ // javascript:alert('XSS')
+ const encodedJavaScriptUrls = [
+ '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041',
+ '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;',
+ '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
+ '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
+ ];
+
+ const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
+ const unsafeUrls = [
+ ...relativeUrls,
+ ...urlsWithoutHost,
+ ...nonHttpUrls,
+ ...encodedJavaScriptUrls,
+ ];
+
+ describe('with URL constructor support', () => {
+ it.each(safeUrls)('returns true for %s', url => {
+ expect(urlUtils.isSafeURL(url)).toBe(true);
+ });
+
+ it.each(unsafeUrls)('returns false for %s', url => {
+ expect(urlUtils.isSafeURL(url)).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js
index ef876dc2941..ff833d2c899 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/frontend/notes/components/note_app_spec.js
@@ -1,18 +1,47 @@
-import $ from 'jquery';
-import _ from 'underscore';
+import $ from 'helpers/jquery';
import Vue from 'vue';
import { mount, createLocalVue } from '@vue/test-utils';
import NotesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
-import * as mockData from '../mock_data';
+import { setTestTimeout } from 'helpers/timeout';
+// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491)
+import * as mockData from '../../../javascripts/notes/mock_data';
+
+const originalInterceptors = [...Vue.http.interceptors];
+
+const emptyResponseInterceptor = (request, next) => {
+ next(
+ request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }),
+ );
+};
+
+setTestTimeout(1000);
describe('note_app', () => {
let mountComponent;
let wrapper;
let store;
+ /**
+ * waits for fetchNotes() to complete
+ */
+ const waitForDiscussionsRequest = () =>
+ new Promise(resolve => {
+ const { vm } = wrapper.find(NotesApp);
+ const unwatch = vm.$watch('isFetching', isFetching => {
+ if (isFetching) {
+ return;
+ }
+
+ unwatch();
+ resolve();
+ });
+ });
+
beforeEach(() => {
$('body').attr('data-page', 'projects:merge_requests:show');
@@ -33,6 +62,7 @@ describe('note_app', () => {
template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>',
},
{
+ attachToDocument: true,
propsData,
store,
localVue,
@@ -44,24 +74,14 @@ describe('note_app', () => {
afterEach(() => {
wrapper.destroy();
+ Vue.http.interceptors = [...originalInterceptors];
});
describe('set data', () => {
- const responseInterceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify([]), {
- status: 200,
- }),
- );
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(responseInterceptor);
+ Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor);
+ return waitForDiscussionsRequest();
});
it('should set notes data', () => {
@@ -87,29 +107,23 @@ describe('note_app', () => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
- });
-
- it('should render list of notes', done => {
+ it('should render list of notes', () => {
const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0];
- setTimeout(() => {
- expect(
- wrapper
- .find('.main-notes-list .note-header-author-name')
- .text()
- .trim(),
- ).toEqual(note.author.name);
+ expect(
+ wrapper
+ .find('.main-notes-list .note-header-author-name')
+ .text()
+ .trim(),
+ ).toEqual(note.author.name);
- expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
- done();
- }, 0);
+ expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
});
it('should render form', () => {
@@ -120,37 +134,42 @@ describe('note_app', () => {
});
it('should not render form when commenting is disabled', () => {
+ wrapper.destroy();
+
store.state.commentsDisabled = true;
wrapper = mountComponent();
-
- expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
+ return waitForDiscussionsRequest().then(() => {
+ expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
+ });
});
it('should render discussion filter note `commentsDisabled` is true', () => {
+ wrapper.destroy();
+
store.state.commentsDisabled = true;
wrapper = mountComponent();
-
- expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
+ return waitForDiscussionsRequest().then(() => {
+ expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
+ });
});
it('should render form comment button as disabled', () => {
expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled');
});
- it('updates discussions badge', done => {
- setTimeout(() => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
-
- done();
- });
+ it('updates discussions badge', () => {
+ expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
});
});
describe('while fetching data', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
});
+ afterEach(() => waitForDiscussionsRequest());
+
it('renders skeleton notes', () => {
expect(wrapper.find('.animation-container').exists()).toBe(true);
});
@@ -165,78 +184,55 @@ describe('note_app', () => {
describe('update note', () => {
describe('individual note', () => {
- beforeEach(done => {
+ beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
- spyOn(service, 'updateNote').and.callThrough();
+ jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
- setTimeout(() => {
+ return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
- Vue.nextTick(done);
- }, 0);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- mockData.individualNoteInterceptor,
- );
+ });
});
it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('calls the service to update the note', done => {
+ it('calls the service to update the note', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled();
- // Wait for the requests to finish before destroying
- setTimeout(() => {
- done();
- });
});
});
describe('discussion note', () => {
- beforeEach(done => {
+ beforeEach(() => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
- spyOn(service, 'updateNote').and.callThrough();
+ jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
-
- setTimeout(() => {
+ return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
- Vue.nextTick(done);
- }, 0);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- mockData.discussionNoteInterceptor,
- );
+ });
});
it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('updates the note and resets the edit form', done => {
+ it('updates the note and resets the edit form', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled();
- // Wait for the requests to finish before destroying
- setTimeout(() => {
- done();
- });
});
});
});
describe('new note form', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
it('should render markdown docs url', () => {
@@ -266,43 +262,37 @@ describe('note_app', () => {
beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
- });
+ it('should render markdown docs url', () => {
+ wrapper.find('.js-note-edit').trigger('click');
+ const { markdownDocsPath } = mockData.notesDataMock;
- it('should render markdown docs url', done => {
- setTimeout(() => {
- wrapper.find('.js-note-edit').trigger('click');
- const { markdownDocsPath } = mockData.notesDataMock;
-
- Vue.nextTick(() => {
- expect(
- wrapper
- .find(`.edit-note a[href="${markdownDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('Markdown is supported');
- done();
- });
- }, 0);
+ return Vue.nextTick().then(() => {
+ expect(
+ wrapper
+ .find(`.edit-note a[href="${markdownDocsPath}"]`)
+ .text()
+ .trim(),
+ ).toEqual('Markdown is supported');
+ });
});
- it('should not render quick actions docs url', done => {
- setTimeout(() => {
- wrapper.find('.js-note-edit').trigger('click');
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- Vue.nextTick(() => {
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
- done();
- });
- }, 0);
+ it('should not render quick actions docs url', () => {
+ wrapper.find('.js-note-edit').trigger('click');
+ const { quickActionsDocsPath } = mockData.notesDataMock;
+ expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
});
});
describe('emoji awards', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(emptyResponseInterceptor);
+ wrapper = mountComponent();
+ return waitForDiscussionsRequest();
+ });
+
it('dispatches toggleAward after toggleAward event', () => {
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
@@ -310,17 +300,18 @@ describe('note_app', () => {
noteId: 1,
},
});
- const toggleAwardAction = jasmine.createSpy('toggleAward');
+ const toggleAwardAction = jest.fn().mockName('toggleAward');
wrapper.vm.$store.hotUpdate({
actions: {
toggleAward: toggleAwardAction,
+ stopPolling() {},
},
});
wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent);
expect(toggleAwardAction).toHaveBeenCalledTimes(1);
- const [, payload] = toggleAwardAction.calls.argsFor(0);
+ const [, payload] = toggleAwardAction.mock.calls[0];
expect(payload).toEqual({
awardName: 'test',
diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js
index de1dd219fe0..986aada0b03 100644
--- a/spec/frontend/operation_settings/components/external_dashboard_spec.js
+++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js
@@ -1,30 +1,64 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
+import store from '~/operation_settings/store';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
import { TEST_HOST } from 'helpers/test_constants';
+jest.mock('~/lib/utils/axios_utils');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
+
describe('operation settings external dashboard component', () => {
let wrapper;
- const externalDashboardPath = `http://mock-external-domain.com/external/dashboard/path`;
+ const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
+ const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
-
- beforeEach(() => {
- wrapper = shallowMount(ExternalDashboard, {
- propsData: {
- externalDashboardPath,
- externalDashboardHelpPagePath,
+ const localVue = createLocalVue();
+ const mountComponent = (shallow = true) => {
+ const config = [
+ ExternalDashboard,
+ {
+ localVue,
+ store: store({
+ operationsSettingsEndpoint,
+ externalDashboardUrl,
+ externalDashboardHelpPagePath,
+ }),
},
- });
+ ];
+ wrapper = shallow ? shallowMount(...config) : mount(...config);
+ };
+
+ afterEach(() => {
+ if (wrapper.destroy) {
+ wrapper.destroy();
+ }
+ axios.patch.mockReset();
+ refreshCurrentPage.mockReset();
+ createFlash.mockReset();
});
it('renders header text', () => {
+ mountComponent();
expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard');
});
+ describe('expand/collapse button', () => {
+ it('renders as an expand button by default', () => {
+ const button = wrapper.find(GlButton);
+
+ expect(button.text()).toBe('Expand');
+ });
+ });
+
describe('sub-header', () => {
let subHeader;
beforeEach(() => {
+ mountComponent();
subHeader = wrapper.find('.js-section-sub-header');
});
@@ -43,57 +77,87 @@ describe('operation settings external dashboard component', () => {
});
describe('form', () => {
- let form;
+ describe('input label', () => {
+ let formGroup;
- beforeEach(() => {
- form = wrapper.find('form');
- });
+ beforeEach(() => {
+ mountComponent();
+ formGroup = wrapper.find(GlFormGroup);
+ });
- describe('external dashboard url', () => {
- describe('input label', () => {
- let formGroup;
+ it('uses label text', () => {
+ expect(formGroup.attributes().label).toBe('Full dashboard URL');
+ });
- beforeEach(() => {
- formGroup = form.find(GlFormGroup);
- });
+ it('uses description text', () => {
+ expect(formGroup.attributes().description).toBe(
+ 'Enter the URL of the dashboard you want to link to',
+ );
+ });
+ });
- it('uses label text', () => {
- expect(formGroup.attributes().label).toBe('Full dashboard URL');
- });
+ describe('input field', () => {
+ let input;
- it('uses description text', () => {
- expect(formGroup.attributes().description).toBe(
- 'Enter the URL of the dashboard you want to link to',
- );
- });
+ beforeEach(() => {
+ mountComponent();
+ input = wrapper.find(GlFormInput);
});
- describe('input field', () => {
- let input;
-
- beforeEach(() => {
- input = form.find(GlFormInput);
- });
+ it('defaults to externalDashboardUrl', () => {
+ expect(input.attributes().value).toBe(externalDashboardUrl);
+ });
- it('defaults to externalDashboardPath prop', () => {
- expect(input.attributes().value).toBe(externalDashboardPath);
- });
+ it('uses a placeholder', () => {
+ expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
+ });
+ });
- it('uses a placeholder', () => {
- expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
- });
+ describe('submit button', () => {
+ const endpointRequest = [
+ operationsSettingsEndpoint,
+ {
+ project: {
+ metrics_setting_attributes: {
+ external_dashboard_url: externalDashboardUrl,
+ },
+ },
+ },
+ ];
+
+ it('renders button label', () => {
+ mountComponent();
+ const submit = wrapper.find(GlButton);
+ expect(submit.text()).toBe('Save Changes');
});
- describe('submit button', () => {
- let submit;
+ it('submits form on click', () => {
+ mountComponent(false);
+ axios.patch.mockResolvedValue();
+ wrapper.find(GlButton).trigger('click');
- beforeEach(() => {
- submit = form.find(GlButton);
- });
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
- it('renders button label', () => {
- expect(submit.text()).toBe('Save Changes');
- });
+ return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ });
+
+ it('creates flash banner on error', () => {
+ mountComponent(false);
+ const message = 'mockErrorMessage';
+ axios.patch.mockRejectedValue({ response: { data: { message } } });
+ wrapper.find(GlButton).trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(jest.runAllTicks)
+ .then(() =>
+ expect(createFlash).toHaveBeenCalledWith(
+ `There was an error saving your changes. ${message}`,
+ 'alert',
+ ),
+ );
});
});
});
diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js
new file mode 100644
index 00000000000..1854142c89a
--- /dev/null
+++ b/spec/frontend/operation_settings/store/mutations_spec.js
@@ -0,0 +1,19 @@
+import mutations from '~/operation_settings/store/mutations';
+import createState from '~/operation_settings/store/state';
+
+describe('operation settings mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = createState();
+ });
+
+ describe('SET_EXTERNAL_DASHBOARD_URL', () => {
+ it('sets externalDashboardUrl', () => {
+ const mockUrl = 'mockUrl';
+ mutations.SET_EXTERNAL_DASHBOARD_URL(localState, mockUrl);
+
+ expect(localState.externalDashboardUrl).toBe(mockUrl);
+ });
+ });
+});
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index 161a637dd75..0ad85e218dc 100644
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -14,7 +14,7 @@ describe('environment row component', () => {
beforeEach(() => {
localVue = createLocalVue();
- vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*');
+ vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
});
afterEach(() => vm.$destroy());
@@ -48,7 +48,11 @@ describe('environment row component', () => {
beforeEach(() => {
localVue = createLocalVue();
- vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test');
+ vm = createComponent(
+ localVue,
+ translate(mockServerlessFunctionsDiffEnv.functions).test,
+ 'test',
+ );
});
afterEach(() => vm.$destroy());
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 6924fb9e91f..d8a80f8031e 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -34,11 +34,11 @@ describe('functionsComponent', () => {
});
it('should render empty state when Knative is not installed', () => {
+ store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
- installed: false,
clustersPath: '',
helpPath: '',
statusPath: '',
@@ -55,7 +55,6 @@ describe('functionsComponent', () => {
localVue,
store,
propsData: {
- installed: true,
clustersPath: '',
helpPath: '',
statusPath: '',
@@ -67,12 +66,11 @@ describe('functionsComponent', () => {
});
it('should render empty state when there is no function data', () => {
- store.dispatch('receiveFunctionsNoDataSuccess');
+ store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
- installed: true,
clustersPath: '',
helpPath: '',
statusPath: '',
@@ -91,12 +89,31 @@ describe('functionsComponent', () => {
);
});
+ it('should render functions and a loader when functions are partially fetched', () => {
+ store.dispatch('receiveFunctionsPartial', {
+ ...mockServerlessFunctions,
+ knative_installed: 'checking',
+ });
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.find('.js-functions-wrapper').exists()).toBe(true);
+ expect(component.find('.js-functions-loader').exists()).toBe(true);
+ });
+
it('should render the functions list', () => {
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
- installed: true,
clustersPath: 'clustersPath',
helpPath: 'helpPath',
statusPath,
diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js
index a2c18616324..ef616ceb37f 100644
--- a/spec/frontend/serverless/mock_data.js
+++ b/spec/frontend/serverless/mock_data.js
@@ -1,56 +1,62 @@
-export const mockServerlessFunctions = [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
-];
+export const mockServerlessFunctions = {
+ knative_installed: true,
+ functions: [
+ {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+ },
+ {
+ name: 'testfunc2',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc2.tm-example.apps.example.com',
+ description: 'A second test service\nThis one with additional descriptions',
+ image: 'knative-test-echo-buildtemplate',
+ },
+ ],
+};
-export const mockServerlessFunctionsDiffEnv = [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: 'test',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
-];
+export const mockServerlessFunctionsDiffEnv = {
+ knative_installed: true,
+ functions: [
+ {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+ },
+ {
+ name: 'testfunc2',
+ namespace: 'tm-example',
+ environment_scope: 'test',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc2.tm-example.apps.example.com',
+ description: 'A second test service\nThis one with additional descriptions',
+ image: 'knative-test-echo-buildtemplate',
+ },
+ ],
+};
export const mockServerlessFunction = {
name: 'testfunc1',
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
index fb549c8f153..92853fda37c 100644
--- a/spec/frontend/serverless/store/getters_spec.js
+++ b/spec/frontend/serverless/store/getters_spec.js
@@ -32,7 +32,7 @@ describe('Serverless Store Getters', () => {
describe('getFunctions', () => {
it('should translate the raw function array to group the functions per environment scope', () => {
- state.functions = mockServerlessFunctions;
+ state.functions = mockServerlessFunctions.functions;
const funcs = getters.getFunctions(state);
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
index ca3053e5c38..e2771c7e5fd 100644
--- a/spec/frontend/serverless/store/mutations_spec.js
+++ b/spec/frontend/serverless/store/mutations_spec.js
@@ -19,13 +19,13 @@ describe('ServerlessMutations', () => {
expect(state.isLoading).toEqual(false);
expect(state.hasFunctionData).toEqual(true);
- expect(state.functions).toEqual(mockServerlessFunctions);
+ expect(state.functions).toEqual(mockServerlessFunctions.functions);
});
it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
const state = {};
- mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state);
+ mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true });
expect(state.isLoading).toEqual(false);
expect(state.hasFunctionData).toEqual(false);
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index b356ea85cad..0f5d47b3bfe 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -4,7 +4,7 @@ describe('getStateKey', () => {
it('should return proper state name', () => {
const context = {
mergeStatus: 'checked',
- mergeWhenPipelineSucceeds: false,
+ autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: false,
isPipelineFailed: false,
@@ -31,9 +31,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('notAllowedToMerge');
- context.mergeWhenPipelineSucceeds = true;
+ context.autoMergeEnabled = true;
- expect(bound()).toEqual('mergeWhenPipelineSucceeds');
+ expect(bound()).toEqual('autoMergeEnabled');
context.isSHAMismatch = true;
@@ -80,7 +80,7 @@ describe('getStateKey', () => {
it('returns rebased state key', () => {
const context = {
mergeStatus: 'checked',
- mergeWhenPipelineSucceeds: false,
+ autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: true,
isPipelineFailed: true,
diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
new file mode 100644
index 00000000000..395e08081d3
--- /dev/null
+++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::NamespaceProjectsResolver, :nested_groups do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+
+ context "with a group" do
+ let(:group) { create(:group) }
+ let(:namespace) { group }
+ let(:project1) { create(:project, namespace: namespace) }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:nested_project) { create(:project, group: nested_group) }
+
+ before do
+ project1.add_developer(current_user)
+ project2.add_developer(current_user)
+ nested_project.add_developer(current_user)
+ end
+
+ describe '#resolve' do
+ it 'finds all projects' do
+ expect(resolve_projects).to contain_exactly(project1, project2)
+ end
+
+ it 'finds all projects including the subgroups' do
+ expect(resolve_projects(include_subgroups: true)).to contain_exactly(project1, project2, nested_project)
+ end
+
+ context 'with an user namespace' do
+ let(:namespace) { current_user.namespace }
+
+ it 'finds all projects' do
+ expect(resolve_projects).to contain_exactly(project1, project2)
+ end
+
+ it 'finds all projects including the subgroups' do
+ expect(resolve_projects(include_subgroups: true)).to contain_exactly(project1, project2)
+ end
+ end
+ end
+ end
+
+ context "when passing a non existent, batch loaded namespace" do
+ let(:namespace) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
+ end
+ end
+
+ it "returns nil without breaking" do
+ expect(resolve_projects).to be_empty
+ end
+ end
+
+ it 'has an high complexity regardless of arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
+
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 24
+ expect(field.to_graphql.complexity.call({}, { include_subgroups: true }, 1)).to eq 24
+ end
+
+ def resolve_projects(args = { include_subgroups: false }, context = { current_user: current_user })
+ resolve(described_class, obj: namespace, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
index 4fe426e2447..a7fb156d9a8 100644
--- a/spec/graphql/types/base_field_spec.rb
+++ b/spec/graphql/types/base_field_spec.rb
@@ -6,7 +6,7 @@ describe Types::BaseField do
context 'when considering complexity' do
let(:resolver) do
Class.new(described_class) do
- def self.resolver_complexity(args)
+ def self.resolver_complexity(args, child_complexity:)
2 if args[:foo]
end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index dc37b15001f..bae560829cc 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -6,4 +6,10 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class.graphql_name).to eq('Issue') }
it { expect(described_class).to require_graphql_authorizations(:read_issue) }
+
+ it 'has specific fields' do
+ %i[relative_position web_path web_url reference].each do |field_name|
+ expect(described_class).to have_graphql_field(field_name)
+ end
+ end
end
diff --git a/spec/graphql/types/namespace_type.rb b/spec/graphql/types/namespace_type_spec.rb
index 7cd6a79ae5d..b4144cc4121 100644
--- a/spec/graphql/types/namespace_type.rb
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -4,4 +4,6 @@ require 'spec_helper'
describe GitlabSchema.types['Namespace'] do
it { expect(described_class.graphql_name).to eq('Namespace') }
+
+ it { expect(described_class).to have_graphql_field(:projects) }
end
diff --git a/spec/graphql/types/project_statistics_type_spec.rb b/spec/graphql/types/project_statistics_type_spec.rb
new file mode 100644
index 00000000000..e9feac57a36
--- /dev/null
+++ b/spec/graphql/types/project_statistics_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['ProjectStatistics'] do
+ it "has all the required fields" do
+ is_expected.to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
+ :build_artifacts_size, :packages_size, :commit_count,
+ :wiki_size)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 075fa7c7e43..cb5ac2e3cb1 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -19,4 +19,6 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_field(:pipelines) }
it { is_expected.to have_graphql_field(:repository) }
+
+ it { is_expected.to have_graphql_field(:statistics) }
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index b4626955816..af1972a2513 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -5,7 +5,17 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :group, :echo, :metadata) }
+ it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) }
+
+ describe 'namespace field' do
+ subject { described_class.fields['namespace'] }
+
+ it 'finds namespaces by full path' do
+ is_expected.to have_graphql_arguments(:full_path)
+ is_expected.to have_graphql_type(Types::NamespaceType)
+ is_expected.to have_graphql_resolver(Resolvers::NamespaceResolver)
+ end
+ end
describe 'project field' do
subject { described_class.fields['project'] }
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 0434af25866..e6aacb5b92b 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -8,19 +8,19 @@ describe EmailsHelper do
context "and format is text" do
it "returns plain text" do
- expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
end
end
context "and format is HTML" do
it "returns HTML" do
- expect(closure_reason_text(merge_request, format: :html)).to eq(" via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
+ expect(closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
end
end
context "and format is unknown" do
it "returns plain text" do
- expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
end
end
end
@@ -29,7 +29,7 @@ describe EmailsHelper do
let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" }
it "returns plain text" do
- expect(closure_reason_text(closed_via)).to eq(" via #{closed_via}")
+ expect(closure_reason_text(closed_via)).to eq("via #{closed_via}")
end
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 143b28728a3..027480143bd 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -101,7 +101,7 @@ describe GitlabRoutingHelper do
it 'returns project milestone edit path when given entity parent is not a Group' do
milestone = create(:milestone, group: nil)
- expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/milestones/#{milestone.iid}/edit")
+ expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/-/milestones/#{milestone.iid}/edit")
end
end
end
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index d3e6eb78e5a..68e66346bfd 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -12,6 +12,7 @@ import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
+import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('Store', () => {
@@ -53,6 +54,39 @@ describe('Store', () => {
});
});
+ describe('toggleFilter', () => {
+ const dummyFilter = 'x=42';
+ let updateTokensSpy;
+
+ beforeEach(() => {
+ updateTokensSpy = jasmine.createSpy('updateTokens');
+ eventHub.$once('updateTokens', updateTokensSpy);
+
+ // prevent using window.history
+ spyOn(boardsStore, 'updateFiltersUrl').and.callFake(() => {});
+ });
+
+ it('adds the filter if it is not present', () => {
+ boardsStore.filter.path = 'something';
+
+ boardsStore.toggleFilter(dummyFilter);
+
+ expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`);
+ expect(updateTokensSpy).toHaveBeenCalled();
+ expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
+ });
+
+ it('removes the filter if it is present', () => {
+ boardsStore.filter.path = `something&${dummyFilter}`;
+
+ boardsStore.toggleFilter(dummyFilter);
+
+ expect(boardsStore.filter.path).toEqual('something');
+ expect(updateTokensSpy).toHaveBeenCalled();
+ expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
+ });
+ });
+
describe('lists', () => {
it('creates new list without persisting to DB', () => {
boardsStore.addList(listObj);
@@ -287,4 +321,27 @@ describe('Store', () => {
expect(boardsStore.detail.issue).toEqual({});
});
});
+
+ describe('setIssueDetail', () => {
+ it('sets issue details', () => {
+ boardsStore.detail.issue = 'some details';
+
+ const dummyValue = 'new details';
+ boardsStore.setIssueDetail(dummyValue);
+
+ expect(boardsStore.detail.issue).toEqual(dummyValue);
+ });
+ });
+
+ describe('startMoving', () => {
+ it('stores list and issue', () => {
+ const dummyIssue = 'some issue';
+ const dummyList = 'some list';
+
+ boardsStore.startMoving(dummyList, dummyIssue);
+
+ expect(boardsStore.moving.issue).toEqual(dummyIssue);
+ expect(boardsStore.moving.list).toEqual(dummyList);
+ });
+ });
});
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 93a0f29af0a..9854cf49e97 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -47,7 +47,7 @@ export const BoardsMockData = {
},
],
},
- '/test/issue-boards/milestones.json': [
+ '/test/issue-boards/-/milestones.json': [
{
id: 1,
title: 'test',
@@ -58,10 +58,10 @@ export const BoardsMockData = {
'/test/-/boards/1/lists': listObj,
},
PUT: {
- '/test/issue-boards/board/1/lists{/id}': {},
+ '/test/issue-boards/-/board/1/lists{/id}': {},
},
DELETE: {
- '/test/issue-boards/board/1/lists{/id}': {},
+ '/test/issue-boards/-/board/1/lists{/id}': {},
},
};
@@ -71,7 +71,7 @@ export const boardsMockInterceptor = config => {
};
export const mockBoardService = (opts = {}) => {
- const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/boards.json';
+ const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
diff --git a/spec/javascripts/diffs/components/commit_item_spec.js b/spec/javascripts/diffs/components/commit_item_spec.js
index 8fc9b10dd0b..cfe0c4bad71 100644
--- a/spec/javascripts/diffs/components/commit_item_spec.js
+++ b/spec/javascripts/diffs/components/commit_item_spec.js
@@ -8,7 +8,7 @@ import getDiffWithCommit from '../mock_data/diff_with_commit';
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
-const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=36`;
+const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
@@ -65,7 +65,7 @@ describe('diffs/components/commit_item', () => {
const imgElement = avatarElement.querySelector('img');
expect(avatarElement).toHaveAttr('href', commit.author.web_url);
- expect(imgElement).toHaveClass('s36');
+ expect(imgElement).toHaveClass('s40');
expect(imgElement).toHaveAttr('alt', commit.author.name);
expect(imgElement).toHaveAttr('src', commit.author.avatar_url);
});
diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js
index 19e27388eeb..121e99c9783 100644
--- a/spec/javascripts/helpers/vue_test_utils_helper.js
+++ b/spec/javascripts/helpers/vue_test_utils_helper.js
@@ -16,4 +16,6 @@ const vNodeContainsText = (vnode, text) =>
* @param {String} text
*/
export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
- !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length;
+ Boolean(
+ shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
+ );
diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js
index 406527b08a3..7d1921cabcf 100644
--- a/spec/javascripts/matchers.js
+++ b/spec/javascripts/matchers.js
@@ -28,7 +28,7 @@ export default {
reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
);
const result = {
- pass: !!matchingIcon,
+ pass: Boolean(matchingIcon),
};
if (result.pass) {
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 58bcd916739..cea8cb18918 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -176,10 +176,6 @@ describe('Dashboard', () => {
store,
});
- component.$store.commit(
- `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
- '/environments',
- );
component.$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, []);
component.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
@@ -211,10 +207,6 @@ describe('Dashboard', () => {
});
component.$store.commit(
- `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
- '/environments',
- );
- component.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -401,7 +393,7 @@ describe('Dashboard', () => {
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
- externalDashboardPath: '/mockPath',
+ externalDashboardUrl: '/mockUrl',
},
store,
});
@@ -427,7 +419,7 @@ describe('Dashboard', () => {
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
- externalDashboardPath: '',
+ externalDashboardUrl: '',
},
store,
});
diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js
index b61e0ac872f..106a3ba94e4 100644
--- a/spec/javascripts/projects/project_new_spec.js
+++ b/spec/javascripts/projects/project_new_spec.js
@@ -10,7 +10,17 @@ describe('New Project', () => {
setFixtures(`
<div class='toggle-import-form'>
<div class='import-url-data'>
- <input id="project_import_url" />
+ <div class="form-group">
+ <input id="project_import_url" />
+ </div>
+ <div id="import-url-auth-method">
+ <div class="form-group">
+ <input id="project-import-url-user" />
+ </div>
+ <div class="form-group">
+ <input id="project_import_url_password" />
+ </div>
+ </div>
<input id="project_name" />
<input id="project_path" />
</div>
@@ -119,7 +129,7 @@ describe('New Project', () => {
});
it('changes project path for HTTPS URL in $projectImportUrl', () => {
- $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git');
+ $projectImportUrl.val('https://gitlab.company.com/group/project.git');
projectNew.deriveProjectPathFromUrl($projectImportUrl);
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
index b9718a78fa4..8e0415b813b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -21,7 +21,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
- setToMWPSBy: {},
+ setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
@@ -106,7 +106,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.merge).toHaveBeenCalledWith({
sha,
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
should_remove_source_branch: true,
});
done();
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 368c997d318..3ae773b6ccb 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -80,7 +80,7 @@ describe('ReadyToMerge', () => {
it('should have default data', () => {
expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.showCommitMessageEditor).toBeFalsy();
expect(vm.isMakingRequest).toBeFalsy();
expect(vm.isMergingImmediately).toBeFalsy();
@@ -91,17 +91,17 @@ describe('ReadyToMerge', () => {
});
describe('computed', () => {
- describe('shouldShowMergeWhenPipelineSucceedsText', () => {
+ describe('shouldShowAutoMergeText', () => {
it('should return true with active pipeline', () => {
vm.mr.isPipelineActive = true;
- expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeTruthy();
+ expect(vm.shouldShowAutoMergeText).toBeTruthy();
});
it('should return false with inactive pipeline', () => {
vm.mr.isPipelineActive = false;
- expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeFalsy();
+ expect(vm.shouldShowAutoMergeText).toBeFalsy();
});
});
@@ -325,16 +325,20 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(true);
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeTruthy();
+ expect(vm.autoMergeStrategy).toBe('merge_when_pipeline_succeeds');
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
const params = vm.service.merge.calls.argsFor(0)[0];
- expect(params.sha).toEqual(vm.mr.sha);
- expect(params.commit_message).toEqual(vm.mr.commitMessage);
- expect(params.should_remove_source_branch).toBeFalsy();
- expect(params.merge_when_pipeline_succeeds).toBeTruthy();
+ expect(params).toEqual(
+ jasmine.objectContaining({
+ sha: vm.mr.sha,
+ commit_message: vm.mr.commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
done();
}, 333);
});
@@ -345,7 +349,7 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(false, true);
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
@@ -363,7 +367,7 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick();
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy();
expect(vm.initiateMergePolling).toHaveBeenCalled();
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 7653c10b94b..918717c4547 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -21,7 +21,6 @@ describe('mrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
beforeEach(() => {
- gon.features = { approvalRules: false };
// Prevent component mounting
delete mrWidgetOptions.el;
@@ -32,7 +31,6 @@ describe('mrWidgetOptions', () => {
});
afterEach(() => {
- gon.features = null;
vm.$destroy();
});
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
index b9059b85fdc..cce1cd0b284 100644
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -70,5 +70,47 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('href').value).to eq(invalid_link)
end
end
+
+ context "when the slug is deemed unsafe or invalid" do
+ let(:link) { "alert(1);" }
+
+ invalid_slugs = [
+ "javascript:",
+ "JaVaScRiPt:",
+ "\u0001java\u0003script:",
+ "javascript :",
+ "javascript: ",
+ "javascript : ",
+ ":javascript:",
+ "javascript&#58;",
+ "javascript&#0058;",
+ "javascript&#x3A;",
+ "javascript&#x003A;",
+ "java\0script:",
+ " &#14; javascript:"
+ ]
+
+ invalid_slugs.each do |slug|
+ context "with the slug #{slug}" do
+ it "doesn't rewrite a (.) relative link" do
+ filtered_link = filter(
+ "<a href='.#{link}'>Link</a>",
+ project_wiki: wiki,
+ page_slug: slug).children[0]
+
+ expect(filtered_link.attribute('href').value).not_to include(slug)
+ end
+
+ it "doesn't rewrite a (..) relative link" do
+ filtered_link = filter(
+ "<a href='..#{link}'>Link</a>",
+ project_wiki: wiki,
+ page_slug: slug).children[0]
+
+ expect(filtered_link.attribute('href').value).not_to include(slug)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index aaeec953e4b..718649e0e10 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -13,10 +13,10 @@ describe Banzai::Redactor do
it 'redacts an array of documents' do
doc1 = Nokogiri::HTML
- .fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+ .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">foo</a>')
doc2 = Nokogiri::HTML
- .fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+ .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">bar</a>')
redacted_data = redactor.redact([doc1, doc2])
@@ -27,7 +27,7 @@ describe Banzai::Redactor do
end
it 'replaces redacted reference with inner HTML' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue'>foo</a>")
redactor.redact([doc])
expect(doc.to_html).to eq('foo')
end
@@ -35,20 +35,24 @@ describe Banzai::Redactor do
context 'when data-original attribute provided' do
let(:original_content) { '<code>foo</code>' }
it 'replaces redacted reference with original content' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
redactor.redact([doc])
expect(doc.to_html).to eq(original_content)
end
- end
-
- it 'returns <a> tag with original href if it is originally a link reference' do
- href = 'http://localhost:3000'
- doc = Nokogiri::HTML
- .fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
- redactor.redact([doc])
+ it 'does not replace redacted reference with original content if href is given' do
+ html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Marge'>Marge</a>"
+ doc = Nokogiri::HTML.fragment(html)
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Marge</a>')
+ end
- expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
+ it 'uses the original content as the link content if given' do
+ html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Homer'>Marge</a>"
+ doc = Nokogiri::HTML.fragment(html)
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Homer</a>')
+ end
end
end
@@ -61,7 +65,7 @@ describe Banzai::Redactor do
end
it 'redacts an issue attached' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
redactor.redact([doc])
@@ -69,7 +73,7 @@ describe Banzai::Redactor do
end
it 'redacts an external issue' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
redactor.redact([doc])
diff --git a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
new file mode 100644
index 00000000000..740781f1aa5
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::BackgroundMigration::ResetMergeStatus, :migration, schema: 20190528180441 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, extra_params = {})
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }.merge(extra_params)
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly updates opened mergeable MRs to unchecked' do
+ create_merge_request(1, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(2, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(3, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(4, state: 'merged', merge_status: 'can_be_merged')
+ create_merge_request(5, state: 'opened', merge_status: 'cannot_be_merged')
+
+ subject.perform(1, 5)
+
+ expected_rows = [
+ { id: 1, state: 'opened', merge_status: 'unchecked' },
+ { id: 2, state: 'opened', merge_status: 'unchecked' },
+ { id: 3, state: 'opened', merge_status: 'unchecked' },
+ { id: 4, state: 'merged', merge_status: 'can_be_merged' },
+ { id: 5, state: 'opened', merge_status: 'cannot_be_merged' }
+ ]
+
+ rows = merge_requests.order(:id).map do |row|
+ row.attributes.slice('id', 'state', 'merge_status').symbolize_keys
+ end
+
+ expect(rows).to eq(expected_rows)
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index a02c00e3340..2e90f6c7f71 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::BitbucketImport::Importer do
before do
stub_omniauth_provider('bitbucket')
+ stub_feature_flags(stricter_mr_branch_name: false)
end
let(:statuses) do
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index d5bd139b5f1..d31866a1987 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -112,6 +112,16 @@ describe Gitlab::Ci::Config::Entry::Service do
it 'is valid' do
expect(entry).to be_valid
end
+
+ context 'when unknown port keys detected' do
+ let(:ports) { [{ number: 80, invalid_key: 'foo' }] }
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors.first)
+ .to match /port config contains unknown keys: invalid_key/
+ end
+ end
end
describe '#ports' do
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index d8a61618e77..46d68097fff 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Remote do
+ include StubRequests
+
let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) }
let(:params) { { remote: location } }
let(:remote_file) { described_class.new(params, context) }
@@ -46,7 +48,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe "#valid?" do
context 'when is a valid remote url' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content)
+ stub_full_request(location).to_return(body: remote_file_content)
end
it 'returns true' do
@@ -92,7 +94,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe "#content" do
context 'with a valid remote file' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content)
+ stub_full_request(location).to_return(body: remote_file_content)
end
it 'returns the content of the file' do
@@ -114,7 +116,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
before do
- WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error'))
+ stub_full_request(location).to_raise(SocketError.new('Some HTTP error'))
end
it 'is nil' do
@@ -144,7 +146,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when timeout error has been raised' do
before do
- WebMock.stub_request(:get, location).to_timeout
+ stub_full_request(location).to_timeout
end
it 'returns error message about a timeout' do
@@ -154,7 +156,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when HTTP error has been raised' do
before do
- WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error)
+ stub_full_request(location).to_raise(Gitlab::HTTP::Error)
end
it 'returns error message about a HTTP error' do
@@ -164,7 +166,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when response has 404 status' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404)
+ stub_full_request(location).to_return(body: remote_file_content, status: 404)
end
it 'returns error message about a timeout' do
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 136974569de..e068b786b02 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::Mapper do
+ include StubRequests
+
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
@@ -18,7 +20,7 @@ describe Gitlab::Ci::Config::External::Mapper do
end
before do
- WebMock.stub_request(:get, remote_url).to_return(body: file_content)
+ stub_full_request(remote_url).to_return(body: file_content)
end
describe '#process' do
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 0f58a4f1d44..856187371e1 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::Processor do
+ include StubRequests
+
set(:project) { create(:project, :repository) }
set(:another_project) { create(:project, :repository) }
set(:user) { create(:user) }
@@ -42,7 +44,7 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) { { include: remote_file, image: 'ruby:2.2' } }
before do
- WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error'))
+ stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error'))
end
it 'raises an error' do
@@ -75,7 +77,7 @@ describe Gitlab::Ci::Config::External::Processor do
end
before do
- WebMock.stub_request(:get, remote_file).to_return(body: external_file_content)
+ stub_full_request(remote_file).to_return(body: external_file_content)
end
it 'appends the file to the values' do
@@ -145,7 +147,7 @@ describe Gitlab::Ci::Config::External::Processor do
allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
.to receive(:fetch_local_content).and_return(local_file_content)
- WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
+ stub_full_request(remote_file).to_return(body: remote_file_content)
end
it 'appends the files to the values' do
@@ -191,7 +193,8 @@ describe Gitlab::Ci::Config::External::Processor do
end
it 'takes precedence' do
- WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
+ stub_full_request(remote_file).to_return(body: remote_file_content)
+
expect(processor.perform[:image]).to eq('ruby:2.2')
end
end
@@ -231,7 +234,8 @@ describe Gitlab::Ci::Config::External::Processor do
HEREDOC
end
- WebMock.stub_request(:get, 'http://my.domain.com/config.yml').to_return(body: 'remote_build: { script: echo Hello World }')
+ stub_full_request('http://my.domain.com/config.yml')
+ .to_return(body: 'remote_build: { script: echo Hello World }')
end
context 'when project is public' do
@@ -273,8 +277,10 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when config includes an external configuration file via SSL web request' do
before do
- stub_request(:get, 'https://sha256.badssl.com/fake.yml').to_return(body: 'image: ruby:2.6', status: 200)
- stub_request(:get, 'https://self-signed.badssl.com/fake.yml')
+ stub_full_request('https://sha256.badssl.com/fake.yml', ip_address: '8.8.8.8')
+ .to_return(body: 'image: ruby:2.6', status: 200)
+
+ stub_full_request('https://self-signed.badssl.com/fake.yml', ip_address: '8.8.8.9')
.to_raise(OpenSSL::SSL::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed (self signed certificate)'))
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 092e9f242b7..7f336ee853e 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config do
+ include StubRequests
+
set(:user) { create(:user) }
let(:config) do
@@ -216,8 +218,7 @@ describe Gitlab::Ci::Config do
end
before do
- WebMock.stub_request(:get, remote_location)
- .to_return(body: remote_file_content)
+ stub_full_request(remote_location).to_return(body: remote_file_content)
allow(project.repository)
.to receive(:blob_data_at).and_return(local_file_content)
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 0d998d89d73..29276d5b686 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
module Gitlab
module Ci
describe YamlProcessor do
+ include StubRequests
+
subject { described_class.new(config, user: nil) }
describe '#build_attributes' do
@@ -648,7 +650,7 @@ module Gitlab
end
before do
- WebMock.stub_request(:get, 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml')
+ stub_full_request('https://gitlab.com/awesome-project/raw/master/.before-script-template.yml')
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 7644d83992f..cb4701e8edc 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -29,51 +29,6 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:storage_path) { TestEnv.repos_path }
let(:user) { build(:user) }
- describe '.create_hooks' do
- let(:repo_path) { File.join(storage_path, 'hook-test.git') }
- let(:hooks_dir) { File.join(repo_path, 'hooks') }
- let(:target_hooks_dir) { Gitlab::Shell.new.hooks_path }
- let(:existing_target) { File.join(repo_path, 'foobar') }
-
- before do
- FileUtils.rm_rf(repo_path)
- FileUtils.mkdir_p(repo_path)
- end
-
- context 'hooks is a directory' do
- let(:existing_file) { File.join(hooks_dir, 'my-file') }
-
- before do
- FileUtils.mkdir_p(hooks_dir)
- FileUtils.touch(existing_file)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) }
- end
-
- context 'hooks is a valid symlink' do
- before do
- FileUtils.mkdir_p existing_target
- File.symlink(existing_target, hooks_dir)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- end
-
- context 'hooks is a broken symlink' do
- before do
- FileUtils.rm_f(existing_target)
- File.symlink(existing_target, hooks_dir)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- end
- end
-
describe "Respond to" do
subject { repository }
@@ -1959,13 +1914,6 @@ describe Gitlab::Git::Repository, :seed_helper do
expect { imported_repo.fsck }.not_to raise_exception
end
- it 'creates a symlink to the global hooks dir' do
- imported_repo.create_from_bundle(valid_bundle_path)
- hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
-
- expect(File.readlink(hooks_path)).to eq(Gitlab::Shell.new.hooks_path)
- end
-
it 'raises an error if the bundle is an attempted malicious payload' do
expect do
imported_repo.create_from_bundle(malicious_bundle_path)
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index 3ab04a1c46d..b63389af29f 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -1,31 +1,69 @@
require 'spec_helper'
describe Gitlab::GitRefValidator do
- it { expect(described_class.validate('feature/new')).to be_truthy }
- it { expect(described_class.validate('implement_@all')).to be_truthy }
- it { expect(described_class.validate('my_new_feature')).to be_truthy }
- it { expect(described_class.validate('my-branch')).to be_truthy }
- it { expect(described_class.validate('#1')).to be_truthy }
- it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy }
- it { expect(described_class.validate('feature/~new/')).to be_falsey }
- it { expect(described_class.validate('feature/^new/')).to be_falsey }
- it { expect(described_class.validate('feature/:new/')).to be_falsey }
- it { expect(described_class.validate('feature/?new/')).to be_falsey }
- it { expect(described_class.validate('feature/*new/')).to be_falsey }
- it { expect(described_class.validate('feature/[new/')).to be_falsey }
- it { expect(described_class.validate('feature/new/')).to be_falsey }
- it { expect(described_class.validate('feature/new.')).to be_falsey }
- it { expect(described_class.validate('feature\@{')).to be_falsey }
- it { expect(described_class.validate('feature\new')).to be_falsey }
- it { expect(described_class.validate('feature//new')).to be_falsey }
- it { expect(described_class.validate('feature new')).to be_falsey }
- it { expect(described_class.validate('refs/heads/')).to be_falsey }
- it { expect(described_class.validate('refs/remotes/')).to be_falsey }
- it { expect(described_class.validate('refs/heads/feature')).to be_falsey }
- it { expect(described_class.validate('refs/remotes/origin')).to be_falsey }
- it { expect(described_class.validate('-')).to be_falsey }
- it { expect(described_class.validate('-branch')).to be_falsey }
- it { expect(described_class.validate('.tag')).to be_falsey }
- it { expect(described_class.validate('my branch')).to be_falsey }
- it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey }
+ using RSpec::Parameterized::TableSyntax
+
+ context '.validate' do
+ it { expect(described_class.validate('feature/new')).to be true }
+ it { expect(described_class.validate('implement_@all')).to be true }
+ it { expect(described_class.validate('my_new_feature')).to be true }
+ it { expect(described_class.validate('my-branch')).to be true }
+ it { expect(described_class.validate('#1')).to be true }
+ it { expect(described_class.validate('feature/refs/heads/foo')).to be true }
+ it { expect(described_class.validate('feature/~new/')).to be false }
+ it { expect(described_class.validate('feature/^new/')).to be false }
+ it { expect(described_class.validate('feature/:new/')).to be false }
+ it { expect(described_class.validate('feature/?new/')).to be false }
+ it { expect(described_class.validate('feature/*new/')).to be false }
+ it { expect(described_class.validate('feature/[new/')).to be false }
+ it { expect(described_class.validate('feature/new/')).to be false }
+ it { expect(described_class.validate('feature/new.')).to be false }
+ it { expect(described_class.validate('feature\@{')).to be false }
+ it { expect(described_class.validate('feature\new')).to be false }
+ it { expect(described_class.validate('feature//new')).to be false }
+ it { expect(described_class.validate('feature new')).to be false }
+ it { expect(described_class.validate('refs/heads/')).to be false }
+ it { expect(described_class.validate('refs/remotes/')).to be false }
+ it { expect(described_class.validate('refs/heads/feature')).to be false }
+ it { expect(described_class.validate('refs/remotes/origin')).to be false }
+ it { expect(described_class.validate('-')).to be false }
+ it { expect(described_class.validate('-branch')).to be false }
+ it { expect(described_class.validate('+foo:bar')).to be false }
+ it { expect(described_class.validate('foo:bar')).to be false }
+ it { expect(described_class.validate('.tag')).to be false }
+ it { expect(described_class.validate('my branch')).to be false }
+ it { expect(described_class.validate("\xA0\u0000\xB0")).to be false }
+ end
+
+ context '.validate_merge_request_branch' do
+ it { expect(described_class.validate_merge_request_branch('HEAD')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/new')).to be true }
+ it { expect(described_class.validate_merge_request_branch('implement_@all')).to be true }
+ it { expect(described_class.validate_merge_request_branch('my_new_feature')).to be true }
+ it { expect(described_class.validate_merge_request_branch('my-branch')).to be true }
+ it { expect(described_class.validate_merge_request_branch('#1')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/refs/heads/foo')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/~new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/^new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/:new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/?new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/*new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/[new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/new.')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature\@{')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature\new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature//new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('refs/heads/master')).to be true }
+ it { expect(described_class.validate_merge_request_branch('refs/heads/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('refs/remotes/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('-')).to be false }
+ it { expect(described_class.validate_merge_request_branch('-branch')).to be false }
+ it { expect(described_class.validate_merge_request_branch('+foo:bar')).to be false }
+ it { expect(described_class.validate_merge_request_branch('foo:bar')).to be false }
+ it { expect(described_class.validate_merge_request_branch('.tag')).to be false }
+ it { expect(described_class.validate_merge_request_branch('my branch')).to be false }
+ it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false }
+ end
end
diff --git a/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb
new file mode 100644
index 00000000000..ec2fcad31e5
--- /dev/null
+++ b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader do
+ describe '#find' do
+ it 'only queries once for project statistics' do
+ stats = create_list(:project_statistics, 2)
+ project1 = stats.first.project
+ project2 = stats.last.project
+
+ expect do
+ described_class.new(project1.id).find
+ described_class.new(project2.id).find
+ end.not_to exceed_query_limit(1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
new file mode 100644
index 00000000000..930d1f62272
--- /dev/null
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HTTPConnectionAdapter do
+ describe '#connection' do
+ context 'when local requests are not allowed' do
+ it 'sets up the connection' do
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+
+ it 'raises error when it is a request to local address' do
+ uri = URI('http://172.16.0.0/12')
+
+ expect { described_class.new(uri).connection }
+ .to raise_error(Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://172.16.0.0/12' is blocked: Requests to the local network are not allowed")
+ end
+
+ it 'raises error when it is a request to localhost address' do
+ uri = URI('http://127.0.0.1')
+
+ expect { described_class.new(uri).connection }
+ .to raise_error(Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed")
+ end
+
+ context 'when port different from URL scheme is used' do
+ it 'sets up the addr_port accordingly' do
+ uri = URI('https://example.org:8080')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org:8080')
+ expect(connection.port).to eq(8080)
+ end
+ end
+ end
+
+ context 'when DNS rebinding protection is disabled' do
+ it 'sets up the connection' do
+ stub_application_setting(dns_rebinding_protection_enabled: false)
+
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'when http(s) environment variable is set' do
+ it 'sets up the connection' do
+ stub_env('https_proxy' => 'https://my.proxy')
+
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'when local requests are allowed' do
+ it 'sets up the connection' do
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+
+ it 'sets up the connection when it is a local network' do
+ uri = URI('http://172.16.0.0/12')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('172.16.0.0')
+ expect(connection.hostname_override).to be(nil)
+ expect(connection.addr_port).to eq('172.16.0.0')
+ expect(connection.port).to eq(80)
+ end
+
+ it 'sets up the connection when it is localhost' do
+ uri = URI('http://127.0.0.1')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('127.0.0.1')
+ expect(connection.hostname_override).to be(nil)
+ expect(connection.addr_port).to eq('127.0.0.1')
+ expect(connection.port).to eq(80)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 6c37c157f5d..158f77cab2c 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -1,6 +1,28 @@
require 'spec_helper'
describe Gitlab::HTTP do
+ include StubRequests
+
+ context 'when allow_local_requests' do
+ it 'sends the request to the correct URI' do
+ stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200)
+
+ described_class.get('https://example.org:8080', allow_local_requests: false)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once
+ end
+ end
+
+ context 'when not allow_local_requests' do
+ it 'sends the request to the correct URI' do
+ stub_full_request('https://example.org:8080')
+
+ described_class.get('https://example.org:8080', allow_local_requests: true)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once
+ end
+ end
+
describe 'allow_local_requests_from_hooks_and_services is' do
before do
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
@@ -21,6 +43,8 @@ describe Gitlab::HTTP do
context 'if allow_local_requests set to true' do
it 'override the global value and allow requests to localhost or private network' do
+ stub_full_request('http://localhost:3003')
+
expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
end
end
@@ -32,6 +56,8 @@ describe Gitlab::HTTP do
end
it 'allow requests to localhost' do
+ stub_full_request('http://localhost:3003')
+
expect { described_class.get('http://localhost:3003') }.not_to raise_error
end
@@ -49,7 +75,7 @@ describe Gitlab::HTTP do
describe 'handle redirect loops' do
before do
- WebMock.stub_request(:any, "http://example.org").to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
+ stub_full_request("http://example.org", method: :any).to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
end
it 'handles GET requests' do
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 7c4ac62790e..21a227335cd 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
+ include StubRequests
+
let(:example_url) { 'http://www.example.com' }
let(:strategy) { subject.new(url: example_url, http_method: 'post') }
let!(:project) { create(:project, :with_export) }
@@ -35,7 +37,7 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
context 'when upload fails' do
it 'stores the export error' do
- stub_request(:post, example_url).to_return(status: [404, 'Page not found'])
+ stub_full_request(example_url, method: :post).to_return(status: [404, 'Page not found'])
strategy.execute(user, project)
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 536cc359d39..99669285d5b 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -18,7 +18,11 @@ describe Gitlab::ImportExport::AttributeCleaner do
'notid' => 99,
'import_source' => 'whatever',
'import_type' => 'whatever',
- 'non_existent_attr' => 'whatever'
+ 'non_existent_attr' => 'whatever',
+ 'some_html' => '<p>dodgy html</p>',
+ 'legit_html' => '<p>legit html</p>',
+ '_html' => '<p>perfectly ordinary html</p>',
+ 'cached_markdown_version' => 12345
}
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 4a7accc4c52..fb7bddb386c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -158,6 +158,8 @@
{
"id": 351,
"note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.",
+ "note_html": "<p>something else entirely</p>",
+ "cached_markdown_version": 917504,
"noteable_type": "Issue",
"author_id": 26,
"created_at": "2016-06-14T15:02:47.770Z",
@@ -2363,6 +2365,8 @@
{
"id": 671,
"note": "Sit voluptatibus eveniet architecto quidem.",
+ "note_html": "<p>something else entirely</p>",
+ "cached_markdown_version": 917504,
"noteable_type": "MergeRequest",
"author_id": 26,
"created_at": "2016-06-14T15:02:56.632Z",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 651aa600fb2..ca46006ea58 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -58,6 +58,26 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end
+ context 'when importing a project with cached_markdown_version and note_html' do
+ context 'for an Issue' do
+ it 'does not import note_html' do
+ note_content = 'Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi'
+ issue_note = Issue.find_by(description: 'Aliquam enim illo et possimus.').notes.select { |n| n.note.match(/#{note_content}/)}.first
+
+ expect(issue_note.note_html).to match(/#{note_content}/)
+ end
+ end
+
+ context 'for a Merge Request' do
+ it 'does not import note_html' do
+ note_content = 'Sit voluptatibus eveniet architecto quidem'
+ merge_request_note = MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{note_content}/)}.first
+
+ expect(merge_request_note.note_html).to match(/#{note_content}/)
+ end
+ end
+ end
+
it 'creates a valid pipeline note' do
expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 8a699eb1461..e2ffb2adb9b 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -34,11 +34,5 @@ describe Gitlab::ImportExport::RepoRestorer do
it 'restores the repo successfully' do
expect(restorer.restore).to be_truthy
end
-
- it 'has the webhooks' do
- restorer.restore
-
- expect(project_hook_exists?(project)).to be true
- end
end
end
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
index c471c30a194..f4a6e1fc7d9 100644
--- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -61,6 +61,33 @@ describe Gitlab::Metrics::Samplers::PumaSampler do
end
end
+ context 'with empty worker stats' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "workers": 2,
+ "phase": 2,
+ "booted_workers": 2,
+ "old_workers": 0,
+ "worker_status": [{
+ "pid": 32534,
+ "index": 0,
+ "phase": 1,
+ "booted": true,
+ "last_checkin": "2019-05-15T07:57:55Z",
+ "last_status": {}
+ }]
+ }
+ EOS
+ end
+
+ it 'does not log worker stats' do
+ expect(subject).not_to receive(:set_worker_metrics)
+
+ subject.sample
+ end
+ end
+
context 'in single mode' do
let(:puma_stats) do
<<~EOS
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 7972ff253fe..aaf8c9fa2a0 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -10,17 +10,20 @@ describe Gitlab::Metrics::Samplers::RubySampler do
describe '#sample' do
it 'samples various statistics' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:cpu_time)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:process_start_time)
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
expect(sampler).to receive(:sample_gc)
sampler.sample
end
- it 'adds a metric containing the memory usage' do
+ it 'adds a metric containing the process resident memory bytes' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
- expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
sampler.sample
end
@@ -34,6 +37,27 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
+ it 'adds a metric containing the process total cpu time' do
+ expect(Gitlab::Metrics::System).to receive(:cpu_time).and_return(0.51)
+ expect(sampler.metrics[:process_cpu_seconds_total]).to receive(:set).with({}, 0.51)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process start time' do
+ expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345)
+ expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process max file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024)
+ expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024)
+
+ sampler.sample
+ end
+
it 'clears any GC profiles' do
expect(GC::Profiler).to receive(:clear)
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 4b03f3c2532..090e456644f 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -39,8 +39,8 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'unix', socket_address: socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
@@ -50,7 +50,6 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
context 'unicorn listens on tcp sockets' do
let(:tcp_socket_address) { '0.0.0.0:8080' }
let(:tcp_sockets) { [tcp_socket_address] }
-
before do
allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
end
@@ -71,13 +70,29 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
end
end
+
+ context 'additional metrics' do
+ let(:unicorn_workers) { 2 }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return([""])
+ allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14)
+ allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers)
+ end
+
+ it "sets additional metrics" do
+ expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers)
+
+ subject.sample
+ end
+ end
end
describe '#start' do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 14afcdf5daa..b0603d96eb2 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -13,6 +13,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to be > 0
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns the max allowed open file descriptors' do
+ expect(described_class.max_open_file_descriptors).to be > 0
+ end
+ end
+
+ describe '.process_start_time' do
+ it 'returns the process start time' do
+ expect(described_class.process_start_time).to be > 0
+ end
+ end
else
describe '.memory_usage' do
it 'returns 0.0' do
@@ -25,6 +37,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to eq(0)
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns 0' do
+ expect(described_class.max_open_file_descriptors).to eq(0)
+ end
+ end
+
+ describe 'process_start_time' do
+ it 'returns 0' do
+ expect(described_class.process_start_time).to eq(0)
+ end
+ end
end
describe '.cpu_time' do
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 312aa3be490..3d27156b356 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -256,4 +256,28 @@ describe Gitlab::SearchResults do
expect(results.objects('merge_requests')).not_to include merge_request
end
+
+ context 'milestones' do
+ it 'returns correct set of milestones' do
+ private_project_1 = create(:project, :private)
+ private_project_2 = create(:project, :private)
+ internal_project = create(:project, :internal)
+ public_project_1 = create(:project, :public)
+ public_project_2 = create(:project, :public, :issues_disabled, :merge_requests_disabled)
+ private_project_1.add_developer(user)
+ # milestones that should not be visible
+ create(:milestone, project: private_project_2, title: 'Private project without access milestone')
+ create(:milestone, project: public_project_2, title: 'Public project with milestones disabled milestone')
+ # milestones that should be visible
+ milestone_1 = create(:milestone, project: private_project_1, title: 'Private project with access milestone', state: 'closed')
+ milestone_2 = create(:milestone, project: internal_project, title: 'Internal project milestone')
+ milestone_3 = create(:milestone, project: public_project_1, title: 'Public project with milestones enabled milestone')
+ # Global search scope takes user authorized projects, internal projects and public projects.
+ limit_projects = ProjectsFinder.new(current_user: user).execute
+
+ milestones = described_class.new(user, limit_projects, 'milestone').objects('milestones')
+
+ expect(milestones).to match_array([milestone_1, milestone_2, milestone_3])
+ end
+ end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index e2f09de2808..bce2e754176 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -612,16 +612,6 @@ describe Gitlab::Shell do
FileUtils.rm_rf(created_path)
end
- it 'creates a repository' do
- expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_truthy
-
- expect(File.stat(created_path).mode & 0o777).to eq(0o770)
-
- hooks_path = File.join(created_path, 'hooks')
- expect(File.lstat(hooks_path)).to be_symlink
- expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
- end
-
it 'returns false when the command fails' do
FileUtils.mkdir_p(File.dirname(created_path))
# This file will block the creation of the repo's .git directory. That
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 445a56ab0d8..253366e0789 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,6 +2,87 @@
require 'spec_helper'
describe Gitlab::UrlBlocker do
+ describe '#validate!' do
+ context 'when URI is nil' do
+ let(:import_url) { nil }
+
+ it 'returns no URI and hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to be(nil)
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'when URI is internal' do
+ let(:import_url) { 'http://localhost' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('http://[::1]'))
+ expect(hostname).to eq('localhost')
+ end
+ end
+
+ context 'when the URL hostname is a domain' do
+ let(:import_url) { 'https://example.org' }
+
+ it 'returns URI and hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to eq('example.org')
+ end
+ end
+
+ context 'when the URL hostname is an IP address' do
+ let(:import_url) { 'https://93.184.216.34' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'disabled DNS rebinding protection' do
+ context 'when URI is internal' do
+ let(:import_url) { 'http://localhost' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('http://localhost'))
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'when the URL hostname is a domain' do
+ let(:import_url) { 'https://example.org' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('https://example.org'))
+ expect(hostname).to eq(nil)
+ end
+ end
+
+ context 'when the URL hostname is an IP address' do
+ let(:import_url) { 'https://93.184.216.34' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to be(nil)
+ end
+ end
+ end
+ end
+
describe '#blocked_url?' do
let(:ports) { Project::VALID_IMPORT_PORTS }
@@ -208,7 +289,7 @@ describe Gitlab::UrlBlocker do
end
def stub_domain_resolv(domain, ip)
- address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)
+ address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false, ipv4?: false)
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 9f495a5d50b..bbcb92608d8 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::UrlBuilder do
url = described_class.build(milestone)
- expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/milestones/#{milestone.iid}"
+ expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/-/milestones/#{milestone.iid}"
end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 5861e6955a6..7242255d535 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -115,6 +115,40 @@ describe Gitlab::UrlSanitizer do
end
end
+ describe '#user' do
+ context 'credentials in hash' do
+ it 'overrides URL-provided user' do
+ sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' })
+
+ expect(sanitizer.user).to eq('c')
+ end
+ end
+
+ context 'credentials in URL' do
+ where(:url, :user) do
+ 'http://foo:bar@example.com' | 'foo'
+ 'http://foo:bar:baz@example.com' | 'foo'
+ 'http://:bar@example.com' | nil
+ 'http://foo:@example.com' | 'foo'
+ 'http://foo@example.com' | 'foo'
+ 'http://:@example.com' | nil
+ 'http://@example.com' | nil
+ 'http://example.com' | nil
+
+ # Other invalid URLs
+ nil | nil
+ '' | nil
+ 'no' | nil
+ end
+
+ with_them do
+ subject { described_class.new(url).user }
+
+ it { is_expected.to eq(user) }
+ end
+ end
+ end
+
describe '#full_url' do
context 'credentials in hash' do
where(:credentials, :userinfo) do
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 767b5779a79..e075904b0cc 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -109,4 +109,34 @@ describe Gitlab do
expect(described_class.ee?).to eq(false)
end
end
+
+ describe '.http_proxy_env?' do
+ it 'returns true when lower case https' do
+ stub_env('https_proxy', 'https://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when upper case https' do
+ stub_env('HTTPS_PROXY', 'https://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when lower case http' do
+ stub_env('http_proxy', 'http://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when upper case http' do
+ stub_env('HTTP_PROXY', 'http://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns false when not set' do
+ expect(described_class.http_proxy_env?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 77fea5b2d24..346455067a7 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Mattermost::Session, type: :request do
include ExclusiveLeaseHelpers
+ include StubRequests
let(:user) { create(:user) }
@@ -24,7 +25,7 @@ describe Mattermost::Session, type: :request do
let(:location) { 'http://location.tld' }
let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
let!(:stub) do
- WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login")
+ stub_full_request("#{mattermost_url}/oauth/gitlab/login")
.to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302)
end
@@ -63,7 +64,7 @@ describe Mattermost::Session, type: :request do
end
before do
- WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete")
+ stub_full_request("#{mattermost_url}/signup/gitlab/complete")
.with(query: hash_including({ 'state' => state }))
.to_return do |request|
post "/oauth/token",
@@ -80,7 +81,7 @@ describe Mattermost::Session, type: :request do
end
end
- WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout")
+ stub_full_request("#{mattermost_url}/api/v4/users/logout", method: :post)
.to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
end
diff --git a/spec/migrations/enqueue_reset_merge_status_spec.rb b/spec/migrations/enqueue_reset_merge_status_spec.rb
new file mode 100644
index 00000000000..0d5e33bfd46
--- /dev/null
+++ b/spec/migrations/enqueue_reset_merge_status_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190528180441_enqueue_reset_merge_status.rb')
+
+describe EnqueueResetMergeStatus, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, extra_params = {})
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }.merge(extra_params)
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly schedules background migrations' do
+ create_merge_request(1, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(2, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(3, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(4, state: 'merged', merge_status: 'can_be_merged')
+ create_merge_request(5, state: 'opened', merge_status: 'unchecked')
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(5.minutes, 1, 2)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(10.minutes, 3, 3)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/generate_missing_routes_spec.rb b/spec/migrations/generate_missing_routes_spec.rb
index 32515d353b0..30ad135d4df 100644
--- a/spec/migrations/generate_missing_routes_spec.rb
+++ b/spec/migrations/generate_missing_routes_spec.rb
@@ -8,7 +8,7 @@ describe GenerateMissingRoutes, :migration do
let(:routes) { table(:routes) }
it 'creates routes for projects without a route' do
- namespace = namespaces.create!(name: 'GitLab', path: 'gitlab')
+ namespace = namespaces.create!(name: 'GitLab', path: 'gitlab', type: 'Group')
routes.create!(
path: 'gitlab',
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index e6d682c24d9..1ba66565e03 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
describe Ci::JobArtifact do
let(:artifact) { create(:ci_job_artifact, :archive) }
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 106365) }
- end
-
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:job) }
@@ -23,6 +19,10 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 106365) }
+ end
+
describe '.with_reports' do
let!(:artifact) { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 42d4769a921..6382be73ea7 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -48,32 +48,116 @@ describe Ci::PipelineSchedule do
end
end
+ describe '.runnable_schedules' do
+ subject { described_class.runnable_schedules }
+
+ let!(:pipeline_schedule) do
+ Timecop.freeze(1.day.ago) do
+ create(:ci_pipeline_schedule, :hourly)
+ end
+ end
+
+ it 'returns the runnable schedule' do
+ is_expected.to eq([pipeline_schedule])
+ end
+
+ context 'when there are no runnable schedules' do
+ let!(:pipeline_schedule) { }
+
+ it 'returns an empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.preloaded' do
+ subject { described_class.preloaded }
+
+ before do
+ create_list(:ci_pipeline_schedule, 3)
+ end
+
+ it 'preloads the associations' do
+ subject
+
+ query = ActiveRecord::QueryRecorder.new { subject.each(&:project) }
+
+ expect(query.count).to eq(2)
+ end
+ end
+
describe '#set_next_run_at' do
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+ let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(ideal_next_run_at)
+ end
+
+ let(:cron_worker_next_run_at) do
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(Time.now)
+ end
context 'when creates new pipeline schedule' do
- let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone)
- .next_time_from(Time.now)
+ it 'updates next_run_at automatically' do
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
end
+ end
- it 'updates next_run_at automatically' do
- expect(described_class.last.next_run_at).to eq(expected_next_run_at)
+ context 'when PipelineScheduleWorker runs at a specific interval' do
+ before do
+ allow(Settings).to receive(:cron_jobs) do
+ {
+ 'pipeline_schedule_worker' => {
+ 'cron' => '0 1 2 3 *'
+ }
+ }
+ end
+ end
+
+ it "updates next_run_at to the sidekiq worker's execution time" do
+ expect(pipeline_schedule.next_run_at.min).to eq(0)
+ expect(pipeline_schedule.next_run_at.hour).to eq(1)
+ expect(pipeline_schedule.next_run_at.day).to eq(2)
+ expect(pipeline_schedule.next_run_at.month).to eq(3)
end
end
- context 'when updates cron of exsisted pipeline schedule' do
- let(:new_cron) { '0 0 1 1 *' }
+ context 'when pipeline schedule runs every minute' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) }
- let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone)
- .next_time_from(Time.now)
+ it "updates next_run_at to the sidekiq worker's execution time" do
+ expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at)
+ end
+ end
+
+ context 'when there are two different pipeline schedules in different time zones' do
+ let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') }
+ let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+
+ it 'sets different next_run_at' do
+ expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at)
+ end
+ end
+
+ context 'when there are two different pipeline schedules in the same time zones' do
+ let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+ let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+
+ it 'sets the sames next_run_at' do
+ expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at)
end
+ end
+
+ context 'when updates cron of exsisted pipeline schedule' do
+ let(:new_cron) { '0 0 1 1 *' }
it 'updates next_run_at automatically' do
pipeline_schedule.update!(cron: new_cron)
- expect(described_class.last.next_run_at).to eq(expected_next_run_at)
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
end
end
end
@@ -83,10 +167,11 @@ describe Ci::PipelineSchedule do
context 'when reschedules after 10 days from now' do
let(:future_time) { 10.days.from_now }
+ let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) }
let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone)
- .next_time_from(future_time)
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(ideal_next_run_at)
end
it 'points to proper next_run_at' do
@@ -99,38 +184,6 @@ describe Ci::PipelineSchedule do
end
end
- describe '#real_next_run' do
- subject do
- described_class.last.real_next_run(worker_cron: worker_cron,
- worker_time_zone: worker_time_zone)
- end
-
- context 'when GitLab time_zone is UTC' do
- before do
- allow(Time).to receive(:zone)
- .and_return(ActiveSupport::TimeZone[worker_time_zone])
- end
-
- let(:worker_time_zone) { 'UTC' }
-
- context 'when cron_timezone is Eastern Time (US & Canada)' do
- before do
- create(:ci_pipeline_schedule, :nightly,
- cron_timezone: 'Eastern Time (US & Canada)')
- end
-
- let(:worker_cron) { '0 1 2 3 *' }
-
- it 'returns the next time worker executes' do
- expect(subject.min).to eq(0)
- expect(subject.hour).to eq(1)
- expect(subject.day).to eq(2)
- expect(subject.month).to eq(3)
- end
- end
- end
- end
-
describe '#job_variables' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index a0319b3eb0a..a8701f0efa4 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1381,6 +1381,40 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'auto merge' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, :running, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ merge_request.update_head_pipeline
+ end
+
+ %w[succeed! drop! cancel! skip!].each do |action|
+ context "when the pipeline recieved #{action} event" do
+ it 'performs AutoMergeProcessWorker' do
+ expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id)
+
+ pipeline.public_send(action)
+ end
+ end
+ end
+
+ context 'when auto merge is not enabled in the merge request' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'performs AutoMergeProcessWorker' do
+ expect(AutoMergeProcessWorker).not_to receive(:perform_async)
+
+ pipeline.succeed!
+ end
+ end
+ end
+
def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
create(:ci_build, *traits,
name: name,
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d5974f47190..b38cf96de7e 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -3,9 +3,6 @@
require 'rails_helper'
describe Clusters::Applications::Knative do
- include KubernetesHelpers
- include ReactiveCachingHelpers
-
let(:knative) { create(:clusters_applications_knative) }
include_examples 'cluster application core specs', :clusters_applications_knative
@@ -146,77 +143,4 @@ describe Clusters::Applications::Knative do
describe 'validations' do
it { is_expected.to validate_presence_of(:hostname) }
end
-
- describe '#service_pod_details' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- let(:namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
-
- before do
- stub_kubeclient_discover(service.api_url)
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
- stub_reactive_cache(knative,
- {
- services: kube_response(kube_knative_services_body),
- pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
- })
- synchronous_reactive_cache(knative)
- end
-
- it 'is able k8s core for pod details' do
- expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
- end
- end
-
- describe '#services' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- let(:namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
-
- subject { knative.services }
-
- before do
- stub_kubeclient_discover(service.api_url)
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
- end
-
- it 'has an unintialized cache' do
- is_expected.to be_nil
- end
-
- context 'when using synchronous reactive cache' do
- before do
- stub_reactive_cache(knative,
- {
- services: kube_response(kube_knative_services_body),
- pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
- })
- synchronous_reactive_cache(knative)
- end
-
- it 'has cached services' do
- is_expected.not_to be_nil
- end
-
- it 'matches our namespace' do
- expect(knative.services_for(ns: namespace)).not_to be_nil
- end
- end
- end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 4739e62289a..f206bb41f45 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -38,6 +38,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to respond_to :project }
+ it do
+ expect(subject.knative_services_finder(subject.project))
+ .to be_instance_of(Clusters::KnativeServicesFinder)
+ end
+
describe '.enabled' do
subject { described_class.enabled }
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index ee613b199ad..e17b98536fa 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -260,4 +260,16 @@ describe Noteable do
end
end
end
+
+ describe '.replyable_types' do
+ it 'exposes the replyable types' do
+ expect(described_class.replyable_types).to include('Issue', 'MergeRequest')
+ end
+ end
+
+ describe '.resolvable_types' do
+ it 'exposes the replyable types' do
+ expect(described_class.resolvable_types).to include('MergeRequest')
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c72b6e9033d..956c5675f38 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -173,6 +173,42 @@ describe MergeRequest do
end
end
+ context 'for branch' do
+ before do
+ stub_feature_flags(stricter_mr_branch_name: false)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch_name, :valid) do
+ 'foo' | true
+ 'foo:bar' | false
+ '+foo:bar' | false
+ 'foo bar' | false
+ '-foo' | false
+ 'HEAD' | true
+ 'refs/heads/master' | true
+ end
+
+ with_them do
+ it "validates source_branch" do
+ subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
+
+ subject.valid?
+
+ expect(subject.errors.added?(:source_branch)).to eq(!valid)
+ end
+
+ it "validates target_branch" do
+ subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
+
+ subject.valid?
+
+ expect(subject.errors.added?(:target_branch)).to eq(!valid)
+ end
+ end
+ end
+
context 'for forks' do
let(:project) { create(:project) }
let(:fork1) { fork_project(project) }
@@ -1038,14 +1074,28 @@ describe MergeRequest do
end
end
- describe "#reset_merge_when_pipeline_succeeds" do
+ describe "#auto_merge_strategy" do
+ subject { merge_request.auto_merge_strategy }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it { is_expected.to eq('merge_when_pipeline_succeeds') }
+
+ context 'when auto merge is disabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe "#reset_auto_merge" do
let(:merge_if_green) do
create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
end
it "sets the item to false" do
- merge_if_green.reset_merge_when_pipeline_succeeds
+ merge_if_green.reset_auto_merge
merge_if_green.reload
expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
@@ -1962,57 +2012,6 @@ describe MergeRequest do
end
end
- describe '#check_if_can_be_merged' do
- let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
-
- shared_examples 'checking if can be merged' do
- context 'when it is not broken and has no conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(true)
- end
-
- it 'is marked as mergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
- end
- end
-
- context 'when broken' do
- before do
- allow(subject).to receive(:broken?) { true }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
-
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
- end
-
- context 'when it has conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
-
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
- end
- end
-
- context 'when merge_status is unchecked' do
- subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
-
- it_behaves_like 'checking if can be merged'
- end
-
- context 'when merge_status is unchecked' do
- subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) }
-
- it_behaves_like 'checking if can be merged'
- end
- end
-
describe '#mergeable?' do
let(:project) { create(:project) }
@@ -2026,7 +2025,7 @@ describe MergeRequest do
it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
allow(subject).to receive(:mergeable_state?) { true }
- expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:check_mergeability)
expect(subject).to receive(:can_be_merged?) { true }
expect(subject.mergeable?).to be_truthy
@@ -2040,7 +2039,7 @@ describe MergeRequest do
it 'checks if merge request can be merged' do
allow(subject).to receive(:mergeable_ci_state?) { true }
- expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:check_mergeability)
subject.mergeable?
end
@@ -3108,38 +3107,6 @@ describe MergeRequest do
end
end
- describe '#mergeable_to_ref?' do
- it 'returns true when merge request is mergeable' do
- subject = create(:merge_request)
-
- expect(subject.mergeable_to_ref?).to be(true)
- end
-
- it 'returns false when merge request is already merged' do
- subject = create(:merge_request, :merged)
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
-
- it 'returns false when merge request is closed' do
- subject = create(:merge_request, :closed)
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
-
- it 'returns false when merge request is work in progress' do
- subject = create(:merge_request, title: 'WIP: The feature')
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
-
- it 'returns false when merge request has no commits' do
- subject = create(:merge_request, source_branch: 'empty-branch', target_branch: 'master')
-
- expect(subject.mergeable_to_ref?).to be(false)
- end
- end
-
describe '#merge_participants' do
it 'contains author' do
expect(subject.merge_participants).to eq([subject.author])
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index e47f2de1ae9..3704a2d468d 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -498,4 +498,20 @@ describe Milestone do
end
end
end
+
+ describe '.reference_pattern' do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match('gitlab-org/gitlab-ce%123') }
+ it { is_expected.to match('gitlab-org/gitlab-ce%"my-milestone"') }
+ end
+
+ describe '.link_reference_pattern' do
+ subject { described_class.link_reference_pattern }
+
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/milestones/123") }
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/-/milestones/123") }
+ it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/issues/123") }
+ it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
+ end
end
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 7742e33e901..2c86c0ec7be 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe AssemblaService do
+ include StubRequests
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -23,12 +25,12 @@ describe AssemblaService do
)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
- WebMock.stub_request(:post, @api_url)
+ stub_full_request(@api_url, method: :post)
end
it "calls Assembla API" do
@assembla_service.execute(@sample_data)
- expect(WebMock).to have_requested(:post, @api_url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
).once
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 08c510f09df..65d227a17f9 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe BambooService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
@@ -257,7 +258,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
def stub_bamboo_request(url, status, body)
- WebMock.stub_request(:get, url).to_return(
+ stub_full_request(url).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 091d4d8f695..ca196069055 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe BuildkiteService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:project) { create(:project) }
@@ -110,10 +111,9 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
body ||= %q({"status":"success"})
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
- WebMock.stub_request(:get, buildkite_full_url).to_return(
- status: status,
- headers: { 'Content-Type' => 'application/json' },
- body: body
- )
+ stub_full_request(buildkite_full_url)
+ .to_return(status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body)
end
end
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index bf4c52fc7ab..0d3dd89e93b 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe CampfireService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -49,39 +51,37 @@ describe CampfireService do
it "calls Campfire API to get a list of rooms and speak in a room" do
# make sure a valid list of rooms is returned
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
- WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
+
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
+
# stub the speak request with the room id found in the previous request's response
speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
- WebMock.stub_request(:post, speak_url).with(basic_auth: @auth)
+ stub_full_request(speak_url, method: :post).with(basic_auth: @auth)
@campfire_service.execute(@sample_data)
- expect(WebMock).to have_requested(:get, @rooms_url).once
- expect(WebMock).to have_requested(:post, speak_url).with(
- body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
- ).once
+ expect(WebMock).to have_requested(:get, stubbed_hostname(@rooms_url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(speak_url))
+ .with(body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/).once
end
it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
# return a list of rooms that do not contain a room named 'test-room'
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
- WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
- # we want to make sure no request is sent to the /speak endpoint, here is a basic
- # regexp that matches this endpoint
- speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
@campfire_service.execute(@sample_data)
- expect(WebMock).to have_requested(:get, @rooms_url).once
- expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9/rooms.json').once
+ expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
end
end
end
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index 773b8b7890f..dde46c82df6 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe PivotaltrackerService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -53,12 +55,12 @@ describe PivotaltrackerService do
end
before do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
end
it 'posts correct message' do
service.execute(push_data)
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
body: {
'source_commit' => {
'commit_id' => '21c12ea',
@@ -85,14 +87,14 @@ describe PivotaltrackerService do
service.execute(push_data(branch: 'master'))
service.execute(push_data(branch: 'v10'))
- expect(WebMock).to have_requested(:post, url).twice
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).twice
end
it 'does not post message if branch is not in the list' do
service.execute(push_data(branch: 'mas'))
service.execute(push_data(branch: 'v11'))
- expect(WebMock).not_to have_requested(:post, url)
+ expect(WebMock).not_to have_requested(:post, stubbed_hostname(url))
end
end
end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index d2a45f48705..380f02739bc 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe PushoverService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -57,13 +59,13 @@ describe PushoverService do
sound: sound
)
- WebMock.stub_request(:post, api_url)
+ stub_full_request(api_url, method: :post, ip_address: '8.8.8.8')
end
it 'calls Pushover API' do
pushover.execute(sample_data)
- expect(WebMock).to have_requested(:post, api_url).once
+ expect(WebMock).to have_requested(:post, 'https://8.8.8.8/1/messages.json').once
end
end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 96dccae733b..1c434b25205 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe TeamcityService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:teamcity_url) { 'http://gitlab.com/teamcity' }
@@ -212,7 +213,7 @@ describe TeamcityService, :use_clean_rails_memory_store_caching do
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
- WebMock.stub_request(:get, teamcity_full_url).with(basic_auth: auth).to_return(
+ stub_full_request(teamcity_full_url).with(basic_auth: auth).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 08662231fdf..aad08b9d4aa 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1147,7 +1147,7 @@ describe Project do
allow(project).to receive(:avatar_in_git) { true }
end
- let(:avatar_path) { "/#{project.full_path}/avatar" }
+ let(:avatar_path) { "/#{project.full_path}/-/avatar" }
it { is_expected.to eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
@@ -3170,6 +3170,23 @@ describe Project do
end
end
+ describe '.ids_with_milestone_available_for' do
+ let!(:user) { create(:user) }
+
+ it 'returns project ids with milestones available for user' do
+ project_1 = create(:project, :public, :merge_requests_disabled, :issues_disabled)
+ project_2 = create(:project, :public, :merge_requests_disabled)
+ project_3 = create(:project, :public, :issues_disabled)
+ project_4 = create(:project, :public)
+ project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
+
+ project_ids = described_class.ids_with_milestone_available_for(user).pluck(:id)
+
+ expect(project_ids).to include(project_2.id, project_3.id)
+ expect(project_ids).not_to include(project_1.id, project_4.id)
+ end
+ end
+
describe '.with_feature_available_for_user' do
let(:user) { create(:user) }
let(:feature) { MergeRequest }
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index f985c114d4b..358873f9a2f 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -11,6 +11,20 @@ describe ProjectStatistics do
it { is_expected.to belong_to(:namespace) }
end
+ describe 'scopes' do
+ describe '.for_project_ids' do
+ it 'returns only requested projects' do
+ stats = create_list(:project_statistics, 3)
+ project_ids = stats[0..1].map { |s| s.project_id }
+ expected_ids = stats[0..1].map { |s| s.id }
+
+ requested_stats = described_class.for_project_ids(project_ids).pluck(:id)
+
+ expect(requested_stats).to eq(expected_ids)
+ end
+ end
+ end
+
describe 'statistics columns' do
it "support values up to 8 exabytes" do
statistics.update!(
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
new file mode 100644
index 00000000000..8e24559341b
--- /dev/null
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IssuePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:issue) { create(:issue, project: project) }
+ let(:presenter) { described_class.new(issue, current_user: user) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ describe '#web_url' do
+ it 'returns correct path' do
+ expect(presenter.web_url).to eq "http://localhost/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ end
+ end
+
+ describe '#issue_path' do
+ it 'returns correct path' do
+ expect(presenter.issue_path).to eq "/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 0e1aed42cc5..6408b0bd748 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -207,25 +207,25 @@ describe MergeRequestPresenter do
end
end
- describe '#cancel_merge_when_pipeline_succeeds_path' do
+ describe '#cancel_auto_merge_path' do
subject do
described_class.new(resource, current_user: user)
- .cancel_merge_when_pipeline_succeeds_path
+ .cancel_auto_merge_path
end
context 'when can cancel mwps' do
it 'returns path' do
- allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ allow(resource).to receive(:can_cancel_auto_merge?)
.with(user)
.and_return(true)
- is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_merge_when_pipeline_succeeds")
+ is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_auto_merge")
end
end
context 'when cannot cancel mwps' do
it 'returns nil' do
- allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ allow(resource).to receive(:can_cancel_auto_merge?)
.with(user)
.and_return(false)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index a132b85b878..f104da6ebba 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require 'mime/types'
describe API::Commits do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
@@ -317,6 +319,96 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(201)
end
end
+
+ context 'when the API user is a guest' do
+ def last_commit_id(project, branch_name)
+ project.repository.find_branch(branch_name)&.dereferenced_target&.id
+ end
+
+ let(:public_project) { create(:project, :public, :repository) }
+ let!(:url) { "/projects/#{public_project.id}/repository/commits" }
+ let(:guest) { create(:user).tap { |u| public_project.add_guest(u) } }
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when start_project is provided' do
+ context 'when posting to a forked project the user owns' do
+ let!(:forked_project) { fork_project(public_project, guest, namespace: guest.namespace, repository: true) }
+ let!(:url) { "/projects/#{forked_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ end
+
+ context 'identified by Integer (id)' do
+ before do
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'adds a new commit to forked_project and returns a 201' do
+ expect { post api(url, guest), params: valid_c_params }
+ .to change { last_commit_id(forked_project, valid_c_params[:branch]) }
+ .and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+
+ context 'identified by String (full_path)' do
+ before do
+ valid_c_params[:start_project] = public_project.full_path
+ end
+
+ it 'adds a new commit to forked_project and returns a 201' do
+ expect { post api(url, guest), params: valid_c_params }
+ .to change { last_commit_id(forked_project, valid_c_params[:branch]) }
+ .and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+ end
+
+ context 'when the target project is not part of the fork network of start_project' do
+ let(:unrelated_project) { create(:project, :public, :repository, creator: guest) }
+ let!(:url) { "/projects/#{unrelated_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when posting to a forked project the user does not have write access' do
+ let!(:forked_project) { fork_project(public_project, user, namespace: user.namespace, repository: true) }
+ let!(:url) { "/projects/#{forked_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
end
describe 'delete' do
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index 8ff95cc9af2..db9f2ac9dd0 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -86,17 +86,18 @@ describe 'getting group information' do
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)
+ queries = [{ query: group_query(group1) },
+ { query: group_query(group2) }]
expect do
- post_graphql(group_query(group1), current_user: admin)
+ post_multiplex(queries, current_user: admin)
end.not_to exceed_query_limit(control_count)
+
+ expect(graphql_errors).to contain_exactly(nil, nil)
end
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
new file mode 100644
index 00000000000..e05273da4bd
--- /dev/null
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting projects', :nested_groups do
+ include GraphqlHelpers
+
+ let(:group) { create(:group) }
+ let!(:project) { create(:project, namespace: subject) }
+ let(:nested_group) { create(:group, parent: group) }
+ let!(:nested_project) { create(:project, group: nested_group) }
+ let!(:public_project) { create(:project, :public, namespace: subject) }
+ let(:user) { create(:user) }
+ let(:include_subgroups) { true }
+
+ subject { group }
+
+ let(:query) do
+ graphql_query_for(
+ 'namespace',
+ { 'fullPath' => subject.full_path },
+ <<~QUERY
+ projects(includeSubgroups: #{include_subgroups}) {
+ edges {
+ node {
+ #{all_graphql_fields_for('Project')}
+ }
+ }
+ }
+ QUERY
+ )
+ end
+
+ before do
+ group.add_owner(user)
+ end
+
+ shared_examples 'a graphql namespace' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "includes the packages size if the user can read the statistics" do
+ post_graphql(query, current_user: user)
+
+ count = if include_subgroups
+ subject.all_projects.count
+ else
+ subject.projects.count
+ end
+
+ expect(graphql_data['namespace']['projects']['edges'].size).to eq(count)
+ end
+
+ context 'with no user' do
+ it 'finds only public projects' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['namespace']['projects']['edges'].size).to eq(1)
+ project = graphql_data['namespace']['projects']['edges'][0]['node']
+ expect(project['id']).to eq(public_project.id.to_s)
+ end
+ end
+ end
+
+ it_behaves_like 'a graphql namespace'
+
+ context 'when the namespace is a user' do
+ subject { user.namespace }
+ let(:include_subgroups) { false }
+
+ it_behaves_like 'a graphql namespace'
+ end
+
+ context 'when not including subgroups' do
+ let(:include_subgroups) { false }
+
+ it_behaves_like 'a graphql namespace'
+ end
+end
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
new file mode 100644
index 00000000000..8683fa1f390
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'rendering namespace statistics' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project) }
+ let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.megabytes) }
+ let(:user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ "statistics { #{all_graphql_fields_for('ProjectStatistics')} }")
+ end
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "includes the packages size if the user can read the statistics" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statistics']['packagesSize']).to eq(5.megabytes)
+ end
+
+ context 'when the project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'includes the statistics regardless of the user' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']['statistics']).to be_present
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 5c94a87529b..4cb4fcc890d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1473,7 +1473,7 @@ describe API::MergeRequests do
end
it "enables merge when pipeline succeeds if the pipeline is active" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest).to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(pipeline).to receive(:active?).and_return(true)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { merge_when_pipeline_succeeds: true }
@@ -1484,7 +1484,7 @@ describe API::MergeRequests do
end
it "enables merge when pipeline succeeds if the pipeline is active and only_allow_merge_if_pipeline_succeeds is true" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest).to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(pipeline).to receive(:active?).and_return(true)
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
@@ -1546,52 +1546,65 @@ describe API::MergeRequests do
end
end
- describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge_to_ref" do
- let(:pipeline) { create(:ci_pipeline_without_jobs) }
+ describe "GET /projects/:id/merge_requests/:merge_request_iid/merge_ref" do
+ before do
+ merge_request.mark_as_unchecked!
+ end
+
+ let(:merge_request_iid) { merge_request.iid }
+
let(:url) do
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge_to_ref"
+ "/projects/#{project.id}/merge_requests/#{merge_request_iid}/merge_ref"
end
it 'returns the generated ID from the merge service in case of success' do
- put api(url, user), params: { merge_commit_message: 'Custom message' }
-
- commit = project.commit(json_response['commit_id'])
+ get api(url, user)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['commit_id']).to be_present
- expect(commit.message).to eq('Custom message')
+ expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha)
end
it "returns 400 if branch can't be merged" do
- merge_request.update!(state: 'merged')
+ merge_request.update!(merge_status: 'cannot_be_merged')
- put api(url, user)
+ get api(url, user)
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message'])
- .to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}")
+ expect(json_response['message']).to eq('Merge request is not mergeable')
end
- it 'returns 403 if user has no permissions to merge to the ref' do
- user2 = create(:user)
- project.add_reporter(user2)
+ context 'when user has no access to the MR' do
+ let(:project) { create(:project, :private) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- put api(url, user2)
+ it 'returns 404' do
+ project.add_guest(user)
- expect(response).to have_gitlab_http_status(403)
- expect(json_response['message']).to eq('403 Forbidden')
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
end
- it 'returns 404 for an invalid merge request IID' do
- put api("/projects/#{project.id}/merge_requests/12345/merge_to_ref", user)
+ context 'when invalid merge request IID' do
+ let(:merge_request_iid) { '12345' }
- expect(response).to have_gitlab_http_status(404)
+ it 'returns 404' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
- it "returns 404 if the merge request id is used instead of iid" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ context 'when merge request ID is used instead IID' do
+ let(:merge_request_iid) { merge_request.id }
- expect(response).to have_gitlab_http_status(404)
+ it 'returns 404' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
@@ -1950,7 +1963,7 @@ describe API::MergeRequests do
describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
before do
- ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request)
+ ::AutoMergeService.new(merge_request.target_project, user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
it 'removes the merge_when_pipeline_succeeds status' do
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 7d61ec9c4d8..3e0b478abb3 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -70,11 +70,30 @@ describe API::Search do
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
+ end
+
+ context 'when user can read project milestones' do
+ before do
+ get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ end
- get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+ context 'when user cannot read project milestones' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns empty array' do
+ get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+
+ milestones = JSON.parse(response.body)
+
+ expect(milestones).to be_empty
+ end
+ end
end
context 'for users scope' do
@@ -318,11 +337,30 @@ describe API::Search do
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
+ end
+
+ context 'when user can read milestones' do
+ before do
+ get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ end
- get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+ context 'when user cannot read project milestones' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+
+ milestones = JSON.parse(response.body)
+
+ expect(milestones).to be_empty
+ end
+ end
end
context 'for users scope' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b6e8d74c2e9..0e2f3face71 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe API::SystemHooks do
+ include StubRequests
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
before do
- stub_request(:post, hook.url)
+ stub_full_request(hook.url, method: :post)
end
describe "GET /hooks" do
@@ -68,6 +70,8 @@ describe API::SystemHooks do
end
it 'sets default values for events' do
+ stub_full_request('http://mep.mep', method: :post)
+
post api('/hooks', admin), params: { url: 'http://mep.mep' }
expect(response).to have_gitlab_http_status(201)
@@ -78,6 +82,8 @@ describe API::SystemHooks do
end
it 'sets explicit values for events' do
+ stub_full_request('http://mep.mep', method: :post)
+
post api('/hooks', admin),
params: {
url: 'http://mep.mep',
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 6f40e88d26f..83775b1040e 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -470,18 +470,23 @@ describe 'project routing' do
it_behaves_like 'RESTful project resources' do
let(:controller) { 'milestones' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ let(:controller_path) { '/-/milestones' }
end
it 'to #promote' do
- expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
+ expect(post('/gitlab/gitlabhq/-/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/milestones", "/gitlab/gitlabhq/-/milestones"
end
# project_labels GET /:project_id/labels(.:format) labels#index
describe Projects::LabelsController, 'routing' do
it 'to #index' do
- expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/labels", "/gitlab/gitlabhq/-/labels"
end
# sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort
@@ -623,20 +628,24 @@ describe 'project routing' do
describe Projects::ForksController, 'routing' do
it 'to #new' do
- expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'to #create' do
- expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(post('/gitlab/gitlabhq/-/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/forks", "/gitlab/gitlabhq/-/forks"
end
# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy
describe Projects::AvatarsController, 'routing' do
it 'to #destroy' do
- expect(delete('/gitlab/gitlabhq/avatar')).to route_to(
+ expect(delete('/gitlab/gitlabhq/-/avatar')).to route_to(
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/avatar", "/gitlab/gitlabhq/-/avatar"
end
describe Projects::PagesDomainsController, 'routing' do
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index b89898f26f7..a27c22191f4 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -297,4 +297,50 @@ describe MergeRequestWidgetEntity do
end
end
end
+
+ describe 'auto merge' do
+ context 'when auto merge is enabled' do
+ let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_truthy
+ expect(subject[:auto_merge_strategy]).to eq('merge_when_pipeline_succeeds')
+ end
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:resource) { create(:merge_request) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_falsy
+ expect(subject[:auto_merge_strategy]).to be_nil
+ end
+ end
+
+ context 'when head pipeline is running' do
+ before do
+ create(:ci_pipeline, :running, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to eq(%w[merge_when_pipeline_succeeds])
+ end
+ end
+
+ context 'when head pipeline is finished' do
+ before do
+ create(:ci_pipeline, :success, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 96f61f3f103..a20bf8e17e4 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe MergeRequests::MergeWhenPipelineSucceedsService do
+describe AutoMerge::MergeWhenPipelineSucceedsService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -21,6 +21,27 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
described_class.new(project, user, commit_message: 'Awesome message')
end
+ describe "#available_for?" do
+ subject { service.available_for?(mr_merge_if_green_enabled) }
+
+ let(:pipeline_status) { :running }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: mr_merge_if_green_enabled.source_branch,
+ sha: mr_merge_if_green_enabled.diff_head_sha,
+ project: mr_merge_if_green_enabled.source_project)
+ mr_merge_if_green_enabled.update_head_pipeline
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe "#execute" do
let(:merge_request) do
create(:merge_request, target_project: project, source_project: project,
@@ -30,8 +51,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
context 'first time enabling' do
before do
allow(merge_request)
- .to receive(:head_pipeline)
- .and_return(pipeline)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
service.execute(merge_request)
end
@@ -39,7 +59,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'sets the params, merge_user, and flag' do
expect(merge_request).to be_valid
expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
- expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
+ expect(merge_request.merge_params).to include commit_message: 'Awesome message'
expect(merge_request.merge_user).to be user
end
@@ -54,8 +74,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do
- allow(mr_merge_if_green_enabled).to receive(:head_pipeline)
- .and_return(pipeline)
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(mr_merge_if_green_enabled).to receive(:mergeable?)
.and_return(true)
@@ -72,7 +92,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
end
- describe "#trigger" do
+ describe "#process" do
let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
let(:merge_request_head) do
project.commit(mr_merge_if_green_enabled.source_branch).id
@@ -86,8 +106,11 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
it "merges all merge requests with merge when the pipeline succeeds enabled" do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: triggering_pipeline, actual_head_pipeline: triggering_pipeline)
+
expect(MergeWorker).to receive(:perform_async)
- service.trigger(triggering_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -99,7 +122,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(old_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -111,7 +134,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(unrelated_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -125,8 +148,11 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
it 'merges the associated merge request' do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+
expect(MergeWorker).to receive(:perform_async)
- service.trigger(pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
end
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
new file mode 100644
index 00000000000..d0eefed3150
--- /dev/null
+++ b/spec/services/auto_merge_service_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMergeService do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '.all_strategies' do
+ subject { described_class.all_strategies }
+
+ it 'returns all strategies' do
+ is_expected.to eq(AutoMergeService::STRATEGIES)
+ end
+ end
+
+ describe '#available_strategies' do
+ subject { service.available_strategies(merge_request) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:pipeline_status) { :running }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: merge_request.source_project)
+
+ merge_request.update_head_pipeline
+ end
+
+ it 'returns available strategies' do
+ is_expected.to include('merge_when_pipeline_succeeds')
+ end
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it 'returns available strategies' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.get_service_class' do
+ subject { described_class.get_service_class(strategy) }
+
+ let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+
+ it 'returns service instance' do
+ is_expected.to eq(AutoMerge::MergeWhenPipelineSucceedsService)
+ end
+
+ context 'when strategy is not present' do
+ let(:strategy) { }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject { service.execute(merge_request, strategy) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:pipeline_status) { :running }
+ let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: merge_request.source_project)
+
+ merge_request.update_head_pipeline
+ end
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it 'returns failed' do
+ is_expected.to eq(:failed)
+ end
+ end
+ end
+
+ describe '#process' do
+ subject { service.process(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:process).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#cancel' do
+ subject { service.cancel(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:cancel).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns error' do
+ expect(subject[:message]).to eq("Can't cancel the automatic merge")
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:http_status]).to eq(406)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 9a3ac75e418..867692d4d64 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -973,7 +973,7 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
@@ -1004,7 +1004,7 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
@@ -1033,7 +1033,7 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
new file mode 100644
index 00000000000..f2ac53cb25a
--- /dev/null
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PipelineScheduleService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ subject { service.execute(schedule) }
+
+ let(:schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+
+ it 'schedules next run' do
+ expect(schedule).to receive(:schedule_next_run!)
+
+ subject
+ end
+
+ it 'runs RunPipelineScheduleWorker' do
+ expect(RunPipelineScheduleWorker)
+ .to receive(:perform_async).with(schedule.id, schedule.owner.id)
+
+ subject
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index ffa612cf315..29b7e0f17e2 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -52,6 +52,14 @@ describe MergeRequests::CloseService do
it 'marks todos as done' do
expect(todo.reload).to be_done
end
+
+ context 'when auto merge is enabled' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'cancels the auto merge' do
+ expect(@merge_request).not_to be_auto_merge_enabled
+ end
+ end
end
it 'updates metrics' do
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index 0ac23050caf..5d492e4b013 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -32,10 +32,8 @@ describe MergeRequests::MergeToRefService do
expect(result[:status]).to eq(:success)
expect(result[:commit_id]).to be_present
- expect(result[:source_id]).to eq(merge_request.source_branch_sha)
- expect(result[:target_id]).to eq(merge_request.target_branch_sha)
expect(repository.ref_exists?(target_ref)).to be(true)
- expect(ref_head.id).to eq(result[:commit_id])
+ expect(ref_head.sha).to eq(result[:commit_id])
end
end
@@ -72,10 +70,6 @@ describe MergeRequests::MergeToRefService do
let(:merge_request) { create(:merge_request, :simple) }
let(:project) { merge_request.project }
- before do
- project.add_maintainer(user)
- end
-
describe '#execute' do
let(:service) do
described_class.new(project, user, commit_message: 'Awesome message',
@@ -92,6 +86,12 @@ describe MergeRequests::MergeToRefService do
it_behaves_like 'successfully evaluates pre-condition checks'
context 'commit history comparison with regular MergeService' do
+ before do
+ # The merge service needs an authorized user while merge-to-ref
+ # doesn't.
+ project.add_maintainer(user)
+ end
+
let(:merge_ref_service) do
described_class.new(project, user, {})
end
@@ -136,9 +136,9 @@ describe MergeRequests::MergeToRefService do
let(:merge_method) { :merge }
it 'returns error' do
- allow(merge_request).to receive(:mergeable_to_ref?) { false }
+ allow(project).to receive_message_chain(:repository, :merge_to_ref) { nil }
- error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}"
+ error_message = 'Conflicts detected during merge'
result = service.execute(merge_request)
@@ -170,28 +170,5 @@ describe MergeRequests::MergeToRefService do
it { expect(todo).not_to be_done }
end
-
- context 'when merge request is WIP state' do
- it 'fails to merge' do
- merge_request = create(:merge_request, title: 'WIP: The feature')
-
- result = service.execute(merge_request)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq("Merge request is not mergeable to #{merge_request.merge_ref_path}")
- end
- end
-
- it 'returns error when user has no authorization to admin the merge request' do
- unauthorized_user = create(:user)
- project.add_reporter(unauthorized_user)
-
- service = described_class.new(project, unauthorized_user)
-
- result = service.execute(merge_request)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('You are not allowed to merge to this ref')
- end
end
end
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
new file mode 100644
index 00000000000..aa0485467ed
--- /dev/null
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::MergeabilityCheckService do
+ shared_examples_for 'unmergeable merge request' do
+ it 'updates or keeps merge status as cannot_be_merged' do
+ subject
+
+ expect(merge_request.merge_status).to eq('cannot_be_merged')
+ end
+
+ it 'does not change the merge ref HEAD' do
+ expect { subject }.not_to change(merge_request, :merge_ref_head)
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ end
+ end
+
+ shared_examples_for 'mergeable merge request' do
+ it 'updates or keeps merge status as can_be_merged' do
+ subject
+
+ expect(merge_request.merge_status).to eq('can_be_merged')
+ end
+
+ it 'updates the merge ref' do
+ expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+
+ it 'ServiceResponse has merge_ref_head payload' do
+ result = subject
+
+ expect(result.payload.keys).to contain_exactly(:merge_ref_head)
+ expect(result.payload[:merge_ref_head].keys)
+ .to contain_exactly(:commit_id, :target_id, :source_id)
+ end
+ end
+
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, merge_status: :unchecked, source_project: project, target_project: project) }
+ let(:repo) { project.repository }
+
+ subject { described_class.new(merge_request).execute }
+
+ before do
+ project.add_developer(merge_request.author)
+ end
+
+ it_behaves_like 'mergeable merge request'
+
+ context 'when multiple calls to the service' do
+ it 'returns success' do
+ subject
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ end
+
+ it 'second call does not change the merge-ref' do
+ expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
+ expect { subject }.not_to change(merge_request, :merge_ref_head)
+ end
+ end
+
+ context 'when broken' do
+ before do
+ allow(merge_request).to receive(:broken?) { true }
+ allow(project.repository).to receive(:can_be_merged?) { false }
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when it has conflicts' do
+ before do
+ allow(merge_request).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?) { false }
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when MR cannot be merged and has no merge ref' do
+ before do
+ merge_request.mark_as_unmergeable!
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when MR cannot be merged and has outdated merge ref' do
+ before do
+ MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
+ merge_request.mark_as_unmergeable!
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when merge request is not given' do
+ subject { described_class.new(nil).execute }
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.message).to eq('Invalid argument')
+ end
+ end
+
+ context 'when read only DB' do
+ it 'returns ServiceResponse.error' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.message).to eq('Unsupported operation')
+ end
+ end
+
+ context 'when MR is mergeable but merge-ref does not exists' do
+ before do
+ merge_request.mark_as_mergeable!
+ end
+
+ it 'keeps merge status as can_be_merged' do
+ expect { subject }.not_to change(merge_request, :merge_status).from('can_be_merged')
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge ref was not found')
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index f7a39bb42d5..54b9c6dae38 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -76,10 +76,11 @@ describe MergeRequests::PushOptionsHandlerService do
shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do
subject(:last_mr) { MergeRequest.last }
- it 'sets merge_when_pipeline_succeeds' do
+ it 'sets auto_merge_enabled' do
service.execute
- expect(last_mr.merge_when_pipeline_succeeds).to eq(true)
+ expect(last_mr.auto_merge_enabled).to eq(true)
+ expect(last_mr.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
it 'sets merge_user to the user' do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7258428589f..6ba67c7165c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -23,7 +23,8 @@ describe MergeRequests::RefreshService do
source_branch: 'master',
target_branch: 'feature',
target_project: @project,
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: @user)
@another_merge_request = create(:merge_request,
@@ -31,7 +32,8 @@ describe MergeRequests::RefreshService do
source_branch: 'master',
target_branch: 'test',
target_project: @project,
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: @user)
@fork_merge_request = create(:merge_request,
@@ -83,7 +85,7 @@ describe MergeRequests::RefreshService do
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
- expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.auto_merge_enabled).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
@@ -292,7 +294,7 @@ describe MergeRequests::RefreshService do
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
- expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.auto_merge_enabled).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index ba4c9ce60f3..fbfcd95e204 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -217,8 +217,9 @@ describe MergeRequests::UpdateService, :mailer do
head_pipeline_of: merge_request
)
- expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user)
+ expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, {})
.and_return(service_mock)
+ allow(service_mock).to receive(:available_for?) { true }
expect(service_mock).to receive(:execute).with(merge_request)
end
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 f4470b50753..75d534c59bf 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadService do
+ include StubRequests
+
let(:project) { create(:project) }
let(:lfs_content) { SecureRandom.random_bytes(10) }
let(:oid) { Digest::SHA256.hexdigest(lfs_content) }
@@ -62,7 +64,7 @@ describe Projects::LfsPointers::LfsDownloadService do
describe '#execute' do
context 'when file download succeeds' do
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
@@ -104,7 +106,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:size) { 1 }
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'no lfs object is created'
@@ -118,7 +120,7 @@ describe Projects::LfsPointers::LfsDownloadService do
context 'when downloaded lfs file has a different oid' do
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar')
end
@@ -136,7 +138,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
before do
- WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
+ stub_full_request(download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
end
it 'the request adds authorization headers' do
@@ -149,7 +151,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:local_request_setting) { true }
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link, ip_address: '192.168.2.120').to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
@@ -173,7 +175,8 @@ describe Projects::LfsPointers::LfsDownloadService do
with_them do
before do
- WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ stub_full_request(download_link, ip_address: '192.168.2.120')
+ .to_return(status: 301, headers: { 'Location' => redirect_link })
end
it_behaves_like 'no lfs object is created'
@@ -184,8 +187,8 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:redirect_link) { "http://example.com/"}
before do
- WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
- WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ stub_full_request(redirect_link).to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index 30bd4d6820b..e790d272e61 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -16,6 +16,13 @@ describe ServiceResponse do
expect(response).to be_success
expect(response.message).to eq('Good orange')
end
+
+ it 'creates a successful response with payload' do
+ response = described_class.success(payload: { good: 'orange' })
+
+ expect(response).to be_success
+ expect(response.payload).to eq(good: 'orange')
+ end
end
describe '.error' do
@@ -33,6 +40,15 @@ describe ServiceResponse do
expect(response.message).to eq('Bad apple')
expect(response.http_status).to eq(400)
end
+
+ it 'creates a failed response with payload' do
+ response = described_class.error(message: 'Bad apple',
+ payload: { bad: 'apple' })
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ expect(response.payload).to eq(bad: 'apple')
+ end
end
describe '#success?' do
diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb
index 78df9bf96bf..653f17a4324 100644
--- a/spec/services/submit_usage_ping_service_spec.rb
+++ b/spec/services/submit_usage_ping_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe SubmitUsagePingService do
+ include StubRequests
+
context 'when usage ping is disabled' do
before do
stub_application_setting(usage_ping_enabled: false)
@@ -99,7 +101,7 @@ describe SubmitUsagePingService do
end
def stub_response(body)
- stub_request(:post, 'https://version.gitlab.com/usage_data')
+ stub_full_request('https://version.gitlab.com/usage_data', method: :post)
.to_return(
headers: { 'Content-Type' => 'application/json' },
body: body.to_json
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 75ba2479b63..37bafc0c002 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe WebHookService do
+ include StubRequests
+
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook) }
let(:headers) do
@@ -67,11 +69,11 @@ describe WebHookService do
let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
it 'uses the credentials' do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
service_instance.execute
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
).once
end
@@ -82,11 +84,11 @@ describe WebHookService do
let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
it 'uses the credentials anyways' do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
service_instance.execute
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
).once
end
diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb
index 99a7c39852e..99c5871ba54 100644
--- a/spec/support/helpers/git_helpers.rb
+++ b/spec/support/helpers/git_helpers.rb
@@ -6,12 +6,4 @@ module GitHelpers
Rugged::Repository.new(path)
end
-
- def project_hook_exists?(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project_path = project.repository.raw_repository.path
-
- File.exist?(File.join(project_path, 'hooks', 'post-receive'))
- end
- end
end
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 78b7ae9c00c..011c4df0fe5 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -17,17 +17,38 @@ module KubernetesHelpers
kube_response(kube_deployments_body)
end
- def stub_kubeclient_discover(api_url)
+ def stub_kubeclient_discover_base(api_url)
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/extensions/v1beta1')
+ .to_return(kube_response(kube_v1beta1_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
+ .to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+ end
+
+ def stub_kubeclient_discover(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ WebMock
+ .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+ .to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ end
+
+ def stub_kubeclient_discover_knative_not_found(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ WebMock
+ .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+ .to_return(status: [404, "Resource Not Found"])
end
- def stub_kubeclient_service_pods(status: nil)
+ def stub_kubeclient_service_pods(response = nil, options = {})
stub_kubeclient_discover(service.api_url)
- pods_url = service.api_url + "/api/v1/pods"
- response = { status: status } if status
+
+ namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
+ pods_url = service.api_url + "/api/v1/#{namespace_path}pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
@@ -56,15 +77,18 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
- def stub_kubeclient_knative_services(**options)
+ def stub_kubeclient_knative_services(options = {})
+ namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
options[:name] ||= "kubetest"
- options[:namespace] ||= "default"
options[:domain] ||= "example.com"
+ options[:response] ||= kube_response(kube_knative_services_body(options))
stub_kubeclient_discover(service.api_url)
- knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
- WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+ knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/#{namespace_path}services"
+
+ WebMock.stub_request(:get, knative_url).to_return(options[:response])
end
def stub_kubeclient_get_secret(api_url, **options)
diff --git a/spec/support/helpers/stub_requests.rb b/spec/support/helpers/stub_requests.rb
new file mode 100644
index 00000000000..5cad35282c0
--- /dev/null
+++ b/spec/support/helpers/stub_requests.rb
@@ -0,0 +1,40 @@
+module StubRequests
+ IP_ADDRESS_STUB = '8.8.8.9'.freeze
+
+ # Fully stubs a request using WebMock class. This class also
+ # stubs the IP address the URL is translated to (DNS lookup).
+ #
+ # It expects the final request to go to the `ip_address` instead the given url.
+ # That's primarily a DNS rebind attack prevention of Gitlab::HTTP
+ # (see: Gitlab::UrlBlocker).
+ #
+ def stub_full_request(url, ip_address: IP_ADDRESS_STUB, port: 80, method: :get)
+ stub_dns(url, ip_address: ip_address, port: port)
+
+ url = stubbed_hostname(url, hostname: ip_address)
+ WebMock.stub_request(method, url)
+ end
+
+ def stub_dns(url, ip_address:, port: 80)
+ url = parse_url(url)
+ socket = Socket.sockaddr_in(port, ip_address)
+ addr = Addrinfo.new(socket)
+
+ # See Gitlab::UrlBlocker
+ allow(Addrinfo).to receive(:getaddrinfo)
+ .with(url.hostname, url.port, nil, :STREAM)
+ .and_return([addr])
+ end
+
+ def stubbed_hostname(url, hostname: IP_ADDRESS_STUB)
+ url = parse_url(url)
+ url.hostname = hostname
+ url.to_s
+ end
+
+ private
+
+ def parse_url(url)
+ url.is_a?(URI) ? url : URI(url)
+ end
+end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 7a04e940ee5..1b09c3dd636 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -3,16 +3,16 @@
require 'spec_helper'
shared_examples_for 'UpdateProjectStatistics' do
- let(:project) { subject.project }
- let(:stat) { described_class.statistic_name }
- let(:attribute) { described_class.statistic_attribute }
+ let(:project) { subject.project }
+ let(:project_statistics_name) { described_class.project_statistics_name }
+ let(:statistic_attribute) { described_class.statistic_attribute }
def reload_stat
- project.statistics.reload.send(stat).to_i
+ project.statistics.reload.send(project_statistics_name).to_i
end
def read_attribute
- subject.read_attribute(attribute).to_i
+ subject.read_attribute(statistic_attribute).to_i
end
it { is_expected.to be_new_record }
@@ -39,7 +39,8 @@ shared_examples_for 'UpdateProjectStatistics' do
.to receive(:increment_statistic)
.and_call_original
- subject.write_attribute(attribute, read_attribute + delta)
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
expect { subject.save! }
.to change { reload_stat }
.by(delta)
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index a9d14070177..c3e912b02c5 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -7,14 +7,8 @@ describe 'gitlab:shell rake tasks' do
stub_warn_user_is_not_gitlab
end
- after do
- TestEnv.sabotage_gitlab_shell_hooks
- end
-
describe 'install task' do
- it 'invokes create_hooks task' do
- expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
-
+ it 'installs and compiles gitlab-shell' do
storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
end
@@ -24,14 +18,4 @@ describe 'gitlab:shell rake tasks' do
run_rake_task('gitlab:shell:install')
end
end
-
- describe 'create_hooks task' do
- it 'calls gitlab-shell bin/create_hooks' do
- expect_any_instance_of(Object).to receive(:system)
- .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks",
- *Gitlab::TaskHelpers.repository_storage_paths_args)
-
- run_rake_task('gitlab:shell:create_hooks')
- end
- end
end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 555a58e9aa1..4188e7caccb 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -8,13 +8,13 @@ describe 'tokens rake tasks' do
end
describe 'reset_all_email task' do
- it 'invokes create_hooks task' do
+ it 'changes the incoming email token' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
end
end
describe 'reset_all_feed task' do
- it 'invokes create_hooks task' do
+ it 'changes the feed token for the user' do
expect { run_rake_task('tokens:reset_all_feed') }.to change { user.reload.feed_token }
end
end
diff --git a/spec/workers/auto_merge_process_worker_spec.rb b/spec/workers/auto_merge_process_worker_spec.rb
new file mode 100644
index 00000000000..616727ce5ca
--- /dev/null
+++ b/spec/workers/auto_merge_process_worker_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMergeProcessWorker do
+ describe '#perform' do
+ subject { described_class.new.perform(merge_request&.id) }
+
+ context 'when merge request is found' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'executes AutoMergeService' do
+ expect_next_instance_of(AutoMergeService) do |auto_merge|
+ expect(auto_merge).to receive(:process)
+ end
+
+ subject
+ end
+ end
+
+ context 'when merge request is not found' do
+ let(:merge_request) { nil }
+
+ it 'does not execute AutoMergeService' do
+ expect(AutoMergeService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 8c604b13297..9326db34209 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -41,16 +41,6 @@ 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)
@@ -77,47 +67,19 @@ describe PipelineScheduleWorker do
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
end
- it 'creates a failed pipeline with the reason' do
- expect { subject }.to change { project.ci_pipelines.count }.by(1)
- expect(Ci::Pipeline.last).to be_config_error
- expect(Ci::Pipeline.last.yaml_errors).not_to be_nil
+ it 'does not creates a new pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
end
end
end
context 'when the schedule is not runnable by the user' do
- before do
- expect(Gitlab::Sentry)
- .to receive(:track_exception)
- .with(Ci::CreatePipelineService::CreateError,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: pipeline_schedule.id } ).once
- end
-
it 'does not deactivate the schedule' do
subject
expect(pipeline_schedule.reload.active).to be_truthy
end
- it 'increments Prometheus counter' do
- expect(Gitlab::Metrics)
- .to receive(:counter)
- .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
- .and_call_original
-
- subject
- end
-
- it 'logging a pipeline error' do
- expect(Rails.logger)
- .to receive(:error)
- .with(a_string_matching("Insufficient permissions to create a new pipeline"))
- .and_call_original
-
- subject
- end
-
it 'does not create a pipeline' do
expect { subject }.not_to change { project.ci_pipelines.count }
end
@@ -131,21 +93,6 @@ describe PipelineScheduleWorker do
before do
stub_ci_pipeline_yaml_file(nil)
project.add_maintainer(user)
-
- expect(Gitlab::Sentry)
- .to receive(:track_exception)
- .with(Ci::CreatePipelineService::CreateError,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: pipeline_schedule.id } ).once
- end
-
- it 'logging a pipeline error' do
- expect(Rails.logger)
- .to receive(:error)
- .with(a_string_matching("Missing .gitlab-ci.yml file"))
- .and_call_original
-
- subject
end
it 'does not create a pipeline' do
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
deleted file mode 100644
index 4cbe384b47a..00000000000
--- a/spec/workers/pipeline_success_worker_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe PipelineSuccessWorker do
- describe '#perform' do
- context 'when pipeline exists' do
- let(:pipeline) { create(:ci_pipeline, status: 'success') }
-
- it 'performs "merge when pipeline succeeds"' do
- expect_any_instance_of(
- MergeRequests::MergeWhenPipelineSucceedsService
- ).to receive(:trigger)
-
- described_class.new.perform(pipeline.id)
- end
- end
-
- context 'when pipeline does not exist' do
- it 'does not raise exception' do
- expect { described_class.new.perform(123) }
- .not_to raise_error
- end
- end
- end
-end
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index 690af22f4dc..7414470f8e7 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -32,7 +32,37 @@ describe RunPipelineScheduleWorker do
it 'calls the Service' do
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
- expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+ expect(create_pipeline_service).to receive(:execute!).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+ end
+
+ context 'when database statement timeout happens' do
+ before do
+ allow(Ci::CreatePipelineService).to receive(:new) { raise ActiveRecord::StatementInvalid }
+
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(ActiveRecord::StatementInvalid,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'increments Prometheus counter' do
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
+ .and_call_original
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching('ActiveRecord::StatementInvalid'))
+ .and_call_original
worker.perform(pipeline_schedule.id, user.id)
end
diff --git a/yarn.lock b/yarn.lock
index c8f3e102bc5..6906d6af89f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -680,10 +680,10 @@
dependencies:
bootstrap "^4.1.3"
-"@gitlab/eslint-config@^1.5.0":
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.5.0.tgz#0c8c3ae74f276eb6671bd7c60f331bc0f2d2e5cf"
- integrity sha512-KgJgoIZNpGauFpCV1iCptesYN7I8abtYRBLU9xcH0oocC/xp3JmbLfsZ+lEtrk8pl99Q2mKiAuaPpzxjXr6hBw==
+"@gitlab/eslint-config@^1.6.0":
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.6.0.tgz#1fd247d6ab477d53d4c330e05f007e3afa303689"
+ integrity sha512-EZffCwsRZmRWPP6N3wp20EJDVGYLG1v43/W7fF/gYQpUjcRclC8ks/jEv8UppasSDlanDmkh1bLWoE9CelSyyw==
dependencies:
babel-eslint "^10.0.1"
eslint-config-airbnb-base "^13.1.0"
@@ -698,10 +698,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.63.0.tgz#9dd544026d203e4ce6efed72b05db68f710c4d49"
integrity sha512-YztrReFTg31B7v5wtUC5j15KHNcMebtW+kACytEU42XomMaIwk4USIbygqWlq0VRHA2VHJrHApfJHIjxiCCQcA==
-"@gitlab/ui@^3.10.1":
- version "3.10.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.1.tgz#473e1383b05847d7226257228a8fd8d2905a9415"
- integrity sha512-LReB+EIBvZlfeEX2s/azuz6NvegrwwzficvBxK5yPCztjDeegcEsTDlt0F32mX6DcvNFacuuAyWN8QtgpaWkhA==
+"@gitlab/ui@^3.10.3":
+ version "3.10.3"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.3.tgz#dba2ddc726e203ab341d870cea2fe634f583c08d"
+ integrity sha512-Y48DKhOSC+Yw0X8PN+TyR8gITAq6jVHbiTsw+eZkCacs367L1u6w82lr7ba/Bl+4W6DhDWT34VCG0hYbGVrUJw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"