summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/issue_templates/Feature proposal.md2
-rw-r--r--.gitlab/issue_templates/Test plan.md2
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG.md20
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock19
-rw-r--r--LICENSE10
-rw-r--r--README.md21
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js4
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js2
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue17
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue28
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js8
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js1
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue4
-rw-r--r--app/assets/javascripts/pages/search/show/refresh_counts.js24
-rw-r--r--app/assets/javascripts/pages/search/show/search.js2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue2
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue178
-rw-r--r--app/assets/javascripts/registry/stores/actions.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/stylesheets/framework/callout.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss20
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss17
-rw-r--r--app/assets/stylesheets/pages/members.scss5
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss3
-rw-r--r--app/assets/stylesheets/pages/todos.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss17
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/concerns/invisible_captcha.rb51
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb10
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/controllers/projects/git_http_client_controller.rb7
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/registry/tags_controller.rb34
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb1
-rw-r--r--app/controllers/search_controller.rb9
-rw-r--r--app/finders/remote_mirror_finder.rb15
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/search_helper.rb34
-rw-r--r--app/helpers/sessions_helper.rb7
-rw-r--r--app/helpers/todos_helper.rb21
-rw-r--r--app/helpers/tracking_helper.rb17
-rw-r--r--app/mailers/emails/remote_mirrors.rb2
-rw-r--r--app/models/analytics/cycle_analytics.rb9
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb9
-rw-r--r--app/models/application_setting.rb5
-rw-r--r--app/models/application_setting_implementation.rb4
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/clusters/applications/cert_manager.rb8
-rw-r--r--app/models/clusters/applications/helm.rb2
-rw-r--r--app/models/clusters/applications/knative.rb12
-rw-r--r--app/models/clusters/applications/prometheus.rb4
-rw-r--r--app/models/commit_status.rb21
-rw-r--r--app/models/concerns/has_status.rb5
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/merge_request.rb13
-rw-r--r--app/models/namespace.rb7
-rw-r--r--app/models/notification_recipient.rb8
-rw-r--r--app/models/project.rb16
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb2
-rw-r--r--app/models/project_services/emails_on_push_service.rb1
-rw-r--r--app/models/project_services/slash_commands_service.rb2
-rw-r--r--app/models/remote_mirror.rb57
-rw-r--r--app/models/repository.rb22
-rw-r--r--app/models/user.rb10
-rw-r--r--app/policies/global_policy.rb3
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/services/ci/process_pipeline_service.rb27
-rw-r--r--app/services/git/base_hooks_service.rb45
-rw-r--r--app/services/git/branch_hooks_service.rb4
-rw-r--r--app/services/groups/update_service.rb5
-rw-r--r--app/services/merge_requests/rebase_service.rb3
-rw-r--r--app/services/notes/create_service.rb2
-rw-r--r--app/services/notification_service.rb17
-rw-r--r--app/services/projects/update_remote_mirror_service.rb53
-rw-r--r--app/services/projects/update_service.rb5
-rw-r--r--app/services/update_deployment_service.rb2
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml30
-rw-r--r--app/views/devise/sessions/_new_base.html.haml17
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml154
-rw-r--r--app/views/layouts/_search.html.haml1
-rw-r--r--app/views/layouts/_snowplow.html.haml29
-rw-r--r--app/views/profiles/preferences/show.html.haml8
-rw-r--r--app/views/projects/issues/_issue.html.haml5
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml7
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml5
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/update.js.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/_new.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/projects/wikis/history.html.haml2
-rw-r--r--app/views/projects/wikis/pages.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/_category.html.haml84
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/build_process_worker.rb16
-rw-r--r--app/workers/post_receive.rb6
-rw-r--r--app/workers/remote_mirror_notification_worker.rb2
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb61
-rwxr-xr-xbin/changelog2
-rw-r--r--changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml5
-rw-r--r--changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml5
-rw-r--r--changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml5
-rw-r--r--changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml6
-rw-r--r--changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml (renamed from changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml)0
-rw-r--r--changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml5
-rw-r--r--changelogs/unreleased/59829-fix-style-lint-wiki.yml5
-rw-r--r--changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml (renamed from changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml)0
-rw-r--r--changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml5
-rw-r--r--changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml5
-rw-r--r--changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml5
-rw-r--r--changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml5
-rw-r--r--changelogs/unreleased/65483-add-a-resend-confirmation-link.yml5
-rw-r--r--changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml6
-rw-r--r--changelogs/unreleased/65790-highlight.yml5
-rw-r--r--changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml5
-rw-r--r--changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml5
-rw-r--r--changelogs/unreleased/bump_helm_kubectl_gitlab.yml5
-rw-r--r--changelogs/unreleased/bvl-remote-mirror-exception-handling.yml6
-rw-r--r--changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml5
-rw-r--r--changelogs/unreleased/enable-specific-embeds.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml6
-rw-r--r--changelogs/unreleased/georgekoltsov-63408-user-mapping.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.59.0.yml5
-rw-r--r--changelogs/unreleased/id-source-code-smau.yml5
-rw-r--r--changelogs/unreleased/improve-quick-action-messages.yml5
-rw-r--r--changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml5
-rw-r--r--changelogs/unreleased/load-search-counts-async.yml5
-rw-r--r--changelogs/unreleased/new-cycle-analytics-backend-migrations.yml5
-rw-r--r--changelogs/unreleased/post-migrate-private-profile.yml5
-rw-r--r--changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml5
-rw-r--r--changelogs/unreleased/sh-fix-discussions-api-perf.yml5
-rw-r--r--changelogs/unreleased/sh-fix-import-export-suggestions.yml5
-rw-r--r--changelogs/unreleased/sh-only-flush-tags-once-per-push.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml5
-rw-r--r--changelogs/unreleased/sh-update-rugged-0-28-3.yml5
-rw-r--r--changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml5
-rw-r--r--config.ru18
-rw-r--r--config/application.rb10
-rw-r--r--config/initializers/7_prometheus_metrics.rb19
-rw-r--r--config/initializers/elastic_client_setup.rb74
-rw-r--r--config/initializers/invisible_captcha.rb7
-rw-r--r--config/locales/doorkeeper.en.yml2
-rw-r--r--config/locales/invisible_captcha.en.yml4
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/project.rb8
-rw-r--r--config/routes/user.rb7
-rw-r--r--db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb21
-rw-r--r--db/migrate/20190715215532_add_project_emails_disabled.rb9
-rw-r--r--db/migrate/20190715215549_add_group_emails_disabled.rb9
-rw-r--r--db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb35
-rw-r--r--db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb35
-rw-r--r--db/post_migrate/20190812070645_migrate_private_profile_nulls.rb31
-rw-r--r--db/schema.rb49
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/container_registry.md34
-rw-r--r--doc/administration/index.md5
-rw-r--r--doc/administration/monitoring/gitlab_instance_administration_project/index.md36
-rw-r--r--doc/administration/monitoring/index.md3
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md30
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/administration/operations/unicorn.md2
-rw-r--r--doc/api/dependencies.md2
-rw-r--r--doc/api/geo_nodes.md81
-rw-r--r--doc/api/releases/links.md10
-rw-r--r--doc/api/settings.md4
-rw-r--r--doc/ci/README.md6
-rw-r--r--doc/ci/directed_acyclic_graph/index.md76
-rw-r--r--doc/ci/environments.md4
-rw-r--r--doc/ci/runners/README.md6
-rw-r--r--doc/ci/yaml/README.md78
-rw-r--r--doc/customization/issue_and_merge_request_template.md4
-rw-r--r--doc/development/automatic_ce_ee_merge.md13
-rw-r--r--doc/development/contributing/merge_request_workflow.md31
-rw-r--r--doc/development/documentation/styleguide.md121
-rw-r--r--doc/development/elasticsearch.md30
-rw-r--r--doc/development/fe_guide/event_tracking.md6
-rw-r--r--doc/development/fe_guide/index.md2
-rw-r--r--doc/development/testing_guide/best_practices.md1
-rw-r--r--doc/development/understanding_explain_plans.md4
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/subscriptions/img/additional_minutes.png (renamed from doc/user/admin_area/settings/img/additional_minutes.png)bin12668 -> 12668 bytes
-rw-r--r--doc/subscriptions/img/buy_btn.png (renamed from doc/user/admin_area/settings/img/buy_btn.png)bin10917 -> 10917 bytes
-rw-r--r--doc/subscriptions/img/buy_minutes_card.png (renamed from doc/user/admin_area/settings/img/buy_minutes_card.png)bin11875 -> 11875 bytes
-rw-r--r--doc/subscriptions/index.md46
-rw-r--r--doc/update/README.md24
-rw-r--r--doc/update/upgrading_from_source.md18
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md33
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md46
-rw-r--r--doc/user/application_security/dependency_list/img/dependency_list_v12_2.pngbin0 -> 207114 bytes
-rw-r--r--doc/user/application_security/dependency_list/index.md49
-rw-r--r--doc/user/application_security/dependency_scanning/index.md13
-rw-r--r--doc/user/application_security/index.md1
-rw-r--r--doc/user/gitlab_com/index.md15
-rw-r--r--doc/user/markdown.md4
-rw-r--r--doc/user/permissions.md13
-rw-r--r--doc/user/profile/personal_access_tokens.md2
-rw-r--r--doc/user/profile/preferences.md10
-rw-r--r--doc/user/project/clusters/index.md3
-rw-r--r--doc/user/project/cycle_analytics.md28
-rw-r--r--doc/user/project/description_templates.md16
-rw-r--r--doc/user/project/img/description_templates_default_settings.pngbin26395 -> 0 bytes
-rw-r--r--doc/user/project/img/description_templates_issue_settings.pngbin0 -> 34698 bytes
-rw-r--r--doc/user/project/img/description_templates_merge_request_settings.pngbin0 -> 144128 bytes
-rw-r--r--doc/user/project/import/bitbucket.md12
-rw-r--r--doc/user/project/index.md1
-rw-r--r--doc/user/project/integrations/img/embed_metrics.pngbin0 -> 102552 bytes
-rw-r--r--doc/user/project/integrations/mattermost.md2
-rw-r--r--doc/user/project/integrations/prometheus.md22
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md7
-rw-r--r--doc/user/project/merge_requests/index.md8
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md17
-rw-r--r--doc/user/project/pipelines/schedules.md4
-rw-r--r--lib/api/discussions.rb26
-rw-r--r--lib/api/settings.rb6
-rw-r--r--lib/api/todos.rb13
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb33
-rw-r--r--lib/bitbucket/representation/comment.rb2
-rw-r--r--lib/bitbucket/representation/issue.rb2
-rw-r--r--lib/bitbucket/representation/pull_request.rb2
-rw-r--r--lib/container_registry/tag.rb7
-rw-r--r--lib/expand_variables.rb18
-rw-r--r--lib/feature/gitaly.rb2
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb8
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/config/entry/job.rb3
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb11
-rw-r--r--lib/gitlab/ci/pipeline/seed/base.rb4
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb60
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb6
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_observer.rb4
-rw-r--r--lib/gitlab/config/entry/attributable.rb4
-rw-r--r--lib/gitlab/config/entry/validators.rb11
-rw-r--r--lib/gitlab/danger/helper.rb2
-rw-r--r--lib/gitlab/data_builder/push.rb5
-rw-r--r--lib/gitlab/git_post_receive.rb12
-rw-r--r--lib/gitlab/gitaly_client/notification_service.rb22
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/kubernetes/helm.rb4
-rw-r--r--lib/gitlab/kubernetes/helm/reset_command.rb4
-rw-r--r--lib/gitlab/kubernetes/kubectl_cmd.rb19
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb16
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb3
-rw-r--r--lib/gitlab/project_search_results.rb15
-rw-r--r--lib/gitlab/project_template.rb8
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb12
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb31
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb10
-rw-r--r--lib/gitlab/search_results.rb23
-rw-r--r--lib/gitlab/snippet_search_results.rb11
-rw-r--r--lib/gitlab/snowplow_tracker.rb35
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--lib/gitlab/usage_data_counters/cycle_analytics_counter.rb8
-rw-r--r--lib/gitlab/usage_data_counters/source_code_counter.rb8
-rw-r--r--lib/tasks/gitlab/update_templates.rake51
-rw-r--r--lib/tasks/services.rake2
-rw-r--r--locale/gitlab.pot287
-rw-r--r--package.json4
-rw-r--r--qa/qa/page/main/login.rb31
-rw-r--r--qa/qa/page/project/issue/new.rb4
-rw-r--r--qa/qa/page/project/issue/show.rb2
-rw-r--r--qa/qa/page/project/menu.rb2
-rw-r--r--qa/qa/resource/merge_request.rb25
-rw-r--r--qa/qa/scenario/test/sanity/selectors.rb8
-rw-r--r--qa/qa/service/omnibus.rb11
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb13
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb23
-rw-r--r--qa/qa/vendor/saml_idp/page/login.rb8
-rw-r--r--qa/spec/resource/repository/push_spec.rb6
-rw-r--r--qa/spec/runtime/env_spec.rb24
-rw-r--r--spec/bin/changelog_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb28
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb14
-rw-r--r--spec/controllers/projects/git_http_controller_spec.rb11
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb6
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb33
-rw-r--r--spec/controllers/registrations_controller_spec.rb86
-rw-r--r--spec/controllers/search_controller_spec.rb202
-rw-r--r--spec/factories/services.rb13
-rw-r--r--spec/features/boards/multiple_boards_spec.rb15
-rw-r--r--spec/features/container_registry_spec.rb10
-rw-r--r--spec/features/invites_spec.rb1
-rw-r--r--spec/features/markdown/metrics_spec.rb26
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb4
-rw-r--r--spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb39
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb4
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb17
-rw-r--r--spec/features/users/login_spec.rb36
-rw-r--r--spec/features/users/signup_spec.rb4
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js (renamed from spec/javascripts/environments/confirm_rollback_modal_spec.js)2
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js (renamed from spec/javascripts/environments/environment_rollback_spec.js)30
-rw-r--r--spec/frontend/matchers.js38
-rw-r--r--spec/frontend/notes/components/discussion_keyboard_navigator_spec.js27
-rw-r--r--spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap7
-rw-r--r--spec/frontend/pages/search/show/refresh_counts_spec.js35
-rw-r--r--spec/frontend/test_setup.js3
-rw-r--r--spec/helpers/search_helper_spec.rb44
-rw-r--r--spec/helpers/sessions_helper_spec.rb17
-rw-r--r--spec/helpers/tracking_helper_spec.rb28
-rw-r--r--spec/javascripts/jobs/components/job_log_spec.js4
-rw-r--r--spec/javascripts/monitoring/charts/area_spec.js29
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js35
-rw-r--r--spec/javascripts/registry/components/table_registry_spec.js175
-rw-r--r--spec/javascripts/registry/mock_data.js11
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js24
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js2
-rw-r--r--spec/lib/banzai/filter/inline_metrics_filter_spec.rb33
-rw-r--r--spec/lib/bitbucket/representation/comment_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/issue_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/pull_request_spec.rb2
-rw-r--r--spec/lib/expand_variables_spec.rb175
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/ansi2html_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb10
-rw-r--r--spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb4
-rw-r--r--spec/lib/gitlab/config/entry/attributable_spec.rb3
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb16
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb34
-rw-r--r--spec/lib/gitlab/git_post_receive_spec.rb95
-rw-r--r--spec/lib/gitlab/gitaly_client/notification_service_spec.rb17
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/project.json16
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb7
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb48
-rw-r--r--spec/lib/gitlab/metrics/dashboard/url_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb22
-rw-r--r--spec/lib/gitlab/project_template_spec.rb12
-rw-r--r--spec/lib/gitlab/search_results_spec.rb37
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb18
-rw-r--r--spec/lib/gitlab/snowplow_tracker_spec.rb45
-rw-r--r--spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb9
-rw-r--r--spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb9
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb5
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb9
-rw-r--r--spec/models/clusters/applications/helm_spec.rb10
-rw-r--r--spec/models/merge_request_spec.rb39
-rw-r--r--spec/models/namespace_spec.rb60
-rw-r--r--spec/models/notification_recipient_spec.rb32
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb6
-rw-r--r--spec/models/project_services/emails_on_push_service_spec.rb20
-rw-r--r--spec/models/project_spec.rb100
-rw-r--r--spec/models/remote_mirror_spec.rb10
-rw-r--r--spec/models/repository_spec.rb25
-rw-r--r--spec/policies/clusters/instance_policy_spec.rb4
-rw-r--r--spec/policies/global_policy_spec.rb28
-rw-r--r--spec/requests/api/discussions_spec.rb55
-rw-r--r--spec/requests/api/settings_spec.rb51
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb3
-rw-r--r--spec/services/git/base_hooks_service_spec.rb72
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb4
-rw-r--r--spec/services/git/branch_push_service_spec.rb9
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb6
-rw-r--r--spec/services/git/tag_push_service_spec.rb4
-rw-r--r--spec/services/groups/update_service_spec.rb15
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb323
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb66
-rw-r--r--spec/services/projects/update_service_spec.rb22
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb36
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/helpers/email_helpers.rb4
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb144
-rw-r--r--spec/support/shared_examples/chat_slash_commands_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/policies/clusterable_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/notification_service_shared_examples.rb54
-rw-r--r--spec/tasks/gitlab/update_templates_rake_spec.rb9
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb17
-rw-r--r--spec/workers/build_process_worker_spec.rb26
-rw-r--r--spec/workers/post_receive_spec.rb91
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb111
-rw-r--r--yarn.lock18
400 files changed, 5810 insertions, 1591 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9531d10be05..f926cbc2939 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.29"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
variables:
RAILS_ENV: "test"
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 50f5b58d4e7..5c3278fcf53 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -15,7 +15,7 @@
.gitlab:assets:compile-metadata:
<<: *assets-compile-cache
extends: .dedicated-no-docs-pull-cache-job
- image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.29-docker-18.06.1
+ image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
dependencies:
- setup-test-env
services:
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index aeae5bd26e9..50476b43dd6 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -90,7 +90,7 @@
.rspec-metadata-pg-10: &rspec-metadata-pg-10
<<: *rspec-metadata
<<: *use-pg-10
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.29"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33"
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index 8a49715e0e8..2ba6b68a53e 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -26,7 +26,7 @@ Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/de
### Testing
-<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/ -->
+<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further help: https://about.gitlab.com/handbook/engineering/quality/test-engineering/ -->
### What does success look like, and how can we measure that?
diff --git a/.gitlab/issue_templates/Test plan.md b/.gitlab/issue_templates/Test plan.md
index 3aedd5859d3..f194adebc87 100644
--- a/.gitlab/issue_templates/Test plan.md
+++ b/.gitlab/issue_templates/Test plan.md
@@ -89,7 +89,7 @@ New end-to-end and integration tests (Selenium and API) should be added to the
Please note if automated tests already exist.
-When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ce/development/testing_guide/testing_levels.html)
+When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html)
in mind.
-->
diff --git a/.rubocop.yml b/.rubocop.yml
index 79e06439ac2..b75c63e1f58 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -8,7 +8,7 @@ require:
- rubocop-rspec
AllCops:
- TargetRubyVersion: 2.5
+ TargetRubyVersion: 2.6
TargetRailsVersion: 5.0
Exclude:
- 'vendor/**/*'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0752708d5e8..267a1caafec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.1.5
+
+### Security (2 changes)
+
+- Upgrade Gitaly to 1.53.2 to prevent revision flag injection exploits.
+- Upgrade pages to 1.7.1 to prevent gitlab api token recovery from cookie.
+
+
## 12.1.4
### Fixed (3 changes, 1 of them is from the community)
@@ -330,6 +338,10 @@ entry.
- Removes EE differences for app/views/admin/users/show.html.haml.
+## 12.0.6
+
+- No changes.
+
## 12.0.3 (2019-06-27)
- No changes.
@@ -678,6 +690,14 @@ entry.
- Moves snowplow to CE repo.
+## 11.11.8
+
+### Security (2 changes)
+
+- Upgrade Gitaly to 1.42.7 to prevent revision flag injection exploits.
+- Upgrade pages to 1.5.1 to prevent gitlab api token recovery from cookie.
+
+
## 11.11.7
### Security (9 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 373aea97570..bb120e876c6 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.57.0
+1.59.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index bd8bf882d06..943f9cbc4ec 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.7.0
+1.7.1
diff --git a/Gemfile b/Gemfile
index 6a8cf2981ca..a91399ab3ad 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,6 +51,7 @@ gem 'jwt', '~> 2.1.0'
# Spam and anti-bot protection
gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0'
+gem 'invisible_captcha', '~> 0.12.1'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
@@ -283,7 +284,7 @@ gem 'sentry-raven', '~> 2.9'
gem 'premailer-rails', '~> 1.9.7'
# LabKit: Tracing and Correlation
-gem 'gitlab-labkit', '~> 0.3.0'
+gem 'gitlab-labkit', '~> 0.4.2'
# I18n
gem 'ruby_parser', '~> 3.8', require: false
@@ -297,6 +298,9 @@ gem 'batch-loader', '~> 1.4.0'
# Perf bar
gem 'peek', '~> 1.0.1'
+# Snowplow events tracking
+gem 'snowplow-tracker', '~> 0.6.1'
+
# Memory benchmarks
gem 'derailed_benchmarks', require: false
@@ -424,8 +428,8 @@ group :ed25519 do
gem 'bcrypt_pbkdf', '~> 1.0'
end
-# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.37.0', require: 'gitaly'
+# Gitaly GRPC protocol definitions
+gem 'gitaly', '~> 1.58.0'
gem 'grpc', '~> 1.19.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 68c40cd19f7..fcc0fb64897 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -152,6 +152,7 @@ GEM
concurrent-ruby-ext (1.1.5)
concurrent-ruby (= 1.1.5)
connection_pool (2.2.2)
+ contracts (0.11.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
@@ -309,13 +310,13 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.37.0)
+ gitaly (1.58.0)
grpc (~> 1.0)
github-markup (1.7.0)
- gitlab-labkit (0.3.0)
+ gitlab-labkit (0.4.2)
actionpack (~> 5)
activesupport (~> 5)
- grpc (~> 1.19.0)
+ grpc (~> 1.19)
jaeger-client (~> 0.10)
opentracing (~> 0.4)
gitlab-markup (1.7.0)
@@ -437,6 +438,8 @@ GEM
influxdb (0.2.3)
cause
json
+ invisible_captcha (0.12.1)
+ rails (>= 3.2.0)
ipaddress (0.8.3)
jaeger-client (0.10.0)
opentracing (~> 0.3)
@@ -843,7 +846,7 @@ GEM
rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.2.2)
- rugged (0.28.2)
+ rugged (0.28.3.1)
safe_yaml (1.0.4)
sanitize (4.6.6)
crass (~> 1.0.2)
@@ -901,6 +904,8 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-notifier (1.5.1)
+ snowplow-tracker (0.6.1)
+ contracts (~> 0.7, <= 0.11)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
@@ -1096,9 +1101,9 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.37.0)
+ gitaly (~> 1.58.0)
github-markup (~> 1.7.0)
- gitlab-labkit (~> 0.3.0)
+ gitlab-labkit (~> 0.4.2)
gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (= 0.5.1)
gitlab-styles (~> 2.7)
@@ -1126,6 +1131,7 @@ DEPENDENCIES
httparty (~> 0.16.4)
icalendar
influxdb (~> 0.2)
+ invisible_captcha (~> 0.12.1)
jira-ruby (~> 1.4)
js_regex (~> 3.1)
json-schema (~> 2.8.0)
@@ -1229,6 +1235,7 @@ DEPENDENCIES
simple_po_parser (~> 1.1.2)
simplecov (~> 0.16.1)
slack-notifier (~> 1.5.1)
+ snowplow-tracker (~> 0.6.1)
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0)
diff --git a/LICENSE b/LICENSE
index a90ea939517..216125d93c7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,12 @@
-Copyright GitLab B.V.
+Copyright (c) 2011-present GitLab B.V.
+
+Portions of this software are licensed as follows:
+
+* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
+* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
+* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license.
+* All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
+* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 73bfcc988cc..054e2d02461 100644
--- a/README.md
+++ b/README.md
@@ -20,10 +20,6 @@ To see how GitLab looks please see the [features page on our website](https://ab
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- Completely free and open source (MIT Expat license)
-## Hiring
-
-We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/).
-
## Editions
There are two editions of GitLab:
@@ -31,6 +27,15 @@ There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
+## Licensing
+
+See the [LICENSE](LICENSE) file for licensing information as it pertains to
+files in this repository.
+
+## Hiring
+
+We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/).
+
## Website
On [about.gitlab.com](https://about.gitlab.com/) you can find more information about:
@@ -58,14 +63,6 @@ There are various other options to install GitLab, please refer to the [installa
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details.
-## Licensing
-
-GitLab Community Edition (CE) is available freely under the MIT Expat license.
-
-All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
-
-All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
-
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index eade1283513..7e3515b1f4b 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -4,7 +4,7 @@ import Mousetrap from 'mousetrap';
import axios from '../../lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
const defaultStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (e, element, combo) => {
@@ -94,7 +94,7 @@ export default class Shortcuts {
responseType: 'text',
})
.then(({ data }) => {
- $.globalEval(data);
+ $.globalEval(data, { nonce: getCspNonceValue() });
if (location && location.length > 0) {
const results = [];
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 8b7e6a56d25..208c91a1f08 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -6,6 +6,8 @@ export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
super();
Mousetrap.bind('e', ShortcutsWiki.editWiki);
+
+ this.enabledHelp.push('.hidden-shortcut.wiki');
}
static editWiki() {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 9e97f345717..ba33d72b1f3 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -107,6 +107,7 @@ function deferredInitialisation() {
.then(() => {
$('select.select2').select2({
width: 'resolve',
+ minimumResultsForSearch: 10,
dropdownAutoWidth: true,
});
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 5b950f8c966..838447e6c75 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,7 +1,6 @@
<script>
import { __ } from '~/locale';
-import { mapState } from 'vuex';
-import { GlLink, GlButton } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
@@ -16,7 +15,6 @@ let debouncedResize;
export default {
components: {
GlAreaChart,
- GlButton,
GlChartSeriesLabel,
GlLink,
Icon,
@@ -69,7 +67,6 @@ export default {
};
},
computed: {
- ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
chartData() {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
@@ -179,18 +176,6 @@ export default {
yAxisLabel() {
return `${this.graphData.y_label}`;
},
- csvText() {
- const chartData = this.chartData[0].data;
- const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
- },
- downloadLink() {
- const data = new Blob([this.csvText], { type: 'text/plain' });
- return window.URL.createObjectURL(data);
- },
},
watch: {
containerWidth: 'onResize',
@@ -259,16 +244,6 @@ export default {
<div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
- <gl-button
- v-if="exportMetricsToCsvEnabled"
- :href="downloadLink"
- :title="__('Download CSV')"
- :aria-label="__('Download CSV')"
- style="margin-left: 200px;"
- download="chart_metrics.csv"
- >
- {{ __('Download CSV') }}
- </gl-button>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 782e4310f3e..587392adbc3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -235,6 +235,19 @@ export default {
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
+ csvText(graphData) {
+ const chartData = graphData.queries[0].result[0].values;
+ const yLabel = graphData.y_label;
+ const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadCsv(graphData) {
+ const data = new Blob([this.csvText(graphData)], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
// TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
// Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845
getGraphAlerts(queries) {
@@ -448,7 +461,6 @@ export default {
@setAlerts="setAlerts"
/>
<gl-dropdown
- v-if="alertWidgetAvailable"
v-gl-tooltip
class="mx-2"
toggle-class="btn btn-transparent border-0"
@@ -459,6 +471,9 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
+ <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
+ {{ __('Download CSV') }}
+ </gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}-${graphIndex}`"
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 295c0851f12..3fbac71f3d7 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -1,7 +1,14 @@
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
-import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlModalDirective,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
import MonitorAreaChart from './charts/area.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
@@ -11,12 +18,14 @@ export default {
MonitorAreaChart,
MonitorSingleStatChart,
MonitorEmptyChart,
+ Icon,
GlDropdown,
GlDropdownItem,
GlModal,
},
directives: {
GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
},
props: {
graphData: {
@@ -41,6 +50,19 @@ export default {
graphDataHasMetrics() {
return this.graphData.queries[0].result.length > 0;
},
+ csvText() {
+ const chartData = this.graphData.queries[0].result[0].values;
+ const yLabel = this.graphData.y_label;
+ const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadCsv() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
},
methods: {
getGraphAlerts(queries) {
@@ -81,7 +103,6 @@ export default {
@setAlerts="setAlerts"
/>
<gl-dropdown
- v-if="alertWidgetAvailable"
v-gl-tooltip
class="mx-2"
toggle-class="btn btn-transparent border-0"
@@ -92,6 +113,9 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
+ <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
+ {{ __('Download CSV') }}
+ </gl-dropdown-item>
<gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`">
{{ __('Alerts') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 366034becd0..c0fee1ebb99 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -13,7 +13,6 @@ export default (props = {}) => {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
- exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled,
});
}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a9c491c7c6c..0cbad179f17 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => {
export const setFeatureFlags = (
{ commit },
- {
- prometheusEndpointEnabled,
- multipleDashboardsEnabled,
- additionalPanelTypesEnabled,
- exportMetricsToCsvEnabled,
- },
+ { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled },
) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
- commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled);
};
export const setShowErrorBanner = ({ commit }, enabled) => {
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 9ec8214b167..4b1aadbcf05 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
-export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index a2dceb21fc0..b19520d6638 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -99,7 +99,4 @@ export default {
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
- [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) {
- state.exportMetricsToCsvEnabled = enabled;
- },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index a14a25e3a20..440bdc951e0 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -10,7 +10,6 @@ export default () => ({
useDashboardEndpoint: false,
multipleDashboardsEnabled: false,
additionalPanelTypesEnabled: false,
- exportMetricsToCsvEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 8caac68e0d4..622db360d1f 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -59,6 +59,10 @@ export default () => {
render(createElement) {
const isDiffView = this.activeTab === 'diffs';
+ // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`,
+ // it adds a global key listener so it works on the diffs tab as well.
+ // If we create a single Vue app for all of the MR tabs, we should move this
+ // up the tree, to the root.
return createElement(discussionKeyboardNavigator, { props: { isDiffView } }, [
createElement('notes-app', {
props: {
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
index 5fc2b6ba04c..7fbfe8eebb2 100644
--- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
@@ -25,6 +25,10 @@ export default {
Mousetrap.bind('n', () => this.jumpToNextDiscussion());
Mousetrap.bind('p', () => this.jumpToPreviousDiscussion());
},
+ beforeDestroy() {
+ Mousetrap.unbind('n');
+ Mousetrap.unbind('p');
+ },
methods: {
...mapActions(['expandDiscussion']),
jumpToNextDiscussion() {
diff --git a/app/assets/javascripts/pages/search/show/refresh_counts.js b/app/assets/javascripts/pages/search/show/refresh_counts.js
new file mode 100644
index 00000000000..fa75ee6075d
--- /dev/null
+++ b/app/assets/javascripts/pages/search/show/refresh_counts.js
@@ -0,0 +1,24 @@
+import axios from '~/lib/utils/axios_utils';
+
+function showCount(el, count) {
+ el.textContent = count;
+ el.classList.remove('hidden');
+}
+
+function refreshCount(el) {
+ const { url } = el.dataset;
+
+ return axios
+ .get(url)
+ .then(({ data }) => showCount(el, data.count))
+ .catch(e => {
+ // eslint-disable-next-line no-console
+ console.error(`Failed to fetch search count from '${url}'.`, e);
+ });
+}
+
+export default function refreshCounts() {
+ const elements = Array.from(document.querySelectorAll('.js-search-count'));
+
+ return Promise.all(elements.map(refreshCount));
+}
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index 81b6225cb18..86ec78e1df8 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -3,6 +3,7 @@ import Flash from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
+import refreshCounts from './refresh_counts';
export default class Search {
constructor() {
@@ -14,6 +15,7 @@ export default class Search {
this.groupId = $groupDropdown.data('groupId');
this.eventListeners();
+ refreshCounts();
$groupDropdown.glDropdown({
selectable: true,
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index e157036871b..bfb2305c48c 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -84,7 +84,7 @@ export default {
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
- class="js-remove-repo"
+ class="js-remove-repo btn-inverted"
variant="danger"
>
<icon name="remove" />
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index a498a553908..e9067bc2b56 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,7 +1,13 @@
<script>
import { mapActions } from 'vuex';
-import { GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui';
-import { n__ } from '../../locale';
+import {
+ GlButton,
+ GlFormCheckbox,
+ GlTooltipDirective,
+ GlModal,
+ GlModalDirective,
+} from '@gitlab/ui';
+import { n__, s__, sprintf } from '../../locale';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
@@ -14,6 +20,7 @@ export default {
components: {
ClipboardButton,
TablePagination,
+ GlFormCheckbox,
GlButton,
Icon,
GlModal,
@@ -31,33 +38,98 @@ export default {
},
data() {
return {
- itemToBeDeleted: null,
+ itemsToBeDeleted: [],
modalId: `confirm-image-deletion-modal-${this.repo.id}`,
+ selectAllChecked: false,
+ modalDescription: '',
};
},
computed: {
+ bulkDeletePath() {
+ return this.repo.tagsPath ? this.repo.tagsPath.replace('?format=json', '/bulk_destroy') : '';
+ },
shouldRenderPagination() {
return this.repo.pagination.total > this.repo.pagination.perPage;
},
+ modalTitle() {
+ return n__(
+ 'ContainerRegistry|Remove image',
+ 'ContainerRegistry|Remove images',
+ this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length,
+ );
+ },
+ },
+ mounted() {
+ this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents);
},
methods: {
- ...mapActions(['fetchList', 'deleteItem']),
+ ...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
+ setModalDescription(itemIndex = -1) {
+ if (itemIndex === -1) {
+ this.modalDescription = sprintf(
+ s__(`ContainerRegistry|You are about to delete <b>%{count}</b> images. This will
+ delete the images and all tags pointing to them.`),
+ { count: this.itemsToBeDeleted.length },
+ );
+ } else {
+ const { tag } = this.repo.list[itemIndex];
+
+ this.modalDescription = sprintf(
+ s__(`ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will
+ delete the image and all tags pointing to this image.`),
+ { title: `${this.repo.name}:${tag}` },
+ );
+ }
+ },
layers(item) {
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
formatSize(size) {
return numberToHumanSize(size);
},
- setItemToBeDeleted(item) {
- this.itemToBeDeleted = item;
+ removeModalEvents() {
+ this.$refs.deleteModal.$refs.modal.$off('ok');
+ },
+ deleteSingleItem(index) {
+ this.setModalDescription(index);
+
+ this.$refs.deleteModal.$refs.modal.$once('ok', () => {
+ this.removeModalEvents();
+ this.handleSingleDelete(this.repo.list[index]);
+ });
+ },
+ deleteMultipleItems() {
+ if (this.itemsToBeDeleted.length === 1) {
+ this.setModalDescription(this.itemsToBeDeleted[0]);
+ } else if (this.itemsToBeDeleted.length > 1) {
+ this.setModalDescription();
+ }
+
+ this.$refs.deleteModal.$refs.modal.$once('ok', () => {
+ this.removeModalEvents();
+ this.handleMultipleDelete();
+ });
},
- handleDeleteRegistry() {
- const { itemToBeDeleted } = this;
- this.itemToBeDeleted = null;
- this.deleteItem(itemToBeDeleted)
+ handleSingleDelete(itemToDelete) {
+ this.deleteItem(itemToDelete)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
},
+ handleMultipleDelete() {
+ const { itemsToBeDeleted } = this;
+ this.itemsToBeDeleted = [];
+
+ if (this.bulkDeletePath) {
+ this.multiDeleteItems({
+ path: this.bulkDeletePath,
+ items: itemsToBeDeleted.map(x => this.repo.list[x].tag),
+ })
+ .then(() => this.fetchList({ repo: this.repo }))
+ .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ } else {
+ this.showError(errorMessagesTypes.DELETE_REGISTRY);
+ }
+ },
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
this.showError(errorMessagesTypes.FETCH_REGISTRY),
@@ -66,6 +138,35 @@ export default {
showError(message) {
createFlash(errorMessages[message]);
},
+ onSelectAllChange() {
+ if (this.selectAllChecked) {
+ this.deselectAll();
+ } else {
+ this.selectAll();
+ }
+ },
+ selectAll() {
+ this.itemsToBeDeleted = this.repo.list.map((x, index) => index);
+ this.selectAllChecked = true;
+ },
+ deselectAll() {
+ this.itemsToBeDeleted = [];
+ this.selectAllChecked = false;
+ },
+ updateItemsToBeDeleted(index) {
+ const delIndex = this.itemsToBeDeleted.findIndex(x => x === index);
+
+ if (delIndex > -1) {
+ this.itemsToBeDeleted.splice(delIndex, 1);
+ this.selectAllChecked = false;
+ } else {
+ this.itemsToBeDeleted.push(index);
+
+ if (this.itemsToBeDeleted.length === this.repo.list.length) {
+ this.selectAllChecked = true;
+ }
+ }
+ },
},
};
</script>
@@ -74,15 +175,44 @@ export default {
<table class="table tags">
<thead>
<tr>
+ <th>
+ <gl-form-checkbox
+ v-if="repo.canDelete"
+ class="js-select-all-checkbox"
+ :checked="selectAllChecked"
+ @change="onSelectAllChange"
+ />
+ </th>
<th>{{ s__('ContainerRegistry|Tag') }}</th>
<th>{{ s__('ContainerRegistry|Tag ID') }}</th>
<th>{{ s__('ContainerRegistry|Size') }}</th>
<th>{{ s__('ContainerRegistry|Last Updated') }}</th>
- <th></th>
+ <th>
+ <gl-button
+ v-if="repo.canDelete"
+ v-gl-tooltip
+ v-gl-modal="modalId"
+ :disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0"
+ class="js-delete-registry float-right"
+ variant="danger"
+ :title="s__('ContainerRegistry|Remove selected images')"
+ :aria-label="s__('ContainerRegistry|Remove selected images')"
+ @click="deleteMultipleItems()"
+ ><icon name="remove"
+ /></gl-button>
+ </th>
</tr>
</thead>
<tbody>
- <tr v-for="item in repo.list" :key="item.tag">
+ <tr v-for="(item, index) in repo.list" :key="item.tag" class="registry-image-row">
+ <td class="check">
+ <gl-form-checkbox
+ v-if="item.canDelete"
+ class="js-select-checkbox"
+ :checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)"
+ @change="updateItemsToBeDeleted(index)"
+ />
+ </td>
<td class="monospace">
{{ item.tag }}
<clipboard-button
@@ -111,16 +241,15 @@ export default {
</span>
</td>
- <td class="content">
+ <td class="content action-buttons">
<gl-button
v-if="item.canDelete"
- v-gl-tooltip
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove image')"
:aria-label="s__('ContainerRegistry|Remove image')"
variant="danger"
- class="js-delete-registry d-none d-sm-block float-right"
- @click="setItemToBeDeleted(item)"
+ class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon"
+ @click="deleteSingleItem(index)"
>
<icon name="remove" />
</gl-button>
@@ -135,19 +264,10 @@ export default {
:page-info="repo.pagination"
/>
- <gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRegistry">
- <template v-slot:modal-title>{{ s__('ContainerRegistry|Remove image') }}</template>
- <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image and tags') }}</template>
- <p
- v-html="
- sprintf(
- s__(
- 'ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will delete the image and all tags pointing to this image.',
- ),
- { title: repo.name },
- )
- "
- ></p>
+ <gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
+ <template v-slot:modal-title>{{ modalTitle }}</template>
+ <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image(s) and tags') }}</template>
+ <p v-html="modalDescription"></p>
</gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 0f5e9cc73a0..a2e0130e79e 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -36,6 +36,8 @@ export const fetchList = ({ commit }, { repo, page }) => {
};
export const deleteItem = (_, item) => axios.delete(item.destroyPath);
+export const multiDeleteItems = (_, { path, items }) =>
+ axios.delete(path, { params: { ids: items } });
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue
index a347269c916..53bf9d5ab6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue
@@ -23,7 +23,7 @@ export default {
};
</script>
<template>
- <section class="mr-widget-help">
+ <section class="mr-widget-help font-italic">
<template v-if="missingBranch">
{{ missingBranchInfo }}
</template>
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 76b96c8c1c0..8fdf61a6b8d 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
@@ -18,8 +18,8 @@ export default {
Deployment,
MrWidgetContainer,
MrWidgetPipeline,
- MergeTrainInfo: () =>
- import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'),
+ MergeTrainPositionIndicator: () =>
+ import('ee_component/vue_merge_request_widget/components/merge_train_position_indicator.vue'),
},
props: {
mr: {
@@ -62,7 +62,7 @@ export default {
showVisualReviewAppLink() {
return this.mr.visualReviewAppAvailable;
},
- showMergeTrainInfo() {
+ showMergeTrainPositionIndicator() {
return _.isNumber(this.mr.mergeTrainIndex);
},
},
@@ -90,8 +90,8 @@ export default {
:visual-review-app-meta="visualReviewAppMeta"
/>
</div>
- <merge-train-info
- v-if="showMergeTrainInfo"
+ <merge-train-position-indicator
+ v-if="showMergeTrainPositionIndicator"
class="mr-widget-extension"
:merge-train-index="mr.mergeTrainIndex"
/>
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 d4514767912..e294e1de976 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
@@ -94,9 +94,6 @@ export default {
return __('Merge');
},
- shouldShowMergeOptionsDropdown() {
- return this.isAutoMergeAvailable && !this.mr.onlyAllowMergeIfPipelineSucceeds;
- },
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
},
@@ -246,7 +243,7 @@ export default {
{{ mergeButtonText }}
</button>
<button
- v-if="isAutoMergeAvailable"
+ v-if="shouldShowMergeImmediatelyDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
@@ -256,7 +253,7 @@ export default {
<i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i>
</button>
<ul
- v-if="shouldShowMergeOptionsDropdown"
+ v-if="shouldShowMergeImmediatelyDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 116d537c463..eef49e20159 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -15,5 +15,8 @@ export default {
// MWPS is currently the only auto merge strategy available in CE
return __('Merge when pipeline succeeds');
},
+ shouldShowMergeImmediatelyDropdown() {
+ return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
+ },
},
};
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 3eab8e6fc0b..0f55bebd3fc 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
@@ -31,6 +31,9 @@ export default class MergeRequestStore {
this.targetBranchSha = data.target_branch_sha;
this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected;
+ this.conflictsDocsPath = data.conflicts_docs_path;
+ this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
+ this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 643b20c56bc..c5bb2a1256a 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -9,7 +9,9 @@
.bs-callout {
margin: $gl-padding 0;
padding: $gl-padding;
- border-left: 3px solid $border-color;
+ border-color: $border-color;
+ border-style: solid;
+ border-width: 0 0 0 3px;
color: $text-color;
background: $gray-light;
@@ -48,6 +50,10 @@
background-color: $blue-100;
border-color: $blue-200;
color: $blue-700;
+
+ h4 {
+ color: $blue-700;
+ }
}
.bs-callout-success {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index c108f45622f..92190f8979e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -603,6 +603,7 @@ $blame-blue: #254e77;
* Builds
*/
$builds-trace-bg: #111;
+$job-log-highlight-height: 18px;
/*
* Commit Page
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6fc742871e7..73166940146 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -124,6 +124,26 @@
float: left;
padding-left: $gl-padding-8;
}
+
+ .section-start {
+ display: inline;
+ }
+
+ .section-start,
+ .section-header {
+ &:hover {
+ cursor: pointer;
+
+ &::after {
+ content: '';
+ background-color: rgba($white-light, 0.2);
+ left: 0;
+ right: 0;
+ position: absolute;
+ height: $job-log-highlight-height;
+ }
+ }
+ }
}
.build-header {
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index a21fa29f34a..0f4bdb219a3 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -31,4 +31,21 @@
.table.tags {
margin-bottom: 0;
+
+ .registry-image-row {
+ .check {
+ padding-right: $gl-padding;
+ width: 5%;
+ }
+
+ .action-buttons {
+ opacity: 0;
+ }
+
+ &:hover {
+ .action-buttons {
+ opacity: 1;
+ }
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 45408c9ab3c..ae92a2fbd7b 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -58,11 +58,6 @@
}
}
-.member-access-text {
- margin-left: auto;
- line-height: 43px;
-}
-
.member-search-btn {
position: absolute;
right: 4px;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 3c1e384d6ed..c8d155706a9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -397,7 +397,6 @@
.mr-widget-help {
padding: 10px 16px 10px ($gl-padding-8 * 7);
- font-style: italic;
}
.ci-coverage {
@@ -906,7 +905,7 @@
}
.deploy-heading,
-.merge-train-info {
+.merge-train-position-indicator {
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 586365eb1ce..7b64c67ae34 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -4,7 +4,7 @@
*/
.todos-list > .todo {
- // workaround because we cannot use border-colapse
+ // workaround because we cannot use border-collapse
border-top: 1px solid transparent;
display: flex;
flex-direction: row;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 60400f10ca5..379df1c4db1 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -1,19 +1,3 @@
-.new-wiki-page {
- .new-wiki-page-slug-tip {
- display: inline-block;
- max-width: 100%;
- margin-top: 5px;
- }
-}
-
-.wiki-form {
- .edit-wiki-page-slug-tip {
- display: inline-block;
- max-width: 100%;
- margin-top: 5px;
- }
-}
-
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
@@ -22,7 +6,6 @@
}
.wiki-page-header {
- @extend .top-area;
position: relative;
.wiki-breadcrumb {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1d55a073f3b..5e65084a110 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -116,7 +116,7 @@ class ApplicationController < ActionController::Base
def render(*args)
super.tap do
# Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse
- if response.content_type == 'text/html' && (400..599).cover?(response.status)
+ if (400..599).cover?(response.status) && workhorse_excluded_content_types.include?(response.content_type)
response.headers['X-GitLab-Custom-Error'] = '1'
end
end
@@ -124,6 +124,10 @@ class ApplicationController < ActionController::Base
protected
+ def workhorse_excluded_content_types
+ @workhorse_excluded_content_types ||= %w(text/html application/json)
+ end
+
def append_info_to_payload(payload)
super
diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb
new file mode 100644
index 00000000000..c9f66e5c194
--- /dev/null
+++ b/app/controllers/concerns/invisible_captcha.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module InvisibleCaptcha
+ extend ActiveSupport::Concern
+
+ included do
+ invisible_captcha only: :create, on_spam: :on_honeypot_spam_callback, on_timestamp_spam: :on_timestamp_spam_callback
+ end
+
+ def on_honeypot_spam_callback
+ return unless Feature.enabled?(:invisible_captcha)
+
+ invisible_captcha_honeypot_counter.increment
+ log_request('Invisible_Captcha_Honeypot_Request')
+
+ head(200)
+ end
+
+ def on_timestamp_spam_callback
+ return unless Feature.enabled?(:invisible_captcha)
+
+ invisible_captcha_timestamp_counter.increment
+ log_request('Invisible_Captcha_Timestamp_Request')
+
+ redirect_to new_user_session_path, alert: InvisibleCaptcha.timestamp_error_message
+ end
+
+ def invisible_captcha_honeypot_counter
+ @invisible_captcha_honeypot_counter ||=
+ Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_honeypot,
+ 'Counter of blocked sign up attempts with filled honeypot')
+ end
+
+ def invisible_captcha_timestamp_counter
+ @invisible_captcha_timestamp_counter ||=
+ Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_timestamp,
+ 'Counter of blocked sign up attempts with invalid timestamp')
+ end
+
+ def log_request(message)
+ request_information = {
+ message: message,
+ env: :invisible_captcha_signup_bot_detected,
+ ip: request.ip,
+ request_method: request.request_method,
+ fullpath: request.fullpath
+ }
+
+ Gitlab::AuthLogger.error(request_information)
+ end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 5472ef05d7c..886d1f99d69 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -176,6 +176,7 @@ class GroupsController < Groups::ApplicationController
[
:avatar,
:description,
+ :emails_disabled,
:lfs_enabled,
:name,
:path,
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 2d46a71bf99..3b0abecf2c9 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -14,8 +14,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
@cycle_analytics_no_data = @cycle_analytics.no_stats?
respond_to do |format|
- format.html
- format.json { render json: cycle_analytics_json }
+ format.html do
+ Gitlab::UsageDataCounters::CycleAnalyticsCounter.count(:views)
+
+ render :show
+ end
+ format.json do
+ render json: cycle_analytics_json
+ end
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index df9e55fda2a..5a1f93dc609 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:export_metrics_to_csv_enabled)
end
def index
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 956093b972b..abf8407a51c 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -49,7 +49,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_final_spnego_response
return # Allow access
end
- elsif project && download_request? && Guest.can?(:download_code, project)
+ elsif project && download_request? && http_allowed? && Guest.can?(:download_code, project)
+
@authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
return # Allow access
@@ -113,4 +114,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def ci?
authentication_result.ci?(project)
end
+
+ def http_allowed?
+ Gitlab::ProtocolAccess.allowed?('http')
+ end
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 3254229d9cb..c94fdd9483d 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -26,7 +26,7 @@ class Projects::RawController < Projects::ApplicationController
limiter.log_request(request, :raw_blob_request_limit, current_user)
flash[:alert] = _('You cannot access the raw file. Please wait a minute.')
- redirect_to project_blob_path(@project, File.join(@ref, @path))
+ redirect_to project_blob_path(@project, File.join(@ref, @path)), status: :too_many_requests
end
def raw_blob_request_limit
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index bf1d8d8b5fc..54e2faa2dd7 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -5,6 +5,8 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_destroy_container_image!, only: [:destroy]
+ LIMIT = 15
+
def index
respond_to do |format|
format.json do
@@ -28,10 +30,40 @@ module Projects
end
end
+ def bulk_destroy
+ unless params[:ids].present?
+ head :bad_request
+ return
+ end
+
+ tag_names = params[:ids] || []
+ if tag_names.size > LIMIT
+ head :bad_request
+ return
+ end
+
+ @tags = tag_names.map { |tag_name| image.tag(tag_name) }
+ unless @tags.all? { |tag| tag.valid_name? }
+ head :bad_request
+ return
+ end
+
+ success_count = 0
+ @tags.each do |tag|
+ if tag.delete
+ success_count += 1
+ end
+ end
+
+ respond_to do |format|
+ format.json { head(success_count == @tags.size ? :no_content : :bad_request) }
+ end
+ end
+
private
def tags
- Kaminari::PaginatableArray.new(image.tags, limit: 15)
+ Kaminari::PaginatableArray.new(image.tags, limit: LIMIT)
end
def image
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index d4ff72c2314..e04cbf10470 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -361,6 +361,7 @@ class ProjectsController < Projects::ApplicationController
:container_registry_enabled,
:default_branch,
:description,
+ :emails_disabled,
:external_authorization_classification_label,
:import_url,
:issues_tracker,
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 638934694e0..db10515c0b4 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
include RecaptchaExperimentHelper
+ include InvisibleCaptcha
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 13741548687..e30935be4b6 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -36,6 +36,15 @@ class SearchController < ApplicationController
check_single_commit_result
end
+ def count
+ params.require([:search, :scope])
+
+ scope = search_service.scope
+ count = search_service.search_results.formatted_count(scope)
+
+ render json: { count: count }
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def autocomplete
term = params[:term]
diff --git a/app/finders/remote_mirror_finder.rb b/app/finders/remote_mirror_finder.rb
deleted file mode 100644
index 420db0077aa..00000000000
--- a/app/finders/remote_mirror_finder.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class RemoteMirrorFinder
- attr_accessor :params
-
- def initialize(params)
- @params = params
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def execute
- RemoteMirror.find_by(id: params[:id])
- end
- # rubocop: enable CodeReuse/ActiveRecord
-end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index acbcf0ded17..0ab19f1d2d2 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -270,7 +270,11 @@ module ApplicationSettingsHelper
:diff_max_patch_bytes,
:commit_email_hostname,
:protected_ci_variables,
- :local_markdown_version
+ :local_markdown_version,
+ :snowplow_collector_hostname,
+ :snowplow_cookie_domain,
+ :snowplow_enabled,
+ :snowplow_site_id
]
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index f5c4686a3bf..91c83380b62 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -145,17 +145,27 @@ module SearchHelper
Sanitize.clean(str)
end
- def search_filter_path(options = {})
- exist_opts = {
- search: params[:search],
- project_id: params[:project_id],
- group_id: params[:group_id],
- scope: params[:scope],
- repository_ref: params[:repository_ref]
- }
+ def search_filter_link(scope, label, data: {}, search: {})
+ search_params = params
+ .merge(search)
+ .merge({ scope: scope })
+ .permit(:search, :scope, :project_id, :group_id, :repository_ref, :snippets)
+
+ if @scope == scope
+ li_class = 'active'
+ count = @search_results.formatted_count(scope)
+ else
+ badge_class = 'js-search-count hidden'
+ badge_data = { url: search_count_path(search_params) }
+ end
- options = exist_opts.merge(options)
- search_path(options)
+ content_tag :li, class: li_class, data: data do
+ link_to search_path(search_params) do
+ concat label
+ concat ' '
+ concat content_tag(:span, count, class: ['badge badge-pill', badge_class], data: badge_data)
+ end
+ end
end
def search_filter_input_options(type)
@@ -212,10 +222,6 @@ module SearchHelper
sanitize(html, tags: %w(a p ol ul li pre code))
end
- def limited_count(count, limit = 1000)
- count > limit ? "#{limit}+" : count
- end
-
def search_tabs?(tab)
return false if Feature.disabled?(:users_search, default_enabled: true)
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
new file mode 100644
index 00000000000..af98a611b8b
--- /dev/null
+++ b/app/helpers/sessions_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module SessionsHelper
+ def unconfirmed_email?
+ flash[:alert] == t(:unconfirmed, scope: [:devise, :failure])
+ end
+end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 645160077f5..38142bc68cb 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -26,7 +26,7 @@ module TodosHelper
end
def todo_target_link(todo)
- text = raw("#{todo.target_type.titleize.downcase} ") +
+ text = raw(todo_target_type_name(todo) + ' ') +
if todo.for_commit?
content_tag(:span, todo.target_reference, class: 'commit-sha')
else
@@ -36,23 +36,34 @@ module TodosHelper
link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title
end
+ def todo_target_type_name(todo)
+ todo.target_type.titleize.downcase
+ end
+
def todo_target_path(todo)
return unless todo.target.present?
- anchor = dom_id(todo.note) if todo.note.present?
+ path_options = todo_target_path_options(todo)
if todo.for_commit?
- project_commit_path(todo.project,
- todo.target, anchor: anchor)
+ project_commit_path(todo.project, todo.target, path_options)
else
path = [todo.parent, todo.target]
path.unshift(:pipelines) if todo.build_failed?
- polymorphic_path(path, anchor: anchor)
+ polymorphic_path(path, path_options)
end
end
+ def todo_target_path_options(todo)
+ { anchor: todo_target_path_anchor(todo) }
+ end
+
+ def todo_target_path_anchor(todo)
+ dom_id(todo.note) if todo.note.present?
+ end
+
def todo_target_state_pill(todo)
return unless show_todo_state?(todo)
diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb
index 51ea79d1ddd..221d9692661 100644
--- a/app/helpers/tracking_helper.rb
+++ b/app/helpers/tracking_helper.rb
@@ -2,6 +2,21 @@
module TrackingHelper
def tracking_attrs(label, event, property)
- {} # CE has no tracking features
+ return {} unless tracking_enabled?
+
+ {
+ data: {
+ track_label: label,
+ track_event: event,
+ track_property: property
+ }
+ }
+ end
+
+ private
+
+ def tracking_enabled?
+ Rails.env.production? &&
+ ::Gitlab::CurrentSettings.snowplow_enabled?
end
end
diff --git a/app/mailers/emails/remote_mirrors.rb b/app/mailers/emails/remote_mirrors.rb
index 2d8137843ec..f3938a052b0 100644
--- a/app/mailers/emails/remote_mirrors.rb
+++ b/app/mailers/emails/remote_mirrors.rb
@@ -3,7 +3,7 @@
module Emails
module RemoteMirrors
def remote_mirror_update_failed_email(remote_mirror_id, recipient_id)
- @remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
+ @remote_mirror = RemoteMirror.find_by_id(remote_mirror_id)
@project = @remote_mirror.project
mail(to: recipient(recipient_id, @project.group), subject: subject('Remote mirror update failed'))
diff --git a/app/models/analytics/cycle_analytics.rb b/app/models/analytics/cycle_analytics.rb
new file mode 100644
index 00000000000..626fc91cc41
--- /dev/null
+++ b/app/models/analytics/cycle_analytics.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ def self.table_name_prefix
+ 'analytics_cycle_analytics_'
+ end
+ end
+end
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
new file mode 100644
index 00000000000..88c8cb40ccb
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ProjectStage < ApplicationRecord
+ belongs_to :project
+ end
+ end
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index cb6346421ec..2a99c6e5c59 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -99,6 +99,11 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :plantuml_enabled
+ validates :snowplow_collector_hostname,
+ presence: true,
+ hostname: true,
+ if: :snowplow_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index b7a4d7aa803..55ac1e129cf 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -97,6 +97,10 @@ module ApplicationSettingImplementation
usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname,
+ snowplow_collector_hostname: nil,
+ snowplow_cookie_domain: nil,
+ snowplow_enabled: false,
+ snowplow_site_id: nil,
protected_ci_variables: false,
local_markdown_version: 0,
outbound_local_requests_whitelist: [],
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ac88d9714ac..f705e67121f 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -384,7 +384,7 @@ module Ci
return unless has_environment?
strong_memoize(:expanded_environment_name) do
- ExpandVariables.expand(environment, simple_variables)
+ ExpandVariables.expand(environment, -> { simple_variables })
end
end
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 2fc1b67dfd2..6bd7473c8ff 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -64,11 +64,15 @@ module Clusters
end
def delete_private_key
- "kubectl delete secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found" if private_key_name.present?
+ return unless private_key_name.present?
+
+ args = %W(secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found)
+
+ Gitlab::Kubernetes::KubectlCmd.delete(*args)
end
def delete_crd(definition)
- "kubectl delete crd #{definition} --ignore-not-found"
+ Gitlab::Kubernetes::KubectlCmd.delete("crd", definition, "--ignore-not-found")
end
def cluster_issuer_file
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 3a175fec148..455cf200fbc 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -41,7 +41,7 @@ module Clusters
extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id))
- applications = applications.present? ? applications.or(extra_apps) : extra_apps
+ applications = applications ? applications.or(extra_apps) : extra_apps
end
!applications.exists?
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 5eae23659ae..244fe738396 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -89,7 +89,7 @@ module Clusters
def delete_knative_services
cluster.kubernetes_namespaces.map do |kubernetes_namespace|
- "kubectl delete ksvc --all -n #{kubernetes_namespace.namespace}"
+ Gitlab::Kubernetes::KubectlCmd.delete("ksvc", "--all", "-n", kubernetes_namespace.namespace)
end
end
@@ -99,14 +99,14 @@ module Clusters
def delete_knative_namespaces
[
- "kubectl delete --ignore-not-found ns knative-serving",
- "kubectl delete --ignore-not-found ns knative-build"
+ Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-serving"),
+ Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-build")
]
end
def delete_knative_and_istio_crds
api_resources.map do |crd|
- "kubectl delete --ignore-not-found crd #{crd}"
+ Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "crd", "#{crd}")
end
end
@@ -119,13 +119,13 @@ module Clusters
def install_knative_metrics
return [] unless cluster.application_prometheus_available?
- ["kubectl apply -f #{METRICS_CONFIG}"]
+ [Gitlab::Kubernetes::KubectlCmd.apply_file(METRICS_CONFIG)]
end
def delete_knative_istio_metrics
return [] unless cluster.application_prometheus_available?
- ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"]
+ [Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)]
end
def verify_cluster?
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 08e52f32bb3..f31a6b8b50e 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -106,13 +106,13 @@ module Clusters
def install_knative_metrics
return [] unless cluster.application_knative_available?
- ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"]
+ [Gitlab::Kubernetes::KubectlCmd.apply_file(Clusters::Applications::Knative::METRICS_CONFIG)]
end
def delete_knative_istio_metrics
return [] unless cluster.application_knative_available?
- ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"]
+ [Gitlab::Kubernetes::KubectlCmd.delete("-f", Clusters::Applications::Knative::METRICS_CONFIG)]
end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index a88cac6b8e6..4be4d95b4a1 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -40,8 +40,11 @@ class CommitStatus < ApplicationRecord
scope :ordered, -> { order(:name) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
+ scope :before_stage, -> (index) { where('stage_idx < ?', index) }
+ scope :for_stage, -> (index) { where(stage_idx: index) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) }
+ scope :for_ids, -> (ids) { where(id: ids) }
scope :with_needs, -> (names = nil) do
needs = Ci::BuildNeed.scoped_build.select(1)
@@ -49,8 +52,10 @@ class CommitStatus < ApplicationRecord
where('EXISTS (?)', needs).preload(:needs)
end
- scope :without_needs, -> do
- where('NOT EXISTS (?)', Ci::BuildNeed.scoped_build.select(1))
+ scope :without_needs, -> (names = nil) do
+ needs = Ci::BuildNeed.scoped_build.select(1)
+ needs = needs.where(name: names) if names
+ where('NOT EXISTS (?)', needs)
end
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
@@ -149,6 +154,18 @@ class CommitStatus < ApplicationRecord
end
end
+ def self.names
+ select(:name)
+ end
+
+ def self.status_for_prior_stages(index)
+ before_stage(index).latest.status || 'success'
+ end
+
+ def self.status_for_names(names)
+ where(name: names).latest.status || 'success'
+ end
+
def locking_enabled?
will_save_change_to_status?
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 27a5c3d5286..71ebb586c13 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -106,10 +106,15 @@ module HasStatus
scope :running_or_pending, -> { with_status(:running, :pending) }
scope :finished, -> { with_status(:success, :failed, :canceled) }
scope :failed_or_canceled, -> { with_status(:failed, :canceled) }
+ scope :incomplete, -> { without_statuses(completed_statuses) }
scope :cancelable, -> do
where(status: [:running, :preparing, :pending, :created, :scheduled])
end
+
+ scope :without_statuses, -> (names) do
+ with_status(all_state_names - names.to_a)
+ end
end
def started?
diff --git a/app/models/label.rb b/app/models/label.rb
index 25de26b8384..d9455b36242 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -138,6 +138,8 @@ class Label < ApplicationRecord
end
def self.on_project_board?(project_id, label_id)
+ return false if label_id.blank?
+
on_project_boards(project_id).where(id: label_id).exists?
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4306dd9266f..bfd636fa62a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -220,18 +220,7 @@ class MergeRequest < ApplicationRecord
end
def rebase_in_progress?
- (rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) ||
- gitaly_rebase_in_progress?
- end
-
- # TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable
- def gitaly_rebase_in_progress?
- strong_memoize(:gitaly_rebase_in_progress) do
- # The source project can be deleted
- next false unless source_project
-
- source_project.repository.rebase_in_progress?(id)
- end
+ rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
end
# Use this method whenever you need to make sure the head_pipeline is synced with the
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 058350b16ce..9f9c4288667 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -172,6 +172,13 @@ class Namespace < ApplicationRecord
end
end
+ # any ancestor can disable emails for all descendants
+ def emails_disabled?
+ strong_memoize(:emails_disabled) do
+ Feature.enabled?(:emails_disabled, self, default_enabled: true) && self_and_ancestors.where(emails_disabled: true).exists?
+ end
+ end
+
def lfs_enabled?
# User namespace will always default to the global setting
Gitlab.config.lfs.enabled
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index a7f73c0f29c..8e44e3d8e17 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -4,6 +4,7 @@ class NotificationRecipient
include Gitlab::Utils::StrongMemoize
attr_reader :user, :type, :reason
+
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
@@ -30,6 +31,7 @@ class NotificationRecipient
def notifiable?
return false unless has_access?
+ return false if emails_disabled?
return false if own_activity?
# even users with :disabled notifications receive manual subscriptions
@@ -109,6 +111,12 @@ class NotificationRecipient
private
+ # They are disabled if the project or group has disallowed it.
+ # No need to check the group if there is already a project
+ def emails_disabled?
+ @project ? @project.emails_disabled? : @group&.emails_disabled?
+ end
+
def read_ability
return if @skip_read_ability
return @read_ability if instance_variable_defined?(:@read_ability)
diff --git a/app/models/project.rb b/app/models/project.rb
index a6e43efa1f3..8efe4b06f87 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -283,6 +283,7 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
+ has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
@@ -631,6 +632,13 @@ class Project < ApplicationRecord
alias_method :ancestors, :ancestors_upto
+ def emails_disabled?
+ strong_memoize(:emails_disabled) do
+ # disabling in the namespace overrides the project setting
+ Feature.enabled?(:emails_disabled, self, default_enabled: true) && (super || namespace.emails_disabled?)
+ end
+ end
+
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
@@ -1230,6 +1238,14 @@ class Project < ApplicationRecord
end
end
+ def has_active_hooks?(hooks_scope = :push_hooks)
+ hooks.hooks_for(hooks_scope).any? || SystemHook.hooks_for(hooks_scope).any?
+ end
+
+ def has_active_services?(hooks_scope = :push_hooks)
+ services.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def valid_repo?
repository.exists?
rescue
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 4edf263433f..a3793d9937b 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -68,7 +68,7 @@ module ChatMessage
title_link: pipeline_url,
fields: attachments_fields,
footer: project.name,
- footer_icon: project.avatar_url,
+ footer_icon: project.avatar_url(only_path: false),
ts: finished_at
}]
end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 45de64a9990..8ca40138a8f 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -24,6 +24,7 @@ class EmailsOnPushService < Service
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
+ return if project.emails_disabled?
EmailsOnPushWorker.perform_async(
project_id,
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
index 5f5cff97808..cb16ad75d14 100644
--- a/app/models/project_services/slash_commands_service.rb
+++ b/app/models/project_services/slash_commands_service.rb
@@ -35,6 +35,8 @@ class SlashCommandsService < Service
chat_user = find_chat_user(params)
if chat_user&.user
+ return Gitlab::SlashCommands::Presenters::Access.new.access_denied unless chat_user.user.can?(:use_slash_commands)
+
Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else
url = authorize_chat_name_url(params)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 6b5605f9999..c9ee0653d86 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -4,6 +4,8 @@ class RemoteMirror < ApplicationRecord
include AfterCommitQueue
include MirrorAuthentication
+ MAX_FIRST_RUNTIME = 3.hours
+ MAX_INCREMENTAL_RUNTIME = 1.hour
PROTECTED_BACKOFF_DELAY = 1.minute
UNPROTECTED_BACKOFF_DELAY = 5.minutes
@@ -31,11 +33,18 @@ class RemoteMirror < ApplicationRecord
scope :enabled, -> { where(enabled: true) }
scope :started, -> { with_update_status(:started) }
- scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.hour.ago, 3.hours.ago) }
+
+ scope :stuck, -> do
+ started
+ .where('(last_update_started_at < ? AND last_update_at IS NOT NULL)',
+ MAX_INCREMENTAL_RUNTIME.ago)
+ .or(where('(last_update_started_at < ? AND last_update_at IS NULL)',
+ MAX_FIRST_RUNTIME.ago))
+ end
state_machine :update_status, initial: :none do
event :update_start do
- transition [:none, :finished, :failed] => :started
+ transition any => :started
end
event :update_finish do
@@ -46,9 +55,14 @@ class RemoteMirror < ApplicationRecord
transition started: :failed
end
+ event :update_retry do
+ transition started: :to_retry
+ end
+
state :started
state :finished
state :failed
+ state :to_retry
after_transition any => :started do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_running)
@@ -138,16 +152,27 @@ class RemoteMirror < ApplicationRecord
end
def updated_since?(timestamp)
- last_update_started_at && last_update_started_at > timestamp && !update_failed?
+ return false if failed?
+
+ last_update_started_at && last_update_started_at > timestamp
end
def mark_for_delete_if_blank_url
mark_for_destruction if url.blank?
end
- def mark_as_failed(error_message)
- update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message))
- update_fail
+ def update_error_message(error_message)
+ self.last_error = Gitlab::UrlSanitizer.sanitize(error_message)
+ end
+
+ def mark_for_retry!(error_message)
+ update_error_message(error_message)
+ update_retry!
+ end
+
+ def mark_as_failed!(error_message)
+ update_error_message(error_message)
+ update_fail!
end
def url=(value)
@@ -190,6 +215,18 @@ class RemoteMirror < ApplicationRecord
update_column(:error_notification_sent, true)
end
+ def backoff_delay
+ if self.only_protected_branches
+ PROTECTED_BACKOFF_DELAY
+ else
+ UNPROTECTED_BACKOFF_DELAY
+ end
+ end
+
+ def max_runtime
+ last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME
+ end
+
private
def store_credentials
@@ -219,14 +256,6 @@ class RemoteMirror < ApplicationRecord
self.last_update_started_at >= Time.now - backoff_delay
end
- def backoff_delay
- if self.only_protected_branches
- PROTECTED_BACKOFF_DELAY
- else
- UNPROTECTED_BACKOFF_DELAY
- end
- end
-
def reset_fields
update_columns(
last_error: nil,
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a89f573e3d6..9d45a12fa6e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -418,25 +418,29 @@ class Repository
end
# Runs code before pushing (= creating or removing) a tag.
+ #
+ # Note that this doesn't expire the tags. You may need to call
+ # expire_caches_for_tags or expire_tags_cache.
def before_push_tag
+ repository_event(:push_tag)
+ end
+
+ def expire_caches_for_tags
expire_statistics_caches
expire_emptiness_caches
expire_tags_cache
-
- repository_event(:push_tag)
end
# Runs code before removing a tag.
def before_remove_tag
- expire_tags_cache
- expire_statistics_caches
+ expire_caches_for_tags
repository_event(:remove_tag)
end
# Runs code after removing a tag.
def after_remove_tag
- expire_tags_cache
+ expire_caches_for_tags
end
# Runs code after the HEAD of a repository is changed.
@@ -460,8 +464,8 @@ class Repository
end
# Runs code after a new branch has been created.
- def after_create_branch
- expire_branches_cache
+ def after_create_branch(expire_cache: true)
+ expire_branches_cache if expire_cache
repository_event(:push_branch)
end
@@ -474,8 +478,8 @@ class Repository
end
# Runs code after an existing branch has been removed.
- def after_remove_branch
- expire_branches_cache
+ def after_remove_branch(expire_cache: true)
+ expire_branches_cache if expire_cache
end
def method_missing(msg, *args, &block)
diff --git a/app/models/user.rb b/app/models/user.rb
index ac83c8e3256..374e00987c5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -438,18 +438,20 @@ class User < ApplicationRecord
order = <<~SQL
CASE
- WHEN users.name = %{query} THEN 0
- WHEN users.username = %{query} THEN 1
- WHEN users.email = %{query} THEN 2
+ WHEN users.name = :query THEN 0
+ WHEN users.username = :query THEN 1
+ WHEN users.email = :query THEN 2
ELSE 3
END
SQL
+ sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query]))
+
where(
fuzzy_arel_match(:name, query, lower_exact_match: true)
.or(fuzzy_arel_match(:username, query, lower_exact_match: true))
.or(arel_table[:email].eq(query))
- ).reorder(order % { query: ApplicationRecord.connection.quote(query) }, :name)
+ ).reorder(sanitized_order_sql, :name)
end
# Limits the result set to users _not_ in the given query/list of IDs.
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 134de1c9ace..311aab0dcd4 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -33,6 +33,7 @@ class GlobalPolicy < BasePolicy
enable :access_git
enable :receive_notifications
enable :use_quick_actions
+ enable :use_slash_commands
end
rule { blocked | internal }.policy do
@@ -40,6 +41,7 @@ class GlobalPolicy < BasePolicy
prevent :access_api
prevent :access_git
prevent :receive_notifications
+ prevent :use_slash_commands
end
rule { required_terms_not_accepted }.policy do
@@ -57,6 +59,7 @@ class GlobalPolicy < BasePolicy
rule { access_locked }.policy do
prevent :log_in
+ prevent :use_slash_commands
end
rule { ~(anonymous & restricted_public_level) }.policy do
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 52c944491bf..c686e7763bb 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -92,6 +92,7 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
enable :set_note_created_at
+ enable :set_emails_disabled
end
rule { can?(:read_nested_project_resources) }.policy do
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index e79bac6bee3..b8dee1b0789 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -162,6 +162,7 @@ class ProjectPolicy < BasePolicy
enable :set_issue_created_at
enable :set_issue_updated_at
enable :set_note_created_at
+ enable :set_emails_disabled
end
rule { can?(:guest_access) }.policy do
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 99d4ff9ecd1..f4bd457ebc6 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -42,14 +42,19 @@ module Ci
return false unless trigger_build_ids.present?
return false unless Feature.enabled?(:ci_dag_support, project)
- # rubocop: disable CodeReuse/ActiveRecord
- trigger_build_names = pipeline.statuses
- .where(id: trigger_build_ids)
- .select(:name)
- # rubocop: enable CodeReuse/ActiveRecord
+ # we find processables that are dependent:
+ # 1. because of current dependency,
+ trigger_build_names = pipeline.processables.latest
+ .for_ids(trigger_build_ids).names
+ # 2. does not have builds that not yet complete
+ incomplete_build_names = pipeline.processables.latest
+ .incomplete.names
+
+ # Each found processable is guaranteed here to have completed status
created_processables
.with_needs(trigger_build_names)
+ .without_needs(incomplete_build_names)
.find_each
.map(&method(:process_build_with_needs))
.any?
@@ -70,17 +75,13 @@ module Ci
end
end
- # rubocop: disable CodeReuse/ActiveRecord
def status_for_prior_stages(index)
- pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
+ pipeline.processables.status_for_prior_stages(index)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def status_for_build_needs(needs)
- pipeline.builds.where(name: needs).latest.status || 'success'
+ pipeline.processables.status_for_names(needs)
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def stage_indexes_of_created_processables_without_needs
@@ -89,12 +90,10 @@ module Ci
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def created_processables_in_stage_without_needs(index)
created_processables_without_needs
- .where(stage_idx: index)
+ .for_stage(index)
end
- # rubocop: enable CodeReuse/ActiveRecord
def created_processables_without_needs
if Feature.enabled?(:ci_dag_support, project)
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index d30df34e54b..1db18fcf401 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -19,7 +19,7 @@ module Git
update_remote_mirrors
- push_data
+ success
end
private
@@ -33,7 +33,7 @@ module Git
end
def limited_commits
- commits.last(PROCESS_COMMIT_LIMIT)
+ @limited_commits ||= commits.last(PROCESS_COMMIT_LIMIT)
end
def commits_count
@@ -48,21 +48,25 @@ module Git
[]
end
+ # Push events in the activity feed only show information for the
+ # last commit.
def create_events
- EventCreateService.new.push(project, current_user, push_data)
+ EventCreateService.new.push(project, current_user, event_push_data)
end
def create_pipelines
return unless params.fetch(:create_pipelines, true)
Ci::CreatePipelineService
- .new(project, current_user, push_data)
+ .new(project, current_user, base_params)
.execute(:push, pipeline_options)
end
def execute_project_hooks
- project.execute_hooks(push_data, hook_name)
- project.execute_services(push_data, hook_name)
+ # Creating push_data invokes one CommitDelta RPC per commit. Only
+ # build this data if we actually need it.
+ project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
+ project.execute_services(push_data, hook_name) if project.has_active_services?(hook_name)
end
def enqueue_invalidate_cache
@@ -73,18 +77,35 @@ module Git
)
end
- def push_data
- @push_data ||= Gitlab::DataBuilder::Push.build(
- project: project,
- user: current_user,
+ def base_params
+ {
oldrev: params[:oldrev],
newrev: params[:newrev],
ref: params[:ref],
- commits: limited_commits,
+ push_options: params[:push_options] || {}
+ }
+ end
+
+ def push_data_params(commits:, with_changed_files: true)
+ base_params.merge(
+ project: project,
+ user: current_user,
+ commits: commits,
message: event_message,
commits_count: commits_count,
- push_options: params[:push_options] || {}
+ with_changed_files: with_changed_files
)
+ end
+
+ def event_push_data
+ # We only need the last commit for the event push, and we don't
+ # need the full deltas either.
+ @event_push_data ||= Gitlab::DataBuilder::Push.build(
+ push_data_params(commits: commits.last, with_changed_files: false))
+ end
+
+ def push_data
+ @push_data ||= Gitlab::DataBuilder::Push.build(push_data_params(commits: limited_commits))
# Dependent code may modify the push data, so return a duplicate each time
@push_data.dup
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index c41f445c3c4..431a5aedf2e 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -63,7 +63,7 @@ module Git
end
def branch_create_hooks
- project.repository.after_create_branch
+ project.repository.after_create_branch(expire_cache: false)
project.after_create_default_branch if default_branch?
end
@@ -78,7 +78,7 @@ module Git
end
def branch_remove_hooks
- project.repository.after_remove_branch
+ project.repository.after_remove_branch(expire_cache: false)
end
# Schedules processing of commit messages
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 73e1e00dc33..116756bacfe 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -46,6 +46,11 @@ module Groups
params.delete(:parent_id)
end
+ # overridden in EE
+ def remove_unallowed_params
+ params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, group)
+ end
+
def valid_share_with_group_lock_change?
return true unless changing_share_with_group_lock?
return true if can?(current_user, :change_share_with_group_lock, group)
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 8d3b9b05819..27c16ba1777 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -15,7 +15,8 @@ module MergeRequests
end
def rebase
- if merge_request.gitaly_rebase_in_progress?
+ # Ensure Gitaly isn't already running a rebase
+ if source_project.repository.rebase_in_progress?(merge_request.id)
log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
return false
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index a09272f0d83..248e81080cc 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -53,7 +53,7 @@ module Notes
# We must add the error after we call #save because errors are reset
# when #save is called
if only_commands
- note.errors.add(:commands_only, message.presence || _('Commands did not apply'))
+ note.errors.add(:commands_only, message.presence || _('Failed to apply commands.'))
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 21fab22e0d4..83710ffce2f 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -321,6 +321,9 @@ class NotificationService
end
def decline_project_invite(project_member)
+ # Must always send, regardless of project/namespace configuration since it's a
+ # response to the user's action.
+
mailer.member_invite_declined_email(
project_member.real_source_type,
project_member.project.id,
@@ -351,8 +354,8 @@ class NotificationService
end
def decline_group_invite(group_member)
- # always send this one, since it's a response to the user's own
- # action
+ # Must always send, regardless of project/namespace configuration since it's a
+ # response to the user's action.
mailer.member_invite_declined_email(
group_member.real_source_type,
@@ -410,6 +413,10 @@ class NotificationService
end
def pipeline_finished(pipeline, recipients = nil)
+ # Must always check project configuration since recipients could be a list of emails
+ # from the PipelinesEmailService integration.
+ return if pipeline.project.emails_disabled?
+
email_template = "pipeline_#{pipeline.status}_email"
return unless mailer.respond_to?(email_template)
@@ -428,6 +435,8 @@ class NotificationService
end
def autodevops_disabled(pipeline, recipients)
+ return if pipeline.project.emails_disabled?
+
recipients.each do |recipient|
mailer.autodevops_disabled_email(pipeline, recipient).deliver_later
end
@@ -472,10 +481,14 @@ class NotificationService
end
def repository_cleanup_success(project, user)
+ return if project.emails_disabled?
+
mailer.send(:repository_cleanup_success_email, project, user).deliver_later
end
def repository_cleanup_failure(project, user, error)
+ return if project.emails_disabled?
+
mailer.send(:repository_cleanup_failure_email, project, user, error).deliver_later
end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 1244a0f72a7..13a467a3ef9 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -2,31 +2,52 @@
module Projects
class UpdateRemoteMirrorService < BaseService
- attr_reader :errors
+ MAX_TRIES = 3
- def execute(remote_mirror)
+ def execute(remote_mirror, tries)
return success unless remote_mirror.enabled?
- errors = []
+ update_mirror(remote_mirror)
- begin
- remote_mirror.ensure_remote!
- repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true)
+ success
+ rescue Gitlab::Git::CommandError => e
+ # This happens if one of the gitaly calls above fail, for example when
+ # branches have diverged, or the pre-receive hook fails.
+ retry_or_fail(remote_mirror, e.message, tries)
- opts = {}
- if remote_mirror.only_protected_branches?
- opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
- end
+ error(e.message)
+ rescue => e
+ remote_mirror.mark_as_failed!(e.message)
+ raise e
+ end
+
+ private
+
+ def update_mirror(remote_mirror)
+ remote_mirror.update_start!
+
+ remote_mirror.ensure_remote!
+ repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true)
- remote_mirror.update_repository(opts)
- rescue => e
- errors << e.message.strip
+ opts = {}
+ if remote_mirror.only_protected_branches?
+ opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
end
- if errors.present?
- error(errors.join("\n\n"))
+ remote_mirror.update_repository(opts)
+
+ remote_mirror.update_finish!
+ end
+
+ def retry_or_fail(mirror, message, tries)
+ if tries < MAX_TRIES
+ mirror.mark_for_retry!(message)
else
- success
+ # It's not likely we'll be able to recover from this ourselves, so we'll
+ # notify the users of the problem, and don't trigger any sidekiq retries
+ # Instead, we'll wait for the next change to try the push again, or until
+ # a user manually retries.
+ mirror.mark_as_failed!(message)
end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index caab946174d..8acbdc7e02b 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -9,6 +9,7 @@ module Projects
# rubocop: disable CodeReuse/ActiveRecord
def execute
+ remove_unallowed_params
validate!
ensure_wiki_exists if enabling_wiki?
@@ -54,6 +55,10 @@ module Projects
end
end
+ def remove_unallowed_params
+ params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, project)
+ end
+
def after_update
todos_features_changes = %w(
issues_access_level
diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb
index 49a7d0178f4..dcafebae52d 100644
--- a/app/services/update_deployment_service.rb
+++ b/app/services/update_deployment_service.rb
@@ -42,7 +42,7 @@ class UpdateDeploymentService
return unless environment_url
@expanded_environment_url =
- ExpandVariables.expand(environment_url, variables)
+ ExpandVariables.expand(environment_url, -> { variables })
end
def environment_url
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
new file mode 100644
index 00000000000..b60b5d55a1b
--- /dev/null
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -0,0 +1,30 @@
+- expanded = true if !@application_setting.valid? && @application_setting.errors.any? { |k| k.to_s.start_with?('snowplow_') }
+%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Snowplow')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') }
+ .settings-content
+
+ = form_for @application_setting, url: integrations_admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :snowplow_enabled, class: 'form-check-input'
+ = f.label :snowplow_enabled, _('Enable snowplow tracking'), class: 'form-check-label'
+ .form-group
+ = f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light'
+ = f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com'
+ .form-group
+ = f.label :snowplow_site_id, _('Site ID'), class: 'label-light'
+ = f.text_field :snowplow_site_id, class: 'form-control'
+ .form-group
+ = f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light'
+ = f.text_field :snowplow_cookie_domain, class: 'form-control'
+
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 2f10f08c839..0b1d3d1ddb3 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,20 +1,23 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group
- = f.label "Username or email", for: "user_login", class: 'label-bold'
- = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' }
+ = f.label _('Username or email'), for: 'user_login', class: 'label-bold'
+ = f.text_field :login, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' }
.form-group
= f.label :password, class: 'label-bold'
- = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' }
+ = f.password_field :password, class: 'form-control bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
- if devise_mapping.rememberable?
.remember-me
- %label{ for: "user_remember_me" }
+ %label{ for: 'user_remember_me' }
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
- .float-right.forgot-password
- = link_to "Forgot your password?", new_password_path(:user)
+ .float-right
+ - if unconfirmed_email?
+ = link_to _('Resend confirmation email'), new_user_confirmation_path
+ - else
+ = link_to _('Forgot your password?'), new_password_path(:user)
%div
- if captcha_enabled?
= recaptcha_tags
.submit-container.move-submit-down
- = f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' }
+ = f.submit _('Sign in'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 074edf645ba..2cd77af6877 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -5,6 +5,8 @@
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= render "devise/shared/error_messages", resource: resource
+ - if Feature.enabled?(:invisible_captcha)
+ = invisible_captcha
.name.form-group
= f.label :name, _('Full name'), class: 'label-bold'
= f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.")
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 0624f66b3a5..a996c86a256 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -3,9 +3,9 @@
.modal-content
.modal-header
%h4.modal-title
- Keyboard Shortcuts
+ = _('Keyboard Shortcuts')
%small
- = link_to '(Show all)', '#', class: 'js-more-help-button'
+ = link_to _('(Show all)'), '#', class: 'js-more-help-button'
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
@@ -15,281 +15,281 @@
%tbody
%tr
%th
- %th Global Shortcuts
+ %th= _('Global Shortcuts')
%tr
%td.shortcut
%kbd s
- %td Focus Search
+ %td= _('Focus Search')
%tr
%td.shortcut
%kbd f
- %td Focus Filter
+ %td= _('Focus Filter')
%tr
%td.shortcut
%kbd p
%kbd b
- %td Toggle the Performance Bar
+ %td= _('Toggle the Performance Bar')
%tr
%td.shortcut
%kbd ?
- %td Show/hide this dialog
+ %td= _('Show/hide this dialog')
%tr
%td.shortcut
- if browser.platform.mac?
%kbd &#8984; shift p
- else
%kbd ctrl shift p
- %td Toggle Markdown preview
+ %td= _('Toggle Markdown preview')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-up
- %td Edit last comment (when focused on an empty textarea)
+ %td= _('Edit last comment (when focused on an empty textarea)')
%tr
%td.shortcut
%kbd shift t
%td
- Go to todos
+ = _('Go to todos')
%tr
%td.shortcut
%kbd shift a
%td
- Go to the activity feed
+ = _('Go to the activity feed')
%tr
%td.shortcut
%kbd shift p
%td
- Go to projects
+ = _('Go to projects')
%tr
%td.shortcut
%kbd shift i
%td
- Go to issues
+ = _('Go to issues')
%tr
%td.shortcut
%kbd shift m
%td
- Go to merge requests
+ = _('Go to merge requests')
%tr
%td.shortcut
%kbd shift g
%td
- Go to groups
+ = _('Go to groups')
%tr
%td.shortcut
%kbd shift l
%td
- Go to milestones
+ = _('Go to milestones')
%tr
%td.shortcut
%kbd shift s
%td
- Go to snippets
+ = _('Go to snippets')
%tbody
%tr
%th
- %th Finding Project File
+ %th= _('Finding Project File')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-up
- %td Move selection up
+ %td= _('Move selection up')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-down
- %td Move selection down
+ %td= _('Move selection down')
%tr
%td.shortcut
%kbd enter
- %td Open Selection
+ %td= _('Open Selection')
%tr
%td.shortcut
%kbd esc
- %td Go back
+ %td= _('Go back')
.col-lg-4
%table.shortcut-mappings
%tbody
%tr
%th
- %th Project
+ %th= _('Project')
%tr
%td.shortcut
%kbd g
%kbd p
%td
- Go to the project's overview page
+ = _('Go to the project\'s overview page')
%tr
%td.shortcut
%kbd g
%kbd v
%td
- Go to the project's activity feed
+ = _('Go to the project\'s activity feed')
%tr
%td.shortcut
%kbd g
%kbd f
%td
- Go to files
+ = _('Go to files')
%tr
%td.shortcut
%kbd g
%kbd c
%td
- Go to commits
+ = _('Go to commits')
%tr
%td.shortcut
%kbd g
%kbd j
%td
- Go to jobs
+ = _('Go to jobs')
%tr
%td.shortcut
%kbd g
%kbd n
%td
- Go to network graph
+ = _('Go to network graph')
%tr
%td.shortcut
%kbd g
%kbd d
%td
- Go to repository charts
+ = _('Go to repository charts')
%tr
%td.shortcut
%kbd g
%kbd i
%td
- Go to issues
+ = _('Go to issues')
%tr
%td.shortcut
%kbd g
%kbd b
%td
- Go to issue boards
+ = _('Go to issue boards')
%tr
%td.shortcut
%kbd g
%kbd m
%td
- Go to merge requests
+ = _('Go to merge requests')
%tr
%td.shortcut
%kbd g
%kbd e
%td
- Go to environments
+ = _('Go to environments')
%tr
%td.shortcut
%kbd g
%kbd l
%td
- Go to metrics
+ = _('Go to metrics')
%tr
%td.shortcut
%kbd g
%kbd k
%td
- Go to kubernetes
+ = _('Go to kubernetes')
%tr
%td.shortcut
%kbd g
%kbd s
%td
- Go to snippets
+ = _('Go to snippets')
%tr
%td.shortcut
%kbd g
%kbd w
%td
- Go to wiki
+ = _('Go to wiki')
%tr
%td.shortcut
%kbd t
- %td Go to finding file
+ %td= _('Go to finding file')
%tr
%td.shortcut
%kbd i
- %td New issue
+ %td= _('New issue')
%tbody
%tr
%th
- %th Project Files browsing
+ %th= _('Project Files browsing')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-up
- %td Move selection up
+ %td= _('Move selection up')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-down
- %td Move selection down
+ %td= _('Move selection down')
%tr
%td.shortcut
%kbd enter
- %td Open Selection
+ %td= _('Open Selection')
%tbody
%tr
%th
- %th Project File
+ %th= _('Project File')
%tr
%td.shortcut
%kbd y
- %td Go to file permalink
+ %td= _('Go to file permalink')
%tbody
%tr
%th
- %th Web IDE
+ %th= _('Web IDE')
%tr
%td.shortcut
- if browser.platform.mac?
%kbd &#8984; p
- else
%kbd ctrl p
- %td Go to file
+ %td= _('Go to file')
%tr
%td.shortcut
- if browser.platform.mac?
%kbd &#8984; enter
- else
%kbd ctrl enter
- %td Commit (when editing commit message)
+ %td= _('Commit (when editing commit message)')
.col-lg-4
%table.shortcut-mappings
- %tbody.hidden-shortcut{ style: 'display:none' }
+ %tbody.hidden-shortcut.network{ style: 'display:none' }
%tr
%th
- %th Network Graph
+ %th= _('Network Graph')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-left
\/
%kbd h
- %td Scroll left
+ %td= _('Scroll left')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-right
\/
%kbd l
- %td Scroll right
+ %td= _('Scroll right')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-up
\/
%kbd k
- %td Scroll up
+ %td= _('Scroll up')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-down
\/
%kbd j
- %td Scroll down
+ %td= _('Scroll down')
%tr
%td.shortcut
%kbd
@@ -298,7 +298,7 @@
\/
%kbd
shift k
- %td Scroll to top
+ %td= _('Scroll to top')
%tr
%td.shortcut
%kbd
@@ -307,80 +307,80 @@
\/
%kbd
shift j
- %td Scroll to bottom
- %tbody.hidden-shortcut{ style: 'display:none' }
+ %td= _('Scroll to bottom')
+ %tbody.hidden-shortcut.issues{ style: 'display:none' }
%tr
%th
- %th Issues
+ %th= _('Issues')
%tr
%td.shortcut
%kbd a
- %td Change assignee
+ %td= _('Change assignee')
%tr
%td.shortcut
%kbd m
- %td Change milestone
+ %td= _('Change milestone')
%tr
%td.shortcut
%kbd r
- %td Reply (quoting selected text)
+ %td= _('Reply (quoting selected text)')
%tr
%td.shortcut
%kbd e
- %td Edit issue
+ %td= _('Edit issue')
%tr
%td.shortcut
%kbd l
- %td Change Label
+ %td= _('Change Label')
%tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
%tr
%th
- %th Merge Requests
+ %th= _('Merge Requests')
%tr
%td.shortcut
%kbd a
- %td Change assignee
+ %td= _('Change assignee')
%tr
%td.shortcut
%kbd m
- %td Change milestone
+ %td= _('Change milestone')
%tr
%td.shortcut
%kbd r
- %td Reply (quoting selected text)
+ %td= _('Reply (quoting selected text)')
%tr
%td.shortcut
%kbd e
- %td Edit merge request
+ %td= _('Edit merge request')
%tr
%td.shortcut
%kbd l
- %td Change Label
+ %td= _('Change Label')
%tr
%td.shortcut
%kbd ]
\/
%kbd j
- %td Move to next file
+ %td= _('Move to next file')
%tr
%td.shortcut
%kbd [
\/
%kbd k
- %td Move to previous file
+ %td= _('Move to previous file')
%tr
%td.shortcut
%kbd n
- %td= _("Move to next unresolved discussion")
+ %td= _('Move to next unresolved discussion')
%tr
%td.shortcut
%kbd p
- %td= _("Move to previous unresolved discussion")
- %tbody.hidden-shortcut{ style: 'display:none' }
+ %td= _('Move to previous unresolved discussion')
+ %tbody.hidden-shortcut.wiki{ style: 'display:none' }
%tr
%th
- %th Wiki pages
+ %th= _('Wiki pages')
%tr
%td.shortcut
%kbd e
- %td Edit wiki page
+ %td= _('Edit wiki page')
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index bc900992cb0..e6a235e39da 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -47,5 +47,6 @@
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
= hidden_field_tag :nav_source, 'navbar'
+ -# workaround for non-JS feature specs, for JS you need to use find('#search').send_keys(:enter)
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml
new file mode 100644
index 00000000000..5f5c5e984c5
--- /dev/null
+++ b/app/views/layouts/_snowplow.html.haml
@@ -0,0 +1,29 @@
+- return unless Gitlab::CurrentSettings.snowplow_enabled?
+
+= javascript_tag nonce: true do
+ :plain
+ ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[];
+ p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments)
+ };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1;
+ n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow"));
+
+ window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_hostname}', {
+ appId: '#{Gitlab::CurrentSettings.snowplow_site_id}',
+ cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}',
+ userFingerprint: false,
+ respectDoNotTrack: true,
+ forceSecureTracker: true,
+ post: true,
+ contexts: { webPage: true },
+ stateStorageStrategy: "localStorage"
+ });
+
+ window.snowplow('enableActivityTracking', 30, 30);
+ window.snowplow('trackPageView');
+
+- return unless Feature.enabled?(:additional_snowplow_tracking, @group)
+
+= javascript_tag nonce: true do
+ :plain
+ window.snowplow('enableFormTracking');
+ window.snowplow('enableLinkClickTracking');
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index d16e2dddbe0..d99063e344f 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -45,20 +45,20 @@
.form-group
= f.label :layout, class: 'label-bold' do
= s_('Preferences|Layout width')
- = f.select :layout, layout_choices, {}, class: 'form-control'
+ = f.select :layout, layout_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout.')
.form-group
= f.label :dashboard, class: 'label-bold' do
= s_('Preferences|Default dashboard')
- = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+ = f.select :dashboard, dashboard_choices, {}, class: 'select2'
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
.form-group
= f.label :project_view, class: 'label-bold' do
= s_('Preferences|Project overview content')
- = f.select :project_view, project_view_choices, {}, class: 'form-control'
+ = f.select :project_view, project_view_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on a project’s overview page.')
@@ -82,7 +82,7 @@
.form-group
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
- = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control'
+ = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2'
- if Feature.enabled?(:user_time_settings)
.col-sm-12
%hr
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 4759991449e..de0c21e7cf6 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -45,7 +45,7 @@
%ul.controls
- if issue.closed?
%li.issuable-status
- CLOSED
+ = _('CLOSED')
- if issue.assignees.any?
%li
= render 'shared/issuable/assignees', project: @project, issuable: issue
@@ -53,4 +53,5 @@
= render 'shared/issuable_meta_data', issuable: issue
.float-right.issuable-updated-at.d-none.d-sm-inline-block
- %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
+ %span
+ = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago') }
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index dee3931ff04..36f19ee6175 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -41,11 +41,11 @@
%ul.controls.d-flex.align-items-end
- if merge_request.merged?
%li.issuable-status.d-none.d-sm-inline-block
- MERGED
+ = _('MERGED')
- elsif merge_request.closed?
%li.issuable-status.d-none.d-sm-inline-block
= icon('ban')
- CLOSED
+ = _('CLOSED')
- if can?(current_user, :read_pipeline, merge_request.head_pipeline)
%li.issuable-pipeline-status.d-none.d-sm-flex
= render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), option_css_classes: 'd-flex'
@@ -61,4 +61,5 @@
= render 'shared/issuable_meta_data', issuable: merge_request
.float-right.issuable-updated-at.d-none.d-sm-inline-block
- %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
+ %span
+ = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago') }
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 280ec6d715b..eb100e5cf47 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -43,7 +43,8 @@
= _('Mirrored repositories')
= render_if_exists 'projects/mirrors/mirrored_repositories_count'
%th= _('Direction')
- %th= _('Last update')
+ %th= _('Last update attempt')
+ %th= _('Last successful update')
%th
%th
%tbody.js-mirrors-table-body
@@ -53,6 +54,8 @@
%tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) }
%td.qa-mirror-repository-url= mirror.safe_url
%td= _('Push')
+ %td
+ = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')
%td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td
- if mirror.disabled?
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index efabb7f7b19..149b0d6cddd 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -2,7 +2,7 @@
.col-sm-12
= form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f|
.form-group
- = label_tag :user_ids, _("Select members to invite"), class: "label-bold"
+ = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold"
= users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
.form-group
= label_tag :access_level, _("Choose a role permission"), class: "label-bold"
diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml
index 70f1bf8ef46..c5eecc900b2 100644
--- a/app/views/projects/update.js.haml
+++ b/app/views/projects/update.js.haml
@@ -4,7 +4,7 @@
location.reload();
- else
:plain
- $(".project-edit-errors").html("#{escape_javascript(render('errors'))}");
+ $(".flash-container").html("#{escape_javascript(render('errors'))}");
$('.save-project-loader').hide();
$('.project-edit-container').show();
$('.edit-project .js-btn-success-general-project-settings').enable();
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 66a614b0197..858731b2dda 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -14,7 +14,7 @@
.col-sm-12
= f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- if @page.persisted?
- %span.edit-wiki-page-slug-tip
+ %span.d-inline-block.mw-100.prepend-top-5
= icon('lightbulb-o')
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index dc12e368b35..2c675c0de9c 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -11,7 +11,7 @@
= label_tag :new_wiki_path do
%span= s_("WikiPage|Page slug")
= text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true
- %span.new-wiki-page-slug-tip
+ %span.d-inline-block.mw-100.prepend-top-5
= icon('lightbulb-o')
= s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
.form-actions
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 1277ea6c743..e8b59a3b8c4 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -5,7 +5,7 @@
= wiki_page_errors(@error)
-.wiki-page-header.has-sidebar-toggle
+.wiki-page-header.top-area.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 8c2cbd495a0..009133be117 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
- page_title s_("WikiClone|Git Access"), _("Wiki")
-.wiki-page-header.has-sidebar-toggle
+.wiki-page-header.top-area.has-sidebar-toggle
%button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index c5fbeeafa54..f8468ef9a78 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,6 +1,6 @@
- page_title _("History"), @page.human_title, _("Wiki")
-.wiki-page-header.has-sidebar-toggle
+.wiki-page-header.top-area.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 2191e5ab287..f7999c3f1bd 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -5,7 +5,7 @@
- sort_title = wiki_sort_title(params[:sort])
%div{ class: container_class }
- .wiki-page-header
+ .wiki-page-header.top-area
.nav-text.flex-fill
%h2.wiki-page-title
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 95cd3356ec8..1d649886331 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -4,7 +4,7 @@
- page_title @page.human_title, _("Wiki")
- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home)
-.wiki-page-header.has-sidebar-toggle
+.wiki-page-header.top-area.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 18613ff4c16..eae2a491ceb 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,10 +1,6 @@
- users = capture_haml do
- if search_tabs?(:members)
- %li{ class: active_when(@scope == 'users') }
- = link_to search_filter_path(scope: 'users') do
- Users
- %span.badge.badge-pill
- = limited_count(@search_results.limited_users_count)
+ = search_filter_link 'users', _("Users")
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
@@ -12,80 +8,28 @@
%ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs
- if @project
- if project_search_tabs?(:blobs)
- %li{ class: active_when(@scope == 'blobs'), data: { qa_selector: 'code_tab' } }
- = link_to search_filter_path(scope: 'blobs') do
- = _("Code")
- %span.badge.badge-pill
- = @search_results.blobs_count
+ = search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
- if project_search_tabs?(:issues)
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- = _("Issues")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_issues_count)
+ = search_filter_link 'issues', _("Issues")
- if project_search_tabs?(:merge_requests)
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- = _("Merge requests")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_merge_requests_count)
+ = search_filter_link 'merge_requests', _("Merge requests")
- if project_search_tabs?(:milestones)
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- = _("Milestones")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_milestones_count)
+ = search_filter_link 'milestones', _("Milestones")
- if project_search_tabs?(:notes)
- %li{ class: active_when(@scope == 'notes') }
- = link_to search_filter_path(scope: 'notes') do
- = _("Comments")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_notes_count)
+ = search_filter_link 'notes', _("Comments")
- if project_search_tabs?(:wiki)
- %li{ class: active_when(@scope == 'wiki_blobs') }
- = link_to search_filter_path(scope: 'wiki_blobs') do
- = _("Wiki")
- %span.badge.badge-pill
- = @search_results.wiki_blobs_count
+ = search_filter_link 'wiki_blobs', _("Wiki")
- if project_search_tabs?(:commits)
- %li{ class: active_when(@scope == 'commits') }
- = link_to search_filter_path(scope: 'commits') do
- = _("Commits")
- %span.badge.badge-pill
- = @search_results.commits_count
+ = search_filter_link 'commits', _("Commits")
= users
- elsif @show_snippets
- %li{ class: active_when(@scope == 'snippet_blobs') }
- = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
- = _("Snippet Contents")
- %span.badge.badge-pill
- = @search_results.snippet_blobs_count
- %li{ class: active_when(@scope == 'snippet_titles') }
- = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
- = _("Titles and Filenames")
- %span.badge.badge-pill
- = @search_results.snippet_titles_count
+ = search_filter_link 'snippet_blobs', _("Snippet Contents"), search: { snippets: true, group_id: nil, project_id: nil }
+ = search_filter_link 'snippet_titles', _("Titles and Filenames"), search: { snippets: true, group_id: nil, project_id: nil }
- else
- %li{ class: active_when(@scope == 'projects') }
- = link_to search_filter_path(scope: 'projects') do
- = _("Projects")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_projects_count)
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- = _("Issues")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_issues_count)
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- = _("Merge requests")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_merge_requests_count)
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- = _("Milestones")
- %span.badge.badge-pill
- = limited_count(@search_results.limited_milestones_count)
+ = search_filter_link 'projects', _("Projects")
+ = search_filter_link 'issues', _("Issues")
+ = search_filter_link 'merge_requests', _("Merge requests")
+ = search_filter_link 'milestones', _("Milestones")
= render_if_exists 'search/category_elasticsearch'
= users
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 400becdd023..991a177018e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -88,7 +88,6 @@
- pipeline_processing:ci_build_prepare
- pipeline_processing:build_queue
- pipeline_processing:build_success
-- pipeline_processing:build_process
- pipeline_processing:pipeline_process
- pipeline_processing:pipeline_success
- pipeline_processing:pipeline_update
diff --git a/app/workers/build_process_worker.rb b/app/workers/build_process_worker.rb
deleted file mode 100644
index 9cd9519df1f..00000000000
--- a/app/workers/build_process_worker.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-class BuildProcessWorker
- include ApplicationWorker
- include PipelineQueue
-
- queue_namespace :pipeline_processing
-
- # rubocop: disable CodeReuse/ActiveRecord
- def perform(build_id)
- CommitStatus.find_by(id: build_id).try do |build|
- build.pipeline.process!([build_id])
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index dba7837bd12..622bd6f1f48 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -42,6 +42,11 @@ class PostReceive
user = identify_user(post_received)
return false unless user
+ # Expire the branches cache so we have updated data for this push
+ post_received.project.repository.expire_branches_cache if post_received.includes_branches?
+ # We only need to expire tags once per push
+ post_received.project.repository.expire_caches_for_tags if post_received.includes_tags?
+
post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index|
service_klass =
if Gitlab::Git.tag_ref?(ref)
@@ -72,6 +77,7 @@ class PostReceive
def after_project_changes_hooks(post_received, user, refs, changes)
hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs)
SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks)
+ Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
end
def process_wiki_changes(post_received)
diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb
index 5bafe8e2046..368abfeda99 100644
--- a/app/workers/remote_mirror_notification_worker.rb
+++ b/app/workers/remote_mirror_notification_worker.rb
@@ -4,7 +4,7 @@ class RemoteMirrorNotificationWorker
include ApplicationWorker
def perform(remote_mirror_id)
- remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
+ remote_mirror = RemoteMirror.find_by_id(remote_mirror_id)
# We check again if there's an error because a newer run since this job was
# fired could've completed successfully.
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index 03a7ff2cd7a..d13c7641eb3 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -1,50 +1,53 @@
# frozen_string_literal: true
class RepositoryUpdateRemoteMirrorWorker
- UpdateAlreadyInProgressError = Class.new(StandardError)
UpdateError = Class.new(StandardError)
include ApplicationWorker
+ include Gitlab::ExclusiveLeaseHelpers
sidekiq_options retry: 3, dead: false
- sidekiq_retry_in { |count| 30 * count }
+ LOCK_WAIT_TIME = 30.seconds
+ MAX_TRIES = 3
- sidekiq_retries_exhausted do |msg, _|
- Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
- end
-
- def perform(remote_mirror_id, scheduled_time)
- remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute
+ def perform(remote_mirror_id, scheduled_time, tries = 0)
+ remote_mirror = RemoteMirror.find_by_id(remote_mirror_id)
+ return unless remote_mirror
return if remote_mirror.updated_since?(scheduled_time)
- raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress?
+ # If the update is already running, wait for it to finish before running again
+ # This will wait for a total of 90 seconds in 3 steps
+ in_lock(remote_mirror_update_lock(remote_mirror.id),
+ retries: 3,
+ ttl: remote_mirror.max_runtime,
+ sleep_sec: LOCK_WAIT_TIME) do
+ update_mirror(remote_mirror, scheduled_time, tries)
+ end
+ rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ # If an update runs longer than 1.5 minutes, we'll reschedule it
+ # with a backoff. The next run will check if the previous update would
+ # include the changes that triggered this update and become a no-op.
+ self.class.perform_in(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, tries)
+ end
- remote_mirror.update_start
+ private
- project = remote_mirror.project
+ def update_mirror(mirror, scheduled_time, tries)
+ project = mirror.project
current_user = project.creator
- result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
- raise UpdateError, result[:message] if result[:status] == :error
-
- remote_mirror.update_finish
- rescue UpdateAlreadyInProgressError
- raise
- rescue UpdateError => ex
- fail_remote_mirror(remote_mirror, ex.message)
- raise
- rescue => ex
- return unless remote_mirror
+ result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(mirror, tries)
- fail_remote_mirror(remote_mirror, ex.message)
- raise UpdateError, "#{ex.class}: #{ex.message}"
+ if result[:status] == :error && mirror.to_retry?
+ schedule_retry(mirror, scheduled_time, tries)
+ end
end
- private
-
- def fail_remote_mirror(remote_mirror, message)
- remote_mirror.mark_as_failed(message)
+ def remote_mirror_update_lock(mirror_id)
+ [self.class.name, mirror_id].join(':')
+ end
- Rails.logger.error(message) # rubocop:disable Gitlab/RailsLogger
+ def schedule_retry(mirror, scheduled_time, tries)
+ self.class.perform_in(mirror.backoff_delay, mirror.id, scheduled_time, tries + 1)
end
end
diff --git a/bin/changelog b/bin/changelog
index 328d9495b96..ec068d06507 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -23,7 +23,7 @@ module ChangelogHelpers
Abort = Class.new(StandardError)
Done = Class.new(StandardError)
- MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters
+ MAX_FILENAME_LENGTH = 99 # GNU tar has a 99 character limit
def capture_stdout(cmd)
output = IO.popen(cmd, &:read)
diff --git a/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml
new file mode 100644
index 00000000000..f249eff572c
--- /dev/null
+++ b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml
@@ -0,0 +1,5 @@
+---
+title: Adjust copy for adding additional members
+merge_request: 31726
+author:
+type: changed
diff --git a/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml
new file mode 100644
index 00000000000..ccfd929b6ba
--- /dev/null
+++ b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml
@@ -0,0 +1,5 @@
+---
+title: Track page views for cycle analytics show page
+merge_request: 31717
+author:
+type: added
diff --git a/changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml b/changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml
new file mode 100644
index 00000000000..5254bd36b9c
--- /dev/null
+++ b/changelogs/unreleased/24705-multi-selection-for-delete-on-registry-page.yml
@@ -0,0 +1,5 @@
+---
+title: Added multi-select deletion of container registry images
+merge_request: 30837
+author:
+type: other
diff --git a/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml
new file mode 100644
index 00000000000..f0cc7fe9b6d
--- /dev/null
+++ b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml
@@ -0,0 +1,6 @@
+---
+title: Updated the personal access token api scope description to reflect the permissions
+ it grants
+merge_request: 31759
+author:
+type: other
diff --git a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml
index 674d53286e6..674d53286e6 100644
--- a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml
+++ b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml
diff --git a/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml b/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml
new file mode 100644
index 00000000000..9137e9339aa
--- /dev/null
+++ b/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml
@@ -0,0 +1,5 @@
+---
+title: Allow email notifications to be disabled for all members of a group or project
+merge_request: 30755
+author: Dustin Spicuzza
+type: added
diff --git a/changelogs/unreleased/59829-fix-style-lint-wiki.yml b/changelogs/unreleased/59829-fix-style-lint-wiki.yml
new file mode 100644
index 00000000000..48242a77c6b
--- /dev/null
+++ b/changelogs/unreleased/59829-fix-style-lint-wiki.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the style-lint errors and warnings for `app/assets/stylesheets/pages/wiki.scss`
+merge_request: 31656
+author:
+type: other
diff --git a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml
index ec6e9c5aff8..ec6e9c5aff8 100644
--- a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml
+++ b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml
diff --git a/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml
new file mode 100644
index 00000000000..10f2b7eaed5
--- /dev/null
+++ b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml
@@ -0,0 +1,5 @@
+---
+title: Harmonize selections in user settings
+merge_request: 31110
+author: Marc Schwede
+type: other
diff --git a/changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml b/changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml
new file mode 100644
index 00000000000..741763403a5
--- /dev/null
+++ b/changelogs/unreleased/63942-remove-config-action_dispatch-use_authenticated_cookie_encryption-configuration.yml
@@ -0,0 +1,5 @@
+---
+title: Enable authenticated cookie encryption
+merge_request: 31463
+author:
+type: other
diff --git a/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml
new file mode 100644
index 00000000000..21771c76873
--- /dev/null
+++ b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml
@@ -0,0 +1,5 @@
+---
+title: 'feat: adds a download to csv functionality to the dropdown in prometheus metrics'
+merge_request: 31679
+author:
+type: changed
diff --git a/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml b/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml
new file mode 100644
index 00000000000..fb9d6fa251d
--- /dev/null
+++ b/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml
@@ -0,0 +1,5 @@
+---
+title: Fix active metric files being wiped after the app starts
+merge_request: 31668
+author:
+type: fixed
diff --git a/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml
new file mode 100644
index 00000000000..a5f62dbcd56
--- /dev/null
+++ b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml
@@ -0,0 +1,5 @@
+---
+title: Allow users to resend a confirmation link when the grace period has expired
+merge_request: 31476
+author:
+type: changed
diff --git a/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml b/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml
new file mode 100644
index 00000000000..fc29a514c42
--- /dev/null
+++ b/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml
@@ -0,0 +1,6 @@
+---
+title: Fixed display of some sections and externalized all text in the shortcuts modal
+ overlay
+merge_request: 31594
+author:
+type: fixed
diff --git a/changelogs/unreleased/65790-highlight.yml b/changelogs/unreleased/65790-highlight.yml
new file mode 100644
index 00000000000..2531a3730ed
--- /dev/null
+++ b/changelogs/unreleased/65790-highlight.yml
@@ -0,0 +1,5 @@
+---
+title: Adds highlight to the collapsible section
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml
new file mode 100644
index 00000000000..217db8aa05a
--- /dev/null
+++ b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml
@@ -0,0 +1,5 @@
+---
+title: Invalidate branches cache on PostReceive
+merge_request: 31653
+author:
+type: fixed
diff --git a/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml
new file mode 100644
index 00000000000..df0ac649ac1
--- /dev/null
+++ b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project avatar image in Slack pipeline notifications
+merge_request: 31788
+author:
+type: fixed
diff --git a/changelogs/unreleased/bump_helm_kubectl_gitlab.yml b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml
new file mode 100644
index 00000000000..d768462e130
--- /dev/null
+++ b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Helm to 2.14.3 and kubectl to 1.11.10 for Kubernetes integration
+merge_request: 31716
+author:
+type: other
diff --git a/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml
new file mode 100644
index 00000000000..962376086b0
--- /dev/null
+++ b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml
@@ -0,0 +1,6 @@
+---
+title: Retry push mirrors faster when running concurrently, improve error handling
+ when push mirrors fail
+merge_request: 31247
+author:
+type: changed
diff --git a/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml b/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml
new file mode 100644
index 00000000000..615a1571e95
--- /dev/null
+++ b/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml
@@ -0,0 +1,5 @@
+---
+title: Allow CI to clone public projects when HTTP protocol is disabled
+merge_request: 31632
+author:
+type: fixed
diff --git a/changelogs/unreleased/enable-specific-embeds.yml b/changelogs/unreleased/enable-specific-embeds.yml
new file mode 100644
index 00000000000..f2e591621a8
--- /dev/null
+++ b/changelogs/unreleased/enable-specific-embeds.yml
@@ -0,0 +1,5 @@
+---
+title: Enable embedding of specific metrics charts in GFM
+merge_request: 31304
+author:
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml b/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml
new file mode 100644
index 00000000000..e28dbd6f0c4
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml
@@ -0,0 +1,6 @@
+---
+title: Fix empty error flash message on profile:account page when updating username
+ with username that has already been taken
+merge_request: 31809
+author:
+type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml b/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml
new file mode 100644
index 00000000000..451aac9c2e3
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix missing author line (`Created by: <user>`) in MRs/issues/comments of imported Bitbucket Cloud project'
+merge_request: 31579
+author:
+type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.59.0.yml b/changelogs/unreleased/gitaly-version-v1.59.0.yml
new file mode 100644
index 00000000000..d103f6b248e
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.59.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.59.0
+merge_request: 31743
+author:
+type: changed
diff --git a/changelogs/unreleased/id-source-code-smau.yml b/changelogs/unreleased/id-source-code-smau.yml
new file mode 100644
index 00000000000..6ba5068544e
--- /dev/null
+++ b/changelogs/unreleased/id-source-code-smau.yml
@@ -0,0 +1,5 @@
+---
+title: Add usage pings for source code pushes
+merge_request: 31734
+author:
+type: added
diff --git a/changelogs/unreleased/improve-quick-action-messages.yml b/changelogs/unreleased/improve-quick-action-messages.yml
new file mode 100644
index 00000000000..86f8c251860
--- /dev/null
+++ b/changelogs/unreleased/improve-quick-action-messages.yml
@@ -0,0 +1,5 @@
+---
+title: Improve quick action error messages
+merge_request: 31451
+author:
+type: other
diff --git a/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml
new file mode 100644
index 00000000000..606c60721b8
--- /dev/null
+++ b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml
@@ -0,0 +1,5 @@
+---
+title: error message for general settings
+merge_request: 31636
+author: Mesut Güneş
+type: fixed
diff --git a/changelogs/unreleased/load-search-counts-async.yml b/changelogs/unreleased/load-search-counts-async.yml
new file mode 100644
index 00000000000..1f466450e76
--- /dev/null
+++ b/changelogs/unreleased/load-search-counts-async.yml
@@ -0,0 +1,5 @@
+---
+title: Load search result counts asynchronously
+merge_request: 31663
+author:
+type: changed
diff --git a/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml b/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml
new file mode 100644
index 00000000000..d56a07fe569
--- /dev/null
+++ b/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Create database tables for the new cycle analytics backend
+merge_request: 31621
+author:
+type: other
diff --git a/changelogs/unreleased/post-migrate-private-profile.yml b/changelogs/unreleased/post-migrate-private-profile.yml
new file mode 100644
index 00000000000..53a55661aa0
--- /dev/null
+++ b/changelogs/unreleased/post-migrate-private-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate remaining users with null private_profile
+merge_request: 31708
+author:
+type: other
diff --git a/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml
new file mode 100644
index 00000000000..cd31fe0f35c
--- /dev/null
+++ b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml
@@ -0,0 +1,5 @@
+---
+title: Restrict slash commands to users who can log in
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-discussions-api-perf.yml b/changelogs/unreleased/sh-fix-discussions-api-perf.yml
new file mode 100644
index 00000000000..8cdbbf03dab
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-discussions-api-perf.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate many Gitaly calls in discussions API
+merge_request: 31834
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-import-export-suggestions.yml b/changelogs/unreleased/sh-fix-import-export-suggestions.yml
new file mode 100644
index 00000000000..4b15fc3858f
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-import-export-suggestions.yml
@@ -0,0 +1,5 @@
+---
+title: Properly save suggestions in project exports
+merge_request: 31690
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml
new file mode 100644
index 00000000000..502fc22ebbd
--- /dev/null
+++ b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml
@@ -0,0 +1,5 @@
+---
+title: Only expire tag cache once per push
+merge_request: 31641
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml
new file mode 100644
index 00000000000..cd63b9bf425
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce Gitaly calls in PostReceive
+merge_request: 31741
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-update-rugged-0-28-3.yml b/changelogs/unreleased/sh-update-rugged-0-28-3.yml
new file mode 100644
index 00000000000..86446564e12
--- /dev/null
+++ b/changelogs/unreleased/sh-update-rugged-0-28-3.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Rugged to 0.28.3
+merge_request: 31794
+author:
+type: security
diff --git a/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml b/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml
new file mode 100644
index 00000000000..898fdef830a
--- /dev/null
+++ b/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml
@@ -0,0 +1,5 @@
+---
+title: Update GraphicsMagick from 1.3.29 to 1.3.33 for CI tests
+merge_request: 31692
+author: Takuya Noguchi
+type: other
diff --git a/config.ru b/config.ru
index 6f6fb85d8fa..f6a7dca0542 100644
--- a/config.ru
+++ b/config.ru
@@ -17,7 +17,25 @@ end
require ::File.expand_path('../config/environment', __FILE__)
+# The following is necessary to ensure stale Prometheus metrics don't accumulate over time.
+# It needs to be done as early as here to ensure metrics files aren't deleted.
+# After we hit our app in `warmup`, first metrics and corresponding files already being created,
+# for example in `lib/gitlab/metrics/requests_rack_middleware.rb`.
+def cleanup_prometheus_multiproc_dir
+ if dir = ::Prometheus::Client.configuration.multiprocess_files_dir
+ old_metrics = Dir[File.join(dir, '*.db')]
+
+ FileUtils.rm_rf(old_metrics)
+ end
+end
+
+def master_process?
+ Prometheus::PidProvider.worker_id.in? %w(unicorn_master puma_master)
+end
+
warmup do |app|
+ cleanup_prometheus_multiproc_dir if master_process?
+
client = Rack::MockRequest.new(app)
client.get('/')
end
diff --git a/config/application.rb b/config/application.rb
index 47c5ab71285..2554dd8cca2 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -41,11 +41,6 @@ module Gitlab
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
- #{config.root}/app/workers/concerns
- #{config.root}/app/policies/concerns
- #{config.root}/app/services/concerns
- #{config.root}/app/serializers/concerns
- #{config.root}/app/finders/concerns
#{config.root}/app/graphql/resolvers/concerns
#{config.root}/app/graphql/mutations/concerns])
@@ -293,10 +288,5 @@ module Gitlab
Gitlab::Routing.add_helpers(project_url_helpers)
Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end
-
- # This makes generated cookies to be compatible with Rails 5.1 and older
- # We can remove this when we're confident that there are no issues with the Rails 5.2 upgrade
- # and we won't need to rollback to older versions
- config.action_dispatch.use_authenticated_cookie_encryption = false
end
end
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 3f2691dde95..70e5dcd042e 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -51,22 +51,3 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
end
end
end
-
-def cleanup_prometheus_multiproc_dir
- # The following is necessary to ensure stale Prometheus metrics don't
- # accumulate over time. It needs to be done in this hook as opposed to
- # inside an init script to ensure metrics files aren't deleted after new
- # unicorn workers start after a SIGUSR2 is received.
- if dir = ::Prometheus::Client.configuration.multiprocess_files_dir
- old_metrics = Dir[File.join(dir, '*.db')]
- FileUtils.rm_rf(old_metrics)
- end
-end
-
-Gitlab::Cluster::LifecycleEvents.on_master_start do
- cleanup_prometheus_multiproc_dir
-end
-
-Gitlab::Cluster::LifecycleEvents.on_master_restart do
- cleanup_prometheus_multiproc_dir
-end
diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb
index 2ecb7956007..a1abb29838b 100644
--- a/config/initializers/elastic_client_setup.rb
+++ b/config/initializers/elastic_client_setup.rb
@@ -5,46 +5,42 @@
require 'gitlab/current_settings'
Gitlab.ee do
+ require 'elasticsearch/model'
+
+ ### Modified from elasticsearch-model/lib/elasticsearch/model.rb
+
+ [
+ Elasticsearch::Model::Client::ClassMethods,
+ Elasticsearch::Model::Naming::ClassMethods,
+ Elasticsearch::Model::Indexing::ClassMethods,
+ Elasticsearch::Model::Searching::ClassMethods
+ ].each do |mod|
+ Elasticsearch::Model::Proxy::ClassMethodsProxy.include mod
+ end
+
+ [
+ Elasticsearch::Model::Client::InstanceMethods,
+ Elasticsearch::Model::Naming::InstanceMethods,
+ Elasticsearch::Model::Indexing::InstanceMethods,
+ Elasticsearch::Model::Serializing::InstanceMethods
+ ].each do |mod|
+ Elasticsearch::Model::Proxy::InstanceMethodsProxy.include mod
+ end
+
+ Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def as_indexed_json(options={})
+ target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super
+ end
+ CODE
+
+ ### Monkey patches
+
Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records
Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records
Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods
-
- module Elasticsearch
- module Model
- module Client
- # This mutex is only used to synchronize *creation* of a new client, so
- # all including classes can share the same client instance
- CLIENT_MUTEX = Mutex.new
-
- cattr_accessor :cached_client
- cattr_accessor :cached_config
-
- module ClassMethods
- # Override the default ::Elasticsearch::Model::Client implementation to
- # return a client configured from application settings. All including
- # classes will use the same instance, which is refreshed automatically
- # if the settings change.
- #
- # _client is present to match the arity of the overridden method, where
- # it is also not used.
- #
- # @return [Elasticsearch::Transport::Client]
- def client(_client = nil)
- store = ::Elasticsearch::Model::Client
-
- store::CLIENT_MUTEX.synchronize do
- config = Gitlab::CurrentSettings.elasticsearch_config
-
- if store.cached_client.nil? || config != store.cached_config
- store.cached_client = ::Gitlab::Elastic::Client.build(config)
- store.cached_config = config
- end
- end
-
- store.cached_client
- end
- end
- end
- end
- end
+ Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing
+ Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client
+ Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
+ Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client
+ Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client
end
diff --git a/config/initializers/invisible_captcha.rb b/config/initializers/invisible_captcha.rb
new file mode 100644
index 00000000000..5177c730596
--- /dev/null
+++ b/config/initializers/invisible_captcha.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+InvisibleCaptcha.setup do |config|
+ config.honeypots = %w(firstname lastname)
+ config.timestamp_enabled = true
+ config.timestamp_threshold = 4
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index a8234263275..258d8a99986 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -69,7 +69,7 @@ en:
email: Allows read-only access to the user's primary email address using OpenID Connect
scope_desc:
api:
- Grants complete read/write access to the API, including all groups and projects.
+ Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry.
read_user:
Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users.
read_repository:
diff --git a/config/locales/invisible_captcha.en.yml b/config/locales/invisible_captcha.en.yml
new file mode 100644
index 00000000000..5978549c0c3
--- /dev/null
+++ b/config/locales/invisible_captcha.en.yml
@@ -0,0 +1,4 @@
+en:
+ invisible_captcha:
+ sentence_for_humans: If you are human, please ignore this field.
+ timestamp_error_message: That was a bit too quick! Please resubmit.
diff --git a/config/routes.rb b/config/routes.rb
index 459f2b22bf0..fdef31429f3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,6 +58,7 @@ Rails.application.routes.draw do
# Search
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
+ get 'search/count' => 'search#count', as: :search_count
# JSON Web Token
get 'jwt/auth' => 'jwt#auth'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index b9258a35f0c..9a453d101a1 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -477,7 +477,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# in JSON format, or a request for tag named `latest.json`.
scope format: false do
resources :tags, only: [:index, :destroy],
- constraints: { id: Gitlab::Regex.container_registry_tag_regex }
+ constraints: { id: Gitlab::Regex.container_registry_tag_regex } do
+ collection do
+ delete :bulk_destroy
+ end
+ end
end
end
end
@@ -505,7 +509,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :discussions, format: :json
Gitlab.ee do
- get 'designs(/*vueroute)', to: 'issues#show', format: false
+ get 'designs(/*vueroute)', to: 'issues#show', as: :designs, format: false
end
end
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 3f768d5d384..d4616c8080d 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -43,13 +43,6 @@ scope '-/users', module: :users do
end
end
-scope '-/users', module: :users do
- resources :terms, only: [:index] do
- post :accept, on: :member
- post :decline, on: :member
- end
-end
-
scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do
scope(path: 'users/:username',
as: :user,
diff --git a/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb
new file mode 100644
index 00000000000..2d3243f3357
--- /dev/null
+++ b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.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 AddObjectStorageFlagToGeoNode < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false
+ end
+
+ def down
+ remove_column :geo_nodes, :sync_object_storage
+ end
+end
diff --git a/db/migrate/20190715215532_add_project_emails_disabled.rb b/db/migrate/20190715215532_add_project_emails_disabled.rb
new file mode 100644
index 00000000000..536ea34c0fb
--- /dev/null
+++ b/db/migrate/20190715215532_add_project_emails_disabled.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddProjectEmailsDisabled < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :emails_disabled, :boolean
+ end
+end
diff --git a/db/migrate/20190715215549_add_group_emails_disabled.rb b/db/migrate/20190715215549_add_group_emails_disabled.rb
new file mode 100644
index 00000000000..d3fd4d2d923
--- /dev/null
+++ b/db/migrate/20190715215549_add_group_emails_disabled.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddGroupEmailsDisabled < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :emails_disabled, :boolean
+ end
+end
diff --git a/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb
new file mode 100644
index 00000000000..5c005377b00
--- /dev/null
+++ b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class CreateAnalyticsCycleAnalyticsProjectStages < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ INDEX_PREFIX = 'index_analytics_ca_project_stages_'
+
+ def change
+ create_table :analytics_cycle_analytics_project_stages do |t|
+ t.timestamps_with_timezone
+ t.integer :relative_position
+ t.integer :start_event_identifier, null: false
+ t.integer :end_event_identifier, null: false
+ t.references(:project, {
+ null: false,
+ foreign_key: { to_table: :projects, on_delete: :cascade },
+ index: { name: INDEX_PREFIX + 'on_project_id' }
+ })
+ t.references(:start_event_label, {
+ foreign_key: { to_table: :labels, on_delete: :cascade },
+ index: { name: INDEX_PREFIX + 'on_start_event_label_id' }
+ })
+ t.references(:end_event_label, {
+ foreign_key: { to_table: :labels, on_delete: :cascade },
+ index: { name: INDEX_PREFIX + 'on_end_event_label_id' }
+ })
+ t.boolean :hidden, default: false, null: false
+ t.boolean :custom, default: true, null: false
+ t.string :name, null: false, limit: 255
+ end
+
+ add_index :analytics_cycle_analytics_project_stages, [:project_id, :name], unique: true, name: INDEX_PREFIX + 'on_project_id_and_name'
+ add_index :analytics_cycle_analytics_project_stages, [:relative_position], name: INDEX_PREFIX + 'on_relative_position'
+ end
+end
diff --git a/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb
new file mode 100644
index 00000000000..5b327dc5332
--- /dev/null
+++ b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class CreateAnalyticsCycleAnalyticsGroupStages < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ INDEX_PREFIX = 'index_analytics_ca_group_stages_'
+
+ def change
+ create_table :analytics_cycle_analytics_group_stages do |t|
+ t.timestamps_with_timezone
+ t.integer :relative_position
+ t.integer :start_event_identifier, null: false
+ t.integer :end_event_identifier, null: false
+ t.references(:group, {
+ null: false,
+ foreign_key: { to_table: :namespaces, on_delete: :cascade },
+ index: { name: INDEX_PREFIX + 'on_group_id' }
+ })
+ t.references(:start_event_label, {
+ foreign_key: { to_table: :labels, on_delete: :cascade },
+ index: { name: INDEX_PREFIX + 'on_start_event_label_id' }
+ })
+ t.references(:end_event_label, {
+ foreign_key: { to_table: :labels, on_delete: :cascade },
+ index: { name: INDEX_PREFIX + 'on_end_event_label_id' }
+ })
+ t.boolean :hidden, default: false, null: false
+ t.boolean :custom, default: true, null: false
+ t.string :name, null: false, limit: 255
+ end
+
+ add_index :analytics_cycle_analytics_group_stages, [:group_id, :name], unique: true, name: INDEX_PREFIX + 'on_group_id_and_name'
+ add_index :analytics_cycle_analytics_group_stages, [:relative_position], name: INDEX_PREFIX + 'on_relative_position'
+ end
+end
diff --git a/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb
new file mode 100644
index 00000000000..063c1e16c27
--- /dev/null
+++ b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class MigratePrivateProfileNulls < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ DELAY = 5.minutes.to_i
+ BATCH_SIZE = 1_000
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ include ::EachBatch
+ end
+
+ def up
+ # Migration will take about 7 hours
+ User.where(private_profile: nil).each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck(Arel.sql("MIN(id)"), Arel.sql("MAX(id)")).first
+ delay = index * DELAY
+
+ BackgroundMigrationWorker.perform_in(delay.seconds, 'MigrateNullPrivateProfileToFalse', [*range])
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 591758af0e4..cf7f0fc0a3d 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: 2019_08_06_071559) do
+ActiveRecord::Schema.define(version: 2019_08_12_070645) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -26,6 +26,44 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do
t.integer "cached_markdown_version"
end
+ create_table "analytics_cycle_analytics_group_stages", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "relative_position"
+ t.integer "start_event_identifier", null: false
+ t.integer "end_event_identifier", null: false
+ t.bigint "group_id", null: false
+ t.bigint "start_event_label_id"
+ t.bigint "end_event_label_id"
+ t.boolean "hidden", default: false, null: false
+ t.boolean "custom", default: true, null: false
+ t.string "name", limit: 255, null: false
+ t.index ["end_event_label_id"], name: "index_analytics_ca_group_stages_on_end_event_label_id"
+ t.index ["group_id", "name"], name: "index_analytics_ca_group_stages_on_group_id_and_name", unique: true
+ t.index ["group_id"], name: "index_analytics_ca_group_stages_on_group_id"
+ t.index ["relative_position"], name: "index_analytics_ca_group_stages_on_relative_position"
+ t.index ["start_event_label_id"], name: "index_analytics_ca_group_stages_on_start_event_label_id"
+ end
+
+ create_table "analytics_cycle_analytics_project_stages", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "relative_position"
+ t.integer "start_event_identifier", null: false
+ t.integer "end_event_identifier", null: false
+ t.bigint "project_id", null: false
+ t.bigint "start_event_label_id"
+ t.bigint "end_event_label_id"
+ t.boolean "hidden", default: false, null: false
+ t.boolean "custom", default: true, null: false
+ t.string "name", limit: 255, null: false
+ t.index ["end_event_label_id"], name: "index_analytics_ca_project_stages_on_end_event_label_id"
+ t.index ["project_id", "name"], name: "index_analytics_ca_project_stages_on_project_id_and_name", unique: true
+ t.index ["project_id"], name: "index_analytics_ca_project_stages_on_project_id"
+ t.index ["relative_position"], name: "index_analytics_ca_project_stages_on_relative_position"
+ t.index ["start_event_label_id"], name: "index_analytics_ca_project_stages_on_start_event_label_id"
+ end
+
create_table "appearances", id: :serial, force: :cascade do |t|
t.string "title", null: false
t.text "description", null: false
@@ -1456,6 +1494,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do
t.integer "container_repositories_max_capacity", default: 10, null: false
t.datetime_with_timezone "created_at"
t.datetime_with_timezone "updated_at"
+ t.boolean "sync_object_storage", default: false, null: false
t.index ["access_key"], name: "index_geo_nodes_on_access_key"
t.index ["name"], name: "index_geo_nodes_on_name", unique: true
t.index ["primary"], name: "index_geo_nodes_on_primary"
@@ -2174,6 +2213,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do
t.boolean "membership_lock", default: false
t.integer "last_ci_minutes_usage_notification_level"
t.integer "subgroup_creation_level", default: 1
+ t.boolean "emails_disabled"
t.index ["created_at"], name: "index_namespaces_on_created_at"
t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)"
t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id"
@@ -2744,6 +2784,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do
t.boolean "reset_approvals_on_push", default: true
t.boolean "service_desk_enabled", default: true
t.integer "approvals_before_merge", default: 0, null: false
+ t.boolean "emails_disabled"
t.index ["archived", "pending_delete", "merge_requests_require_code_owner_approval"], name: "projects_requiring_code_owner_approval", where: "((pending_delete = false) AND (archived = false) AND (merge_requests_require_code_owner_approval = true))"
t.index ["created_at"], name: "index_projects_on_created_at"
t.index ["creator_id"], name: "index_projects_on_creator_id"
@@ -3629,6 +3670,12 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do
t.index ["type"], name: "index_web_hooks_on_type"
end
+ add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "end_event_label_id", on_delete: :cascade
+ add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "start_event_label_id", on_delete: :cascade
+ add_foreign_key "analytics_cycle_analytics_group_stages", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "end_event_label_id", on_delete: :cascade
+ add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "start_event_label_id", on_delete: :cascade
+ add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "instance_administration_project_id", on_delete: :nullify
diff --git a/doc/README.md b/doc/README.md
index c60e4eb177d..8ce5d2e240a 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -354,6 +354,7 @@ The following documentation relates to the DevOps **Secure** stage:
| Secure Topics | Description |
|:------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------|
| [Container Scanning](user/application_security/container_scanning/index.md) **(ULTIMATE)** | Use Clair to scan docker images for known vulnerabilities. |
+| [Dependency List](user/application_security/dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. |
| [Dependency Scanning](user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](user/application_security/dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. |
| [Group Security Dashboard](user/application_security/security_dashboard/index.md) **(ULTIMATE)** | View vulnerabilities in all the projects in a group and its subgroups. |
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index e418938451a..d0adeb89543 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -669,6 +669,39 @@ To get around this, you can [change the group path](../user/group/index.md#chang
branch name. Another option is to create a [push rule](../push_rules/push_rules.html) to prevent
this at the instance level.
+### Image push errors
+
+When getting errors or "retrying" loops in an attempt to push an image but `docker login` works fine,
+there is likely an issue with the headers forwarded to the registry by NGINX. The default recommended
+NGINX configurations should handle this, but it might occur in custom setups where the SSL is
+offloaded to a third party reverse proxy.
+
+This problem was discussed in a [docker project issue][docker-image-push-issue] and a simple solution
+would be to enable relative urls in the registry.
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['env'] = {
+ "REGISTRY_HTTP_RELATIVEURLS" => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**For installations from source**
+
+1. Edit the YML configuration file you created when you [deployed the registry][registry-deploy]. Add the following snippet:
+
+ ```yaml
+ http:
+ relativeurls: true
+ ```
+
+1. Restart the registry for the changes to take affect.
+
[ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239
[docker-insecure-self-signed]: https://docs.docker.com/registry/insecure/#use-self-signed-certificates
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
@@ -687,3 +720,4 @@ this at the instance level.
[new-domain]: #configure-container-registry-under-its-own-domain
[notifications-config]: https://docs.docker.com/registry/notifications/
[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
+[docker-image-push-issue]: https://github.com/docker/distribution/issues/970
diff --git a/doc/administration/index.md b/doc/administration/index.md
index fb07b41eb8b..2b25e8efd23 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -190,3 +190,8 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Troubleshooting ElasticSearch](troubleshooting/elasticsearch.md): Tips to troubleshoot ElasticSearch.
- [Kubernetes troubleshooting](troubleshooting/kubernetes_cheat_sheet.md): Commands and tips useful
for troubleshooting Kubernetes-related issues.
+- Useful links from the Support Team:
+ - [GitLab Developer Docs](https://docs.gitlab.com/ee/development/README.html).
+ - [Repairing and recovering broken git repositories](https://git.seveas.net/repairing-and-recovering-broken-git-repositories.html).
+ - [Testing with OpenSSL](https://www.feistyduck.com/library/openssl-cookbook/online/ch-testing-with-openssl.html).
+ - [Strace zine](https://wizardzines.com/zines/strace/).
diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
new file mode 100644
index 00000000000..8e33cea6217
--- /dev/null
+++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
@@ -0,0 +1,36 @@
+# GitLab instance administration project
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/56883) in GitLab 12.2.
+
+GitLab has been adding the ability for administrators to see insights into the health of
+their GitLab instance. In order to surface this experience in a native way, similar to how
+you would interact with an application deployed via GitLab, a base project called
+"GitLab Instance Administration" with
+[internal visibility](../../../public_access/public_access.md#internal-projects) will be
+added under a group called "GitLab Instance Administrators" specifically created for
+visualizing and configuring the monitoring of your GitLab instance.
+
+All administrators at the time of creation of the project and group will be added
+as maintainers of the group and project, and as an admin, you'll be able to add new
+members to the group in order to give them maintainer access to the project.
+
+This project will be used for self-monitoring your GitLab instance.
+
+## Connection to Prometheus
+
+The project will be automatically configured to connect to the
+[internal Prometheus](../prometheus/index.md) instance if the Prometheus
+instance is present (should be the case if GitLab was installed via Omnibus
+and you haven't disabled it).
+
+If that's not the case or if you have an external Prometheus instance or an HA setup,
+you should
+[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus).
+
+## Taking action on Prometheus alerts **[ULTIMATE]**
+
+You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances)
+to the Prometheus config in order for GitLab to receive notifications of any alerts.
+
+Once the webhook is setup, you can
+[take action on incoming alerts](../../../user/project/integrations/prometheus.md#taking-action-on-incidents-ultimate).
diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md
index fa0459b24ff..2b3daec42bd 100644
--- a/doc/administration/monitoring/index.md
+++ b/doc/administration/monitoring/index.md
@@ -2,6 +2,9 @@
Explore our features to monitor your GitLab instance:
+- [GitLab self-monitoring](gitlab_instance_administration_project/index.md): The
+ GitLab instance administration project helps to monitor the GitLab instance and
+ take action on alerts.
- [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance.
- [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products.
- [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics.
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index 6778c339922..95be0d5fd88 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -118,6 +118,36 @@ If you have set up Grafana, you can enable a link to access it easily from the s
1. Click **Save changes**.
1. The new link will be available in the admin area under **Monitoring > Metrics Dashboard**.
+## Security Update
+
+Users running GitLab version 12.0 or later should immediately upgrade to one of the following security releases due to a known vulnerability with the embedded Grafana dashboard:
+
+- 12.0.6
+- 12.1.6
+
+After upgrading, the Grafana dashboard will be disabled and the location of your existing Grafana data will be changed from `/var/opt/gitlab/grafana/data/` to `/var/opt/gitlab/grafana/data.bak.#{Date.today}/`.
+
+To prevent the data from being relocated, you can run the following command prior to upgrading:
+
+```sh
+echo "0" > /var/opt/gitlab/grafana/CVE_reset_status
+```
+
+To reinstate your old data, move it back into its original location:
+
+```
+sudo mv /var/opt/gitlab/grafana/data.bak.xxxx/ /var/opt/gitlab/grafana/data/
+```
+
+However, you should **not** reinstate your old data _except_ under one of the following conditions:
+
+1. If you are certain that you changed your default admin password when you enabled Grafana
+1. If you run GitLab in a private network, accessed only by trusted users, and your Grafana login page has not been exposed to the internet
+
+If you require access to your old Grafana data but do not meet one of these criteria, you may consider reinstating it temporarily, [exporting the dashboards](https://grafana.com/docs/reference/export_import/#exporting-a-dashboard) you need, then refreshing the data and [re-importing your dashboards](https://grafana.com/docs/reference/export_import/#importing-a-dashboard). Note that this poses a temporary vulnerability while your old Grafana data is in use, and the decision to do so should be weighed carefully with your need to access existing data and dashboards.
+
+For more information and further mitigation details, please refer to our [blog post on the security release](https://about.gitlab.com/2019/08/12/critical-security-release-gitlab-12-dot-1-dot-6-released/).
+
---
Read more on:
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 054fa547704..ec26c0b2e7e 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -120,7 +120,6 @@ When Puma is used instead of Unicorn, following metrics are available:
| puma_workers | Gauge | 12.0 | Total number of workers |
| puma_running_workers | Gauge | 12.0 | Number of booted workers |
| puma_stale_workers | Gauge | 12.0 | Number of old workers |
-| puma_phase | Gauge | 12.0 | Phase number (increased during phased restarts) |
| puma_running | Gauge | 12.0 | Number of running threads |
| puma_queued_connections | Gauge | 12.0 | Number of connections in that worker's "todo" set waiting for a worker thread |
| puma_active_connections | Gauge | 12.0 | Number of threads processing a request |
diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md
index ae67d7f08d6..8178cb243f3 100644
--- a/doc/administration/operations/unicorn.md
+++ b/doc/administration/operations/unicorn.md
@@ -69,7 +69,7 @@ unicorn['worker_memory_limit_min'] = "400 * 1 << 20"
unicorn['worker_memory_limit_max'] = "650 * 1 << 20"
```
-Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MIN`
+Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MAX`
[environment variables](../environment_variables.md).
This is what a Unicorn worker memory restart looks like in unicorn_stderr.log.
diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md
index 2496b038c7f..015ffbe60f6 100644
--- a/doc/api/dependencies.md
+++ b/doc/api/dependencies.md
@@ -11,7 +11,7 @@ Every call to this endpoint requires authentication. To perform this call, user
## List project dependencies
Get a list of project dependencies. This API partially mirroring
-[Dependency List](../user/application_security/dependency_scanning/index.md#dependency-list) feature.
+[Dependency List](../user/application_security/dependency_list/index.md) feature.
This list can be generated only for [languages and package managers](../user/application_security/dependency_scanning/index.md#supported-languages-and-package-managers)
supported by Gemnasium.
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index e3af5d60533..d0b33ab467f 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -10,7 +10,7 @@ GET /geo_nodes
```
```bash
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes
```
Example response:
@@ -27,8 +27,15 @@ Example response:
"current": true,
"files_max_capacity": 10,
"repos_max_capacity": 25,
+ "container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
- "clone_protocol": "http"
+ "clone_protocol": "http",
+ "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
+ "_links": {
+ "self": "https://primary.example.com/api/v4/geo_nodes/1",
+ "status":"https://primary.example.com/api/v4/geo_nodes/1/status",
+ "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair"
+ }
},
{
"id": 2,
@@ -40,8 +47,17 @@ Example response:
"current": false,
"files_max_capacity": 10,
"repos_max_capacity": 25,
+ "container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
- "clone_protocol": "http"
+ "sync_object_storage": true,
+ "clone_protocol": "http",
+ "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit",
+ "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
+ "_links": {
+ "self":"https://primary.example.com/api/v4/geo_nodes/2",
+ "status":"https://primary.example.com/api/v4/geo_nodes/2/status",
+ "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair"
+ }
}
]
```
@@ -53,7 +69,7 @@ GET /geo_nodes/:id
```
```bash
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/1
```
Example response:
@@ -69,8 +85,15 @@ Example response:
"current": true,
"files_max_capacity": 10,
"repos_max_capacity": 25,
+ "container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
- "clone_protocol": "http"
+ "clone_protocol": "http",
+ "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
+ "_links": {
+ "self": "https://primary.example.com/api/v4/geo_nodes/1",
+ "status":"https://primary.example.com/api/v4/geo_nodes/1/status",
+ "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair"
+ }
}
```
@@ -84,16 +107,18 @@ _This can only be run against a primary Geo node._
PUT /geo_nodes/:id
```
-| Attribute | Type | Required | Description |
-|----------------------|---------|-----------|---------------------------------------------------------------------------|
-| `id` | integer | yes | The ID of the Geo node. |
-| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
-| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. |
-| `url` | string | yes | The user-facing URL of the Geo node. |
-| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.|
-| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
-| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
-| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
+| Attribute | Type | Required | Description |
+|-----------------------------|---------|-----------|---------------------------------------------------------------------------|
+| `id` | integer | yes | The ID of the Geo node. |
+| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
+| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. |
+| `url` | string | yes | The user-facing URL of the Geo node. |
+| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.|
+| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
+| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
+| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
+| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. |
+| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. |
Example response:
@@ -108,8 +133,17 @@ Example response:
"current": true,
"files_max_capacity": 10,
"repos_max_capacity": 25,
+ "container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
- "clone_protocol": "http"
+ "sync_object_storage": true,
+ "clone_protocol": "http",
+ "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit",
+ "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects",
+ "_links": {
+ "self":"https://primary.example.com/api/v4/geo_nodes/2",
+ "status":"https://primary.example.com/api/v4/geo_nodes/2/status",
+ "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair"
+ }
}
```
@@ -151,8 +185,15 @@ Example response:
"current": true,
"files_max_capacity": 10,
"repos_max_capacity": 25,
+ "container_repositories_max_capacity": 10,
"verification_max_capacity": 100,
- "clone_protocol": "http"
+ "clone_protocol": "http",
+ "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit",
+ "_links": {
+ "self": "https://primary.example.com/api/v4/geo_nodes/1",
+ "status":"https://primary.example.com/api/v4/geo_nodes/1/status",
+ "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair"
+ }
}
```
@@ -163,7 +204,7 @@ GET /geo_nodes/status
```
```bash
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/status
```
Example response:
@@ -314,7 +355,7 @@ GET /geo_nodes/:id/status
```
```bash
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/2/status
```
Example response:
@@ -388,7 +429,7 @@ GET /geo_nodes/current/failures
This endpoint uses [Pagination](README.md#pagination).
```bash
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/current/failures
```
Example response:
diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md
index 9c91264ed65..b1b75abe32f 100644
--- a/doc/api/releases/links.md
+++ b/doc/api/releases/links.md
@@ -21,7 +21,7 @@ GET /projects/:id/releases/:tag_name/assets/links
Example request:
```sh
-curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links"
+curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
```
Example response:
@@ -60,7 +60,7 @@ GET /projects/:id/releases/:tag_name/assets/links/:link_id
Example request:
```sh
-curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1"
+curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
@@ -96,7 +96,7 @@ curl --request POST \
--header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \
--data name="awesome-v0.2.dmg" \
--data url="http://192.168.10.15:3000" \
- "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links"
+ "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
```
Example response:
@@ -132,7 +132,7 @@ You have to specify at least one of `name` or `url`
Example request:
```sh
-curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1"
+curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
@@ -163,7 +163,7 @@ DELETE /projects/:id/releases/:tag_name/assets/links/:link_id
Example request:
```sh
-curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1"
+curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 83125aff264..248d19461f6 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -321,4 +321,8 @@ are listed in the descriptions of the relevant settings.
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
+| `snowplow_enabled` | boolean | no | Enable snowplow tracking. |
+| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) |
+| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) |
+| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) |
| `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
diff --git a/doc/ci/README.md b/doc/ci/README.md
index f3006528d01..ca9d0aa61bd 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -16,7 +16,7 @@ through the [continuous methodologies](introduction/index.md#introduction-to-cic
NOTE: **Out-of-the-box management systems can decrease hours spent on maintaining toolchains by 10% or more.**
Watch our
["Mastering continuous software development"](https://about.gitlab.com/webcast/mastering-ci-cd/)
-webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development.
+webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development.
## Overview
@@ -67,11 +67,11 @@ to your needs:
For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide.
Once you're familiar with how GitLab CI/CD works, see the
-[`. gitlab-ci.yml` full reference](yaml/README.md)
+[`.gitlab-ci.yml` full reference](yaml/README.md)
for all the attributes you can set and use.
NOTE: **Note:**
-GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota-free-only).
+GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/gitlab_com/index.md#shared-runners).
## Configuration
diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md
new file mode 100644
index 00000000000..e54be9f3bd9
--- /dev/null
+++ b/doc/ci/directed_acyclic_graph/index.md
@@ -0,0 +1,76 @@
+---
+type: reference
+---
+
+# Directed Acyclic Graph
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/47063) in GitLab 12.2 (enabled by `ci_dag_support` feature flag).
+
+A [directed acyclic graph](https://www.techopedia.com/definition/5739/directed-acyclic-graph-dag) can be
+used in the context of a CI/CD pipeline to build relationships between jobs such that
+execution is performed in the quickest possible manner, regardless how stages may
+be set up.
+
+For example, you may have a specific tool or separate website that is built
+as part of your main project. Using a DAG, you can specify the relationship between
+these jobs and GitLab will then execute the jobs as soon as possible instead of waiting
+for each stage to complete.
+
+Unlike other DAG solutions for CI/CD, GitLab does not require you to choose one or the
+other. You can implement a hybrid combination of DAG and traditional
+stage-based operation within a single pipeline. Configuration is kept very simple,
+requiring a single keyword to enable the feature for any job.
+
+Consider a monorepo as follows:
+
+```
+./service_a
+./service_b
+./service_c
+./service_d
+```
+
+It has a pipeline that looks like the following:
+
+| build | test | deploy |
+| ----- | ---- | ------ |
+| build_a | test_a | deploy_a |
+| build_b | test_b | deploy_b |
+| build_c | test_c | deploy_c |
+| build_d | test_d | deploy_d |
+
+Using a DAG, you can relate the `_a` jobs to each other separately from the `_b` jobs,
+and even if service `a` takes a very long time to build, service `b` will not
+wait for it and will finish as quickly as it can. In this very same pipeline, `_c` and
+`_d` can be left alone and will run together in staged sequence just like any normal
+GitLab pipeline.
+
+## Use cases
+
+A DAG can help solve several different kinds of relationships between jobs within
+a CI/CD pipeline. Most typically this would cover when jobs need to fan in or out,
+and/or merge back together (diamond dependencies). This can happen when you're
+handling multi-platform builds or complex webs of dependencies as in something like
+an operating system build or a complex deployment graph of independently deployable
+but related microservices.
+
+Additionally, a DAG can help with general speediness of pipelines and helping
+to deliver fast feedback. By creating dependency relationships that don't unnecessarily
+block each other, your pipelines will run as quickly as possible regardless of
+pipeline stages, ensuring output (including errors) is available to developers
+as quickly as possible.
+
+## Usage
+
+Relationships are defined between jobs using the [`needs:` keyword](../yaml/README.md#needs).
+
+Note that `needs:` also works with the [parallel](../yaml/README.md#parallel) keyword,
+giving your powerful options for parallelization within your pipeline.
+
+## Limitations
+
+A directed acyclic graph is a complicated feature, and as of the initial MVC there
+are certain use cases that you may need to work around. For more information:
+
+- [`needs` requirements and limitations](../yaml/README.md#requirements-and-limitations).
+- Related epic [gitlab-org#1716](https://gitlab.com/groups/gitlab-org/-/epics/1716).
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 9e1a62bae71..f6c47a99712 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -619,6 +619,10 @@ versions of the app, all without leaving GitLab.
Add a [button to the Monitoring dashboard](../user/project/operations/linking_to_an_external_dashboard.md) linking directly to your existing external dashboards.
+#### Embedding metrics in GitLab Flavored Markdown
+
+Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details.
+
### Web terminals
> Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index c3678fc948e..8474d4ef66e 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -319,21 +319,21 @@ How this feature will work:
1. You set the _maximum job timeout_ for a Runner to 24 hours
1. You set the _CI/CD Timeout_ for a project to **2 hours**
1. You start a job
-1. The job, if running longer, will be timeouted after **2 hours**
+1. The job, if running longer, will be timed out after **2 hours**
**Example 2 - Runner timeout not configured**
1. You remove the _maximum job timeout_ configuration from a Runner
1. You set the _CI/CD Timeout_ for a project to **2 hours**
1. You start a job
-1. The job, if running longer, will be timeouted after **2 hours**
+1. The job, if running longer, will be timed out after **2 hours**
**Example 3 - Runner timeout smaller than project timeout**
1. You set the _maximum job timeout_ for a Runner to **30 minutes**
1. You set the _CI/CD Timeout_ for a project to 2 hours
1. You start a job
-1. The job, if running longer, will be timeouted after **30 minutes**
+1. The job, if running longer, will be timed out after **30 minutes**
### Be careful with sensitive information
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index a6051e87366..2be93433b36 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1665,6 +1665,84 @@ You can ask your administrator to
[flip this switch](../../administration/job_artifacts.md#validation-for-dependencies)
and bring back the old behavior.
+### `needs`
+
+> Introduced in GitLab 12.2.
+
+The `needs:` keyword enables executing jobs out-of-order, allowing you to implement
+a [directed acyclic graph](../directed_acyclic_graph/index.md) in your `.gitlab-ci.yml`.
+
+This lets you run some jobs without waiting for other ones, disregarding stage ordering
+so you can have multiple stages running concurrently.
+
+Let's consider the following example:
+
+```yaml
+linux:build:
+ stage: build
+
+mac:build:
+ stage: build
+
+linux:rspec:
+ stage: test
+ needs: [linux:build]
+
+linux:rubocop:
+ stage: test
+ needs: [linux:build]
+
+mac:rspec:
+ stage: test
+ needs: [mac:build]
+
+mac:rubocop:
+ stage: test
+ needs: [mac:build]
+
+production:
+ stage: deploy
+```
+
+This example creates three paths of execution:
+
+- Linux path: the `linux:rspec` and `linux:rubocop` jobs will be run as soon
+ as the `linux:build` job finishes without waiting for `mac:build` to finish.
+
+- macOS path: the `mac:rspec` and `mac:rubocop` jobs will be run as soon
+ as the `mac:build` job finishes, without waiting for `linux:build` to finish.
+
+- The `production` job will be executed as soon as all previous jobs
+ finish; in this case: `linux:build`, `linux:rspec`, `linux:rubocop`,
+ `mac:build`, `mac:rspec`, `mac:rubocop`.
+
+#### Requirements and limitations
+
+1. If `needs:` is set to point to a job that is not instantiated
+ because of `only/except` rules or otherwise does not exist, the
+ job will fail.
+1. Note that one day one of the launch, we are temporarily limiting the
+ maximum number of jobs that a single job can need in the `needs:` array. Track
+ our [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541)
+ for details on the current limit.
+1. If you use `dependencies:` with `needs:`, it's important that you
+ do not mark a job as having a dependency on something that won't
+ have been run at the time it needs it. It's better to use both
+ keywords in this case so that GitLab handles the ordering appropriately.
+1. It is impossible for now to have `needs: []` (empty needs),
+ the job always needs to depend on something, unless this is the job
+ in the first stage (see [gitlab-ce#65504](https://gitlab.com/gitlab-org/gitlab-ce/issues/65504)).
+1. If `needs:` refers to a job that is marked as `parallel:`.
+ the current job will depend on all parallel jobs created.
+1. `needs:` is similar to `dependencies:` in that needs to use jobs from
+ prior stages, this means that it is impossible to create circular
+ dependencies or depend on jobs in the current stage (see [gitlab-ce#65505](https://gitlab.com/gitlab-org/gitlab-ce/issues/65505)).
+1. Related to the above, stages must be explicitly defined for all jobs
+ that have the keyword `needs:` or are referred to by one.
+1. For self-managed users, the feature must be turned on using the `ci_dag_support`
+ feature flag. The `ci_dag_limit_needs` option, if set, will limit the number of
+ jobs that a single job can need to `50`. If unset, the limit is `5`.
+
### `coverage`
> [Introduced][ce-7447] in GitLab 8.17.
diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md
index adaa120a37e..ebf711e105b 100644
--- a/doc/customization/issue_and_merge_request_template.md
+++ b/doc/customization/issue_and_merge_request_template.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter'
+redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter'
---
-This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter).
+This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter).
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 423b35a9e3a..98b8a48abf4 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -171,6 +171,19 @@ Now, every time you create an MR for CE and EE:
job failed, you are required to submit the EE MR so that you can fix the conflicts in EE
before merging your changes into CE.
+## How we run the Automatic CE->EE merge at GitLab
+
+At GitLab, we use the [Merge Train](https://gitlab.com/gitlab-org/merge-train)
+project to keep our [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee)
+repository updated with commits from
+[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce).
+
+We have a mirror of the [Merge Train](https://gitlab.com/gitlab-org/merge-train)
+project [configured](https://ops.gitlab.net/gitlab-org/merge-train) to run an
+automatic CE->EE merge job every twenty minutes as a scheduled CI job. The
+[configured](https://ops.gitlab.net/gitlab-org/merge-train) Merge Train project
+is only accessible to authorized GitLab staff.
+
## FAQ
### How does automatic merging work?
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 17328762c5b..4e9c5c81379 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -143,6 +143,37 @@ If the guidelines are not met, the MR will not pass the
[Danger checks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/danger/commit_messages/Dangerfile).
For more information see [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).
+Example commit message template that can be used on your machine that embodies the above (guide for [how to apply template](https://codeinthehole.com/tips/a-useful-template-for-commit-messages/)):
+
+```text
+# (If applied, this commit will...) <subject> (Max 50 char)
+# |<---- Using a Maximum Of 50 Characters ---->|
+
+
+# Explain why this change is being made
+# |<---- Try To Limit Each Line to a Maximum Of 72 Characters ---->|
+
+# Provide links or keys to any relevant tickets, articles or other resources
+# Use issues and merge requests' full URLs instead of short references,
+# as they are displayed as plain text outside of GitLab
+
+# --- COMMIT END ---
+# --------------------
+# Remember to
+# Capitalize the subject line
+# Use the imperative mood in the subject line
+# Do not end the subject line with a period
+# Subject must contain at least 3 words
+# Separate subject from body with a blank line
+# Commits that change 30 or more lines across at least 3 files must
+# describe these changes in the commit body
+# Do not use Emojis
+# Use the body to explain what and why vs. how
+# Can use multiple lines with "-" for bullet points in body
+# For more information: https://chris.beams.io/posts/git-commit/
+# --------------------
+```
+
## Contribution acceptance criteria
To make sure that your merge request can be approved, please ensure that it meets
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index e84d65f424e..680f2cd13c2 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -41,7 +41,7 @@ Include any media types/sources if the content is relevant to readers. You can f
### No special types
-In the software industry, it is a best practice to organize documentatioin in different types. For example, [Divio recommends](https://www.divio.com/blog/documentation/):
+In the software industry, it is a best practice to organize documentation in different types. For example, [Divio recommends](https://www.divio.com/blog/documentation/):
1. Tutorials
1. How-to guides
@@ -283,24 +283,44 @@ Check specific punctuation rules for [list items](#list-items) below.
## List items
-- Always start list items with a capital letter.
+- Always start list items with a capital letter, unless they are parameters or commands
+ that are in backticks, or similar.
- Always leave a blank line before and after a list.
-- Begin a line with spaces (not tabs) to denote a subitem.
-- To nest subitems, indent them with two spaces.
-- To nest code blocks, indent them with four spaces.
-- Only use ordered lists when their items describe a sequence of steps to follow.
+- Begin a line with spaces (not tabs) to denote a [nested subitem](#nesting-inside-a-list-item).
+- Only use ordered lists when their items describe a sequence of steps to follow:
+
+ Do:
+
+ These are the steps to do something:
+
+ 1. First, do step 1
+ 1. Then, do step 2
+ 1. Finally, do step 3
+
+ Don't:
+
+ This is a list of different features:
+
+ 1. Feature 1
+ 1. Feature 2
+ 1. Feature 3
**Markup:**
- Use dashes (`-`) for unordered lists instead of asterisks (`*`).
-- Use the number one (`1`) for each item in an ordered list.
- When rendered, the list items will appear with sequential numbering.
+- Prefix `1.` to each item in an ordered list.
+ When rendered, the list items will appear with sequential numbering automatically.
**Punctuation:**
-- Do not add commas (`,`) or semicolons (`;`) to the end of a list item.
-- Only add periods to the end of a list item if the item consists of a complete sentence. The [definition of full sentence](https://www2.le.ac.uk/offices/ld/resources/writing/grammar/grammar-guides/sentence) is: _"a complete sentence always contains a verb, expresses a complete idea, and makes sense standing alone"_.
-- Be consistent throughout the list: if the majority of the items do not end in a period, do not end any of the items in a period, even if they consist of a complete sentence. The opposite is also valid: if the majority of the items end with a period, end all with a period.
+- Do not add commas (`,`) or semicolons (`;`) to the end of list items.
+- Only add periods to the end of a list item if the item consists of a complete sentence.
+ The [definition of full sentence](https://www2.le.ac.uk/offices/ld/resources/writing/grammar/grammar-guides/sentence)
+ is: _"a complete sentence always contains a verb, expresses a complete idea, and makes sense standing alone"_.
+- Be consistent throughout the list: if the majority of the items do not end in a period,
+ do not end any of the items in a period, even if they consist of a complete sentence.
+ The opposite is also valid: if the majority of the items end with a period, end
+ all with a period.
- Separate list items from explanatory text with a colon (`:`). For example:
```md
@@ -330,12 +350,86 @@ Do:
- Let's say this is also a complete sentence.
- Not a complete sentence.
-Don't:
+Don't (third item should have a `.` to match the first and second items):
- Let's say this is a complete sentence.
- Let's say this is also a complete sentence.
- Not a complete sentence
+### Nesting inside a list item
+
+It is possible to nest items under a list item, so that they render with the same indentation
+as the list item. This can be done with:
+
+- [Code blocks](#code-blocks)
+- [Blockquotes](#blockquotes)
+- [Alert boxes](#alert-boxes)
+- [Images](#images)
+
+Items nested in lists should always align with the first character of the list item.
+In unordered lists (using `-`), this means two spaces for each level of indentation:
+
+~~~md
+- Unordered list item 1
+
+ A line nested using 2 spaces to align with the `U` above.
+
+- Unordered list item 2
+
+ > A quote block that will nest
+ > inside list item 2.
+
+- Unordered list item 3
+
+ ```text
+ a codeblock that will next inside list item 3
+ ```
+
+- Unordered list item 4
+
+ ![an image that will nest inside list item 4](image.png)
+~~~
+
+For ordered lists, use three spaces for each level of indentation:
+
+~~~md
+1. Ordered list item 1
+
+ A line nested using 3 spaces to align with the `O` above.
+
+1. Ordered list item 2
+
+ > A quote block that will nest
+ > inside list item 2.
+
+1. Ordered list item 3
+
+ ```text
+ a codeblock that will next inside list item 3
+ ```
+
+1. Ordered list item 4
+
+ ![an image that will nest inside list item 4](image.png)
+~~~
+
+You can nest full lists inside other lists using the same rules as above. If you wish
+to mix types, that is also possible, as long as you don't mix items at the same level:
+
+```
+1. Ordered list item one.
+1. Ordered list item two.
+ - Nested unordered list item one.
+ - Nested unordered list item two.
+1. Ordered list item three.
+
+- Unordered list item one.
+- Unordered list item two.
+ 1. Nested ordered list item one.
+ 1. Nested ordered list item two.
+- Unordered list item three.
+```
+
## Quotes
Valid for markdown content only, not for frontmatter entries:
@@ -783,10 +877,10 @@ Other text includes deprecation notices and version-specific how-to information.
When a feature is available in EE-only tiers, add the corresponding tier according to the
feature availability:
+- For GitLab Core and GitLab.com Free: `**(CORE)**`.
- For GitLab Starter and GitLab.com Bronze: `**(STARTER)**`.
- For GitLab Premium and GitLab.com Silver: `**(PREMIUM)**`.
- For GitLab Ultimate and GitLab.com Gold: `**(ULTIMATE)**`.
-- For GitLab Core and GitLab.com Free: `**(CORE)**`.
To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
keyword "only":
@@ -798,6 +892,7 @@ keyword "only":
For GitLab.com only tiers (when the feature is not available for self-hosted instances):
+- For GitLab Free and higher tiers: `**(FREE ONLY)**`.
- For GitLab Bronze and higher tiers: `**(BRONZE ONLY)**`.
- For GitLab Silver and higher tiers: `**(SILVER ONLY)**`.
- For GitLab Gold: `**(GOLD ONLY)**`.
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
index 0965db29557..635895051bc 100644
--- a/doc/development/elasticsearch.md
+++ b/doc/development/elasticsearch.md
@@ -148,6 +148,36 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/
- Searches can have their own analyzers. Remember to check when editing analyzers
- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches
+## Architecture
+
+GitLab uses `elasticsearch-rails` for handling communication with Elasticsearch server. However, in order to achieve zero-downtime deployment during schema changes, an extra abstraction layer is built to allow:
+
+* Indexing (writes) to multiple indexes, with different mappings
+* Switching to different index for searches (reads) on the fly
+
+Currently we are on the process of migrating models to this new design (e.g. `Snippet`), and it is hardwired to work with a single version for now.
+
+Traditionally, `elasticsearch-rails` provides class and instance level `__elasticsearch__` proxy methods. If you call `Issue.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`, and if you call `Issue.first.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`. These proxy objects would talk to Elasticsearch server directly.
+
+In the new design, `__elasticsearch__` instead represents one extra layer of proxy. It would keep multiple versions of the actual proxy objects, and it would forward read and write calls to the proxy of the intended version.
+
+The `elasticsearch-rails`'s way of specifying each model's mappings and other settings is to create a module for the model to include. However in the new design, each model would have its own corresponding subclassed proxy object, where the settings reside in. For example, snippet related setting in the past reside in `SnippetsSearch` module, but in the new design would reside in `SnippetClassProxy` (which is a subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy`). This reduces namespace pollution in model classes.
+
+The global configurations per version are now in the `Elastic::(Version)::Config` class. You can change mappings there.
+
+### Creating new version of schema
+
+Currently GitLab would still work with a single version of setting. Once it is implemented, multiple versions of setting can exists in different folders (e.g. `ee/lib/elastic/v12p1` and `ee/lib/elastic/v12p3`). To keep a continuous git history, the latest version lives under the `/latest` folder, but is aliased as the latest version.
+
+If the current version is `v12p1`, and we need to create a new version for `v12p3`, the steps are as follows:
+
+1. Copy the entire folder of `v12p1` as `v12p3`
+1. Change the namespace for files under `v12p3` folder from `V12p1` to `V12p3` (which are still aliased to `Latest`)
+1. Delete `v12p1` folder
+1. Copy the entire folder of `latest` as `v12p1`
+1. Change the namespace for files under `v12p1` folder from `Latest` to `V12p1`
+1. Make changes to `Latest` as needed
+
## Troubleshooting
### Getting `flood stage disk watermark [95%] exceeded`
diff --git a/doc/development/fe_guide/event_tracking.md b/doc/development/fe_guide/event_tracking.md
index 715d91c6db6..1b417d4c8c2 100644
--- a/doc/development/fe_guide/event_tracking.md
+++ b/doc/development/fe_guide/event_tracking.md
@@ -1,6 +1,8 @@
-# Event Tracking
+# Event tracking
-We use a tracking interface that wraps up [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. Snowplow implements page tracking, but also exposes custom event tracking.
+GitLab provides `Tracking`, an interface that wraps
+[Snowplow](https://github.com/snowplow/snowplow) for tracking custom events.
+It uses Snowplow's custom event tracking functions.
The tracking interface can be imported in JS files as follows:
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 6bf6113dd81..deaef8e768b 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -17,6 +17,8 @@ Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn
For our currently-supported browsers, see our [requirements][requirements].
+Use [BrowserStack](https://www.browserstack.com/) to test with our supported browsers. Login to BrowserStack with the credentials saved in GitLab's [shared 1Password account](https://about.gitlab.com/handbook/security/#1password-for-teams).
+
## Initiatives
Current high-level frontend goals are listed on [Frontend Epics](https://gitlab.com/groups/gitlab-org/-/epics?label_name%5B%5D=frontend).
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 448d9fd01c4..9d6792e9139 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -70,6 +70,7 @@ bundle exec rspec spec/[path]/[to]/[spec].rb
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists.
+- Use `focus: true` to isolate parts of the specs you want to run.
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md
index 11aafd7b639..7c926c83a36 100644
--- a/doc/development/understanding_explain_plans.md
+++ b/doc/development/understanding_explain_plans.md
@@ -199,7 +199,7 @@ more common ones here.
A full list of all the available nodes and their descriptions can be found in
the [PostgreSQL source file
-"plannodes.h"](https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h)
+"plannodes.h"](https://gitlab.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h)
### Seq Scan
@@ -224,7 +224,7 @@ used when we would read too much data from an index scan, but too little to
perform a sequential scan. A bitmap scan uses what is known as a [bitmap
index](https://en.wikipedia.org/wiki/Bitmap_index) to perform its work.
-The [source code of PostgreSQL](https://github.com/postgres/postgres/blob/1c2cb2744bf3d8ad751cd5cf3b347f10f48492b3/src/include/nodes/plannodes.h#L446-L457)
+The [source code of PostgreSQL](https://gitlab.com/postgres/postgres/blob/REL_11_STABLE/src/include/nodes/plannodes.h#L441)
states the following on bitmap scans:
> Bitmap Index Scan delivers a bitmap of potential tuple locations; it does not
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 72a3514e2d5..295d9804497 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -172,7 +172,7 @@ sudo make install
# Download and compile from source
cd /tmp
curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.22.0.tar.gz
-echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b' git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz
+echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz
cd git-2.22.0/
./configure --with-libpcre
make prefix=/usr/local all
@@ -202,8 +202,8 @@ Then select 'Internet Site' and press enter to confirm the hostname.
The Ruby interpreter is required to run GitLab.
-**Note:** The current supported Ruby (MRI) version is 2.5.x. GitLab 11.6
- dropped support for Ruby 2.4.x.
+**Note:** The current supported Ruby (MRI) version is 2.6.x. GitLab 12.2
+ dropped support for Ruby 2.5.x.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 83a9e7fe294..234e5acb394 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -40,7 +40,7 @@ Please consider using a virtual machine to run GitLab.
## Ruby versions
-GitLab requires Ruby (MRI) 2.5. Support for Ruby versions below 2.5 (2.3, 2.4) will stop with GitLab 11.6.
+GitLab requires Ruby (MRI) 2.6. Support for Ruby versions below 2.6 (2.4, 2.5) will stop with GitLab 12.2.
You will have to use the standard MRI implementation of Ruby.
We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab
diff --git a/doc/user/admin_area/settings/img/additional_minutes.png b/doc/subscriptions/img/additional_minutes.png
index b159b98c9ce..b159b98c9ce 100644
--- a/doc/user/admin_area/settings/img/additional_minutes.png
+++ b/doc/subscriptions/img/additional_minutes.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/buy_btn.png b/doc/subscriptions/img/buy_btn.png
index 4fd05c0fba7..4fd05c0fba7 100644
--- a/doc/user/admin_area/settings/img/buy_btn.png
+++ b/doc/subscriptions/img/buy_btn.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/buy_minutes_card.png b/doc/subscriptions/img/buy_minutes_card.png
index cab098300cd..cab098300cd 100644
--- a/doc/user/admin_area/settings/img/buy_minutes_card.png
+++ b/doc/subscriptions/img/buy_minutes_card.png
Binary files differ
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index 9317ba0c71e..13c406727ab 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -221,6 +221,52 @@ The following table describes details of your subscription for groups:
| Subscription start date | Date your subscription started. If this is for a Free plan, is the date you transitioned off your group's paid plan. |
| Subscription end date | Date your current subscription will end. Does not apply to Free plans. |
+#### Extra Shared Runners pipeline minutes
+
+If you're using GitLab.com, you can purchase additional CI minutes so your
+pipelines will not be blocked after you have used all your CI minutes from your
+main quota. Additional minutes:
+
+- Are only used once the shared quota included in your subscription runs out.
+- Roll over month to month.
+
+In order to purchase additional minutes, you should follow these steps:
+
+1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**.
+
+ ![Buy additional minutes](img/buy_btn.png)
+
+1. Locate the subscription card that is linked to your group on GitLab.com,
+ click on **Buy more CI minutes**, and complete the details about the transaction.
+
+ ![Buy additional minutes](img/buy_minutes_card.png)
+
+1. Once we have processed your payment, the extra CI minutes
+ will be synced to your Group and you can visualize it from the
+ **Group > Settings > Pipelines quota** page:
+
+ ![Additional minutes](img/additional_minutes.png)
+
+Be aware that:
+
+1. If you have purchased extra CI minutes before the purchase of a paid plan,
+ we will calculate a pro-rated charge for your paid plan. That means you may
+ be charged for less than one year since your subscription was previously
+ created with the extra CI minutes.
+1. Once the extra CI minutes has been assigned to a Group they cannot be transferred
+ to a different Group.
+1. If you have some minutes used over your default quota, these minutes will
+ be deducted from your Additional Minutes quota immediately after your purchase of additional
+ minutes.
+
+##### What happens when my CI minutes run out
+
+When the CI minutes run out, an email is sent automatically to notify the owner(s)
+of the group/namespace, including a link to [purchase more minutes](https://customers.gitlab.com/plans).
+
+If you are not the owner of the group, you will need to contact them to let them know they need to
+[purchase more minutes](https://customers.gitlab.com/plans).
+
## Subscription changes and your data
When your subscription or trial expires, GitLab does not delete your data.
diff --git a/doc/update/README.md b/doc/update/README.md
index 974982da5d0..42c43110a19 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -135,6 +135,30 @@ If you need to downgrade your Enterprise Edition installation back to Community
Edition, you can follow [this guide][ee-ce] to make the process as smooth as
possible.
+## Version specific upgrading instructions
+
+### 12.2.0
+
+In 12.2.0, we enabled Rails' authenticated cookie encryption. Old sessions are
+automatically upgraded.
+
+However, session cookie downgrades are not supported. So after upgrading to 12.2.0,
+any downgrades would result to all sessions being invalidated and users are logged out.
+
+### 12.0.0
+
+In 12.0.0 we made various database related changes. These changes require that
+users first upgrade to the latest 11.11 patch release. Once upgraded to 11.11.x,
+users can upgrade to 12.x. Failure to do so may result in database migrations
+not being applied, which could lead to application errors.
+
+Example 1: you are currently using GitLab 11.11.3, which is the latest patch
+release for 11.11.x. You can upgrade as usual to 12.0.0, 12.1.0, etc.
+
+Example 2: you are currently using a version of GitLab 10.x. To upgrade, first
+upgrade to 11.11.3. Once upgraded to 11.11.3 you can safely upgrade to 12.0.0
+or future versions.
+
## Miscellaneous
- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index d3b0a3c2829..df35638cba2 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -47,8 +47,8 @@ sudo service gitlab stop
### 3. Update Ruby
-NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped
-support for Ruby 2.4. Be sure to upgrade if necessary.
+NOTE: Beginning in GitLab 12.2, we only support Ruby 2.6 or higher, and dropped
+support for Ruby 2.5. Be sure to upgrade if necessary.
You can check which version you are running with `ruby -v`.
@@ -378,20 +378,6 @@ Example:
Additional instructions here.
-->
-### 12.0.0
-
-In 12.0.0 we made various database related changes. These changes require that
-users first upgrade to the latest 11.11 patch release. Once upgraded to 11.11.x,
-users can upgrade to 12.x. Failure to do so may result in database migrations
-not being applied, which could lead to application errors.
-
-Example 1: you are currently using GitLab 11.11.3, which is the latest patch
-release for 11.11.x. You can upgrade as usual to 12.0.0, 12.1.0, etc.
-
-Example 2: you are currently using a version of GitLab 10.x. To upgrade, first
-upgrade to 11.11.3. Once upgraded to 11.11.3 you can safely upgrade to 12.0.0
-or future versions.
-
## Things went south? Revert to previous version
### 1. Revert the code to the previous version
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 7fbb4d84cfc..6faab685b26 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -4,6 +4,17 @@ type: reference
# Account and limit settings
+## Max attachment size
+
+You can change the maximum file size for attachments in comments and replies in GitLab.
+Navigate to **Admin Area (wrench icon) > Settings > General**, then expand **Account and Limit**.
+From here, you can increase or decrease by changing the value in `Maximum attachment size (MB)`.
+
+NOTE: **Note:**
+If you choose a size larger than what is currently configured for the web server,
+you will likely get errors. See the [troubleshooting section](#troubleshooting) for more
+details.
+
## Repository size limit **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/2016/09/22/gitlab-8-12-released/#limit-project-size-ee).
@@ -51,14 +62,18 @@ For details on manually purging files, see [reducing the repository size using G
NOTE: **Note:**
GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit).
-<!-- ## Troubleshooting
+## Troubleshooting
+
+### 413 Request Entity Too Large
+
+If you are attaching a file to a comment or reply in GitLab and receive the `413 Request Entity Too Large`
+error, it is likely caused by having a [max attachment size](#max-attachment-size)
+larger than what the web server is configured to allow.
-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.
+If you wanted to increase the max attachment size to 200m in a GitLab
+[Omnibus](https://docs.gitlab.com/omnibus/) install, for example, you might need to
+add the line below to `/etc/gitlab/gitlab.rb` before increasing the max attachment size:
-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. -->
+```
+nginx['client_max_body_size'] = "200m"
+```
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 43640f1b16a..bd76b052422 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -94,52 +94,6 @@ a group in the **Usage Quotas** page available to the group page settings list.
![Group pipelines quota](img/group_pipelines_quota.png)
-## Extra Shared Runners pipeline minutes quota **(FREE ONLY)**
-
-If you're using GitLab.com, you can purchase additional CI minutes so your
-pipelines will not be blocked after you have used all your CI minutes from your
-main quota. Additional minutes:
-
-- Are only used once the shared quota included in your subscription runs out.
-- Roll over month to month.
-
-In order to purchase additional minutes, you should follow these steps:
-
-1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**.
-
- ![Buy additional minutes](img/buy_btn.png)
-
-1. Locate the subscription card that is linked to your group on GitLab.com,
- click on **Buy more CI minutes**, and complete the details about the transaction.
-
- ![Buy additional minutes](img/buy_minutes_card.png)
-
-1. Once we have processed your payment, the extra CI minutes
- will be synced to your Group and you can visualize it from the
- **Group > Settings > Pipelines quota** page:
-
- ![Additional minutes](img/additional_minutes.png)
-
-Be aware that:
-
-1. If you have purchased extra CI minutes before the purchase of a paid plan,
- we will calculate a pro-rated charge for your paid plan. That means you may
- be charged for less than one year since your subscription was previously
- created with the extra CI minutes.
-1. Once the extra CI minutes has been assigned to a Group they cannot be transferred
- to a different Group.
-1. If you have some minutes used over your default quota, these minutes will
- be deducted from your Additional Minutes quota immediately after your purchase of additional
- minutes.
-
-## What happens when my CI minutes quota run out
-
-When the CI minutes quota run out, an email is sent automatically to notifies the owner(s) of the group/namespace which
-includes a link to [purchase more minutes](https://customers.gitlab.com/plans).
-
-If you are not the owner of the group, you will need to contact them to let them know they need to
-[purchase more minutes](https://customers.gitlab.com/plans).
-
## Archive jobs **(CORE ONLY)**
Archiving jobs is useful for reducing the CI/CD footprint on the system by
diff --git a/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png b/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png
new file mode 100644
index 00000000000..af9cee08d71
--- /dev/null
+++ b/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png
Binary files differ
diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md
new file mode 100644
index 00000000000..38c38bbd8a9
--- /dev/null
+++ b/doc/user/application_security/dependency_list/index.md
@@ -0,0 +1,49 @@
+# Dependency List **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
+
+The Dependency list allows you to see your project's dependencies, and key
+details about them, including their known vulnerabilities. To see it,
+navigate to **Security & Compliance > Dependency List** in your project's
+sidebar.
+
+## Requirements
+
+1. The [Dependency Scanning](../dependency_scanning/index.md) CI job must be
+ configured for your project.
+1. Your project uses at least one of the
+ [languages and package managers](../dependency_scanning/index.md#supported-languages-and-package-managers)
+ supported by Gemnasium.
+
+## Viewing dependencies
+
+![Dependency List](img/dependency_list_v12_2.png)
+
+Dependencies are displayed with the following information:
+
+| Field | Description |
+| --------- | ----------- |
+| Status | Displays whether or not the dependency has any known vulnerabilities |
+| Component | The dependency's name |
+| Version | The exact locked version of the dependency your project uses |
+| Packager | The packager used to install the depedency |
+| Location | A link to the packager-specific lockfile in your project that declared the dependency |
+
+Dependencies shown are initially sorted by their names. They can also be sorted
+by the packager they were installed by, or by the severity of their known
+vulnerabilities.
+
+There is a second list under the `Vulnerable components` tab displaying only
+those dependencies with known vulnerabilities. If there are none, this tab is
+disabled.
+
+### Vulnerabilities
+
+If a dependency has known vulnerabilities, they can be viewed by clicking on the
+`Status` cell of that dependency. The severity and description of each
+vulnerability will then be displayed below it.
+
+## Downloading the Dependency List
+
+Your project's full list of dependencies and their details can be downloaded in
+`JSON` format by clicking on the download button.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 10b4d9d4c7c..3148ec63c79 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -327,16 +327,11 @@ Once a vulnerability is found, you can interact with it. Read more on how to
For more information about the vulnerabilities database update, check the
[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
-## Dependency List
+## Dependency List **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
-
-An additional benefit of Dependency Scanning is the ability to get a list of your
-project's dependencies with their versions. This list can be generated only for
-[languages and package managers](#supported-languages-and-package-managers)
-supported by Gemnasium.
-
-To see the generated dependency list, navigate to your project's **Security & Compliance > Dependency List**.
+An additional benefit of Dependency Scanning is the ability to view your
+project's dependencies and their known vulnerabilities. Read more about
+the [Dependency List](../dependency_list/index.md).
## Versioning and release process
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 4dcb416c110..83ea0ea3386 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -25,6 +25,7 @@ GitLab can scan and report any vulnerabilities found in your project.
| Secure scanning tool | Description |
|:-----------------------------------------------------------------------------|:-----------------------------------------------------------------------|
| [Container Scanning](container_scanning/index.md) **(ULTIMATE)** | Scan Docker containers for known vulnerabilities. |
+| [Dependency List](dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. |
| [Dependency Scanning](dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. |
| [License Management](license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. |
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 928950126da..c9fbd7effa0 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -81,13 +81,12 @@ IP based firewall can be configured by looking up all
## Shared Runners
-Shared Runners on GitLab.com run in [autoscale mode] and powered by
-Google Cloud Platform. Autoscaling means reduced
-waiting times to spin up CI/CD jobs, and isolated VMs for each project,
-thus maximizing security.
-They're free to use for public open source projects and limited to 2000 CI
-minutes per month per group for private projects. Read about all
-[GitLab.com plans](https://about.gitlab.com/pricing/).
+Shared Runners on GitLab.com run in [autoscale mode] and powered by Google Cloud Platform.
+Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project,
+thus maximizing security. They're free to use for public open source projects and limited
+to 2000 CI minutes per month per group for private projects. More minutes
+[can be purchased](../../subscriptions/index.md#extra-shared-runners-pipeline-minutes), if
+needed. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/).
All your CI/CD jobs run on [n1-standard-1 instances](https://cloud.google.com/compute/docs/machine-types) with 3.75GB of RAM, CoreOS and the latest Docker Engine
installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
@@ -223,7 +222,7 @@ and the following environment variables:
| Setting | GitLab.com | Default |
|-------- |----------- |-------- |
-| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `1000000` |
+| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `2000000` |
| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL` | `SIGKILL` | - |
| `SIDEKIQ_LOG_ARGUMENTS` | `1` | - |
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index edb1e904f2b..6cfc8b6429b 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -533,6 +533,10 @@ This snippet links to `<wiki_root>/miscellaneous.md`:
[Link to Related Page](/miscellaneous.md)
```
+### Embedding metrics in GitLab Flavored Markdown
+
+Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details.
+
## Standard markdown and extensions in GitLab
All standard markdown formatting should work as expected within GitLab. Some standard
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index d92435ef724..16684b9f72b 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -47,6 +47,7 @@ The following table depicts the various user permission levels in a project.
| View approved/blacklisted licenses **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View license management reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Security reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
@@ -93,7 +94,7 @@ The following table depicts the various user permission levels in a project.
| Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
| Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
-| View dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
+| View vulnerabilities in Dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
| Create issue from vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
| Dismiss vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
| Apply code change suggestions | | | ✓ | ✓ | ✓ |
@@ -232,6 +233,16 @@ nested groups if you have membership in one of its parents.
To learn more, read through the documentation on
[subgroups memberships](group/subgroups/index.md#membership).
+## Guest User
+
+Create a user and assign to a project with a role as `Guest` user, this user
+will be considered as guest user by GitLab and will not take up the license.
+There is no specific `Guest` role for newly created users. If this user will
+be assigned a higher role to any of the projects and groups then this user will
+take a license seat. If a user creates a project this user becomes a maintainer,
+therefore, takes up a license seat as well, in order to prevent this you have
+to go and edit user profile and mark the user as External.
+
## External users permissions
In cases where it is desired that a user has access only to some internal or
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 248188a6457..d556daa3460 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -44,7 +44,7 @@ the following table.
| Scope | Introduced in | Description |
| ------------------ | ------------- | ----------- |
| `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed. |
-| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete access to the API and Container Registry (read/write). |
+| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. |
| `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) | Allows to read (pull) [container registry] images if a project is private and authorization is required. |
| `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an admin). |
| `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894) | Allows read-only access (pull) to the repository through git clone. |
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index 5bd4b263a58..82a6d2b3703 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -87,6 +87,16 @@ You have 8 options here that you can use for your default dashboard view:
- Assigned Merge Requests
- Operations Dashboard **(PREMIUM)**
+### Group overview content
+
+The **Group overview content** dropdown allows you to choose what information is
+displayed on a group’s home page.
+
+You can choose between 2 options:
+
+- Details (default)
+- [Security dashboard](../application_security/security_dashboard/index.md) **(ULTIMATE)**
+
### Project overview content
The project overview content setting allows you to choose what content you want to
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index ffaa07cb3a4..cf3a3fef79f 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -173,6 +173,9 @@ Starting from [GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ce/issues/55902
### Add existing Kubernetes cluster
+NOTE: **Note:**
+Kubernetes integration is not supported for arm64 clusters. See the issue [Helm Tiller fails to install on arm64 cluster](https://gitlab.com/gitlab-org/gitlab-ce/issues/64044) for details.
+
To add an existing Kubernetes cluster to your project:
1. Navigate to your project's **Operations > Kubernetes** page.
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 6707b88c317..424bee6e9f1 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -1,5 +1,7 @@
# Cycle Analytics
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) at a group level in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2 (enabled by feature flag `analytics`).
+
Cycle Analytics measures the time spent to go from an [idea to production] - also known
as cycle time - for each of your projects. Cycle Analytics displays the median time for an idea to
reach production, along with the time typically spent in each DevOps stage along the way.
@@ -13,10 +15,16 @@ calculates a separate median for each stage.
## Overview
-You can find the Cycle Analytics page under your project's **Project ➔ Cycle
-Analytics** tab.
+Cycle Analytics are available at a:
+
+- Group level from the top navigation bar **Analytics > Cycle Analytics**. **(PREMIUM)**
+
+ In the future, multiple groups will be selectable which will effectively make this an
+ instance-level feature.
+
+- Project level from a project's **Project > Cycle Analytics**.
-![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
+ ![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
There are seven stages that are tracked as part of the Cycle Analytics calculations.
@@ -134,7 +142,7 @@ A few notes:
## Permissions
-The current permissions on the Cycle Analytics dashboard are:
+The current permissions on the Project Cycle Analytics dashboard are:
- Public projects - anyone can access
- Internal projects - any authenticated user can access
@@ -142,6 +150,18 @@ The current permissions on the Cycle Analytics dashboard are:
You can [read more about permissions][permissions] in general.
+NOTE: **Note:**
+As of GitLab 12.2, the project-level page is deprecated. You should access
+project-level Cycle Analytics from **Analytics > Cycle Analytics** in the top
+navigation bar. We will ensure that the same project-level functionality is available
+to CE users in the new analytics space.
+
+For Cycle Analytics functionality introduced in GitLab 12.2 and later:
+
+- Users must have Reporter access or above.
+- Features are available only on
+ [Premium or Silver tiers](https://about.gitlab.com/pricing/) and above.
+
## More resources
Learn more about Cycle Analytics in the following resources:
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index 196874fdc86..f53dc056010 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -55,7 +55,7 @@ changes you made after picking the template and return it to its initial status.
![Description templates](img/description_templates.png)
-## Setting a default template for issues and merge requests **(STARTER)**
+## Setting a default template for merge requests and issues **(STARTER)**
> **Notes:**
>
@@ -66,20 +66,20 @@ changes you made after picking the template and return it to its initial status.
> - Templates for merge requests were [introduced][ee-7478ece] in GitLab EE 6.9.
The visibility of issues and/or merge requests should be set to either "Everyone
-with access" or "Only Project Members" in your project's **Settings** otherwise the
+with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
template text areas won't show. This is the default behavior so in most cases
you should be fine.
-Go to your project's **Settings** and fill in the "Default description template
-for issues" and "Default description template for merge requests" text areas
-for issues and merge requests respectively. Since GitLab issues and merge
-request support [Markdown](../markdown.md), you can use special markup like
+Go to your project's **Settings** and under the **Merge requests** header, click *Expand* and fill in the "Default description template
+for merge requests" text area. Under the **Default issue template**, click *Expand* and fill in "Default description template for issues" text area. Since GitLab merge request and issues
+ support [Markdown](../markdown.md), you can use special markup like
headings, lists, etc.
-![Default description templates](img/description_templates_default_settings.png)
+![Default merge request description templates](img/description_templates_merge_request_settings.png)
+![Default issue description templates](img/description_templates_issue_settings.png)
After you add the description, hit **Save changes** for the settings to take
-effect. Now, every time a new issue or merge request is created, it will be
+effect. Now, every time a new merge request or issue is created, it will be
pre-filled with the text you entered in the template(s).
## Description template example
diff --git a/doc/user/project/img/description_templates_default_settings.png b/doc/user/project/img/description_templates_default_settings.png
deleted file mode 100644
index ab314e83d06..00000000000
--- a/doc/user/project/img/description_templates_default_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/description_templates_issue_settings.png b/doc/user/project/img/description_templates_issue_settings.png
new file mode 100644
index 00000000000..53328108835
--- /dev/null
+++ b/doc/user/project/img/description_templates_issue_settings.png
Binary files differ
diff --git a/doc/user/project/img/description_templates_merge_request_settings.png b/doc/user/project/img/description_templates_merge_request_settings.png
new file mode 100644
index 00000000000..eda264f7f37
--- /dev/null
+++ b/doc/user/project/img/description_templates_merge_request_settings.png
Binary files differ
diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md
index 5abce39c093..e509e333313 100644
--- a/doc/user/project/import/bitbucket.md
+++ b/doc/user/project/import/bitbucket.md
@@ -31,12 +31,12 @@ to enable this if not already.
## How it works
When issues/pull requests are being imported, the Bitbucket importer tries to find
-the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
-to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
-and **associated their Bitbucket account**. If the user is not
-found in GitLab's database, the project creator (most of the times the current
-user that started the import process) is set as the author, but a reference on
-the issue about the original Bitbucket author is kept.
+the Bitbucket author/assignee in GitLab's database using the Bitbucket `nickname`.
+For this to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
+and **associated their Bitbucket account**. Their `nickname` must also match their Bitbucket
+`username.`. If the user is not found in GitLab's database, the project creator
+(most of the times the current user that started the import process) is set as the author,
+but a reference on the issue about the original Bitbucket author is kept.
The importer will create any new namespaces (groups) if they don't exist or in
the case the namespace is taken, the repository will be imported under the user's
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 45e96437517..30ff0e9ff07 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -99,6 +99,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [NPM packages](packages/npm_registry.md): your private NPM package registry in GitLab. **(PREMIUM)**
- [Code owners](code_owners.md): specify code owners for certain files **(STARTER)**
- [License Management](../application_security/license_management/index.md): approve and blacklist licenses for projects. **(ULTIMATE)**
+- [Dependency List](../application_security/dependency_list/index.md): view project dependencies. **(ULTIMATE)**
### Project integrations
diff --git a/doc/user/project/integrations/img/embed_metrics.png b/doc/user/project/integrations/img/embed_metrics.png
new file mode 100644
index 00000000000..6f9660c9aec
--- /dev/null
+++ b/doc/user/project/integrations/img/embed_metrics.png
Binary files differ
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index ea58a08e127..6e0f39956d3 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -14,7 +14,7 @@ To enable Mattermost integration you must create an incoming webhook integration
1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
-it on `https://mattermost.example/admin_console/integrations/custom`.
+it on **Mattermost System Console > Integrations > Integration Management**, or on **Mattermost System Console > Integrations > Custom Integrations** in Mattermost versions 5.11 and earlier.
Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 44439b59e77..aa7db97c413 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -354,6 +354,27 @@ Prometheus server.
![Merge Request with Performance Impact](img/merge_request_performance.png)
+## Embedding metric charts within Gitlab Flavored Markdown
+
+> [Introduced][ce-29691] in GitLab 12.2.
+> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics.
+
+It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
+
+To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`.
+
+The following requirements must be met for the metric to unfurl:
+
+- The `<environment_id>` must correspond to a real environment.
+- Prometheus must be monitoring the environment.
+- The GitLab instance must be configured to receive data from the environment.
+- The user must be allowed access to the monitoring dashboard for the environment ([Reporter or higher](../../permissions.md)).
+- The dashboard must have data within the last 8 hours.
+
+ If all of the above are true, then the metric will unfurl as seen below:
+
+![Embedded Metrics](img/embed_metrics.png)
+
## Troubleshooting
If the "No data found" screen continues to appear, it could be due to:
@@ -376,4 +397,5 @@ If the "No data found" screen continues to appear, it could be due to:
[ci-environment-slug]: ../../../ci/variables/#predefined-environment-variables
[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
+[ce-29691]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29691
[promgldocs]: ../../../administration/monitoring/prometheus/index.md
diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md
index 7b031f83cb1..d7d168710ef 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -50,7 +50,12 @@ The button to do this has a different label depending on whether the issue is al
#### 3. Assignee
-An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER).
+An issue can be assigned to:
+
+- Yourself.
+- Another person.
+- [Many people](#31-multiple-assignees-STARTER). **(STARTER)**
+
The assignee(s) can be changed as often as needed. The idea is that the assignees are
responsible for that issue until it's reassigned to someone else to take it from there.
When assigned to someone, it will appear in their assigned issues list.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 7ff30d1b813..4c8bd230b3f 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -287,6 +287,8 @@ as pushing changes:
- Set the target of the merge request to a particular branch.
- Set the merge request to merge when its pipeline succeeds.
- Set the merge request to remove the source branch when it's merged.
+- Set the title of the merge request to a particular title.
+- Set the description of the merge request to a particular description.
### Create a new merge request using git push options
@@ -332,6 +334,8 @@ git push -o merge_request.create -o merge_request.merge_when_pipeline_succeeds
### Set removing the source branch using git push options
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2.
+
To set an existing merge request to remove the source branch when the
merge request is merged, the
`merge_request.remove_source_branch` push option can be used:
@@ -345,6 +349,8 @@ You can also use this push option in addition to the
### Set merge request title using git push options
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2.
+
To set the title of an existing merge request, use
the `merge_request.title` push option:
@@ -357,6 +363,8 @@ You can also use this push option in addition to the
### Set merge request description using git push options
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2.
+
To set the description of an existing merge request, use
the `merge_request.description` push option:
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 509576c8d6f..5161b25de99 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -229,12 +229,6 @@ The default approval settings can now be overridden when creating a
in the **No. approvals required** box.
1. Click **Update approvers**.
-There are however some restrictions:
-
-- The amount of required approvals, if changed, must be greater than the default
- set at the project level. This ensures that you're not forced to adjust settings
- when someone is unavailable for approval, yet the process is still enforced.
-
NOTE: **Note:**
If you are contributing to a forked project, things are a little different.
Read what happens when the
@@ -242,14 +236,9 @@ Read what happens when the
## Overriding merge request approvals default settings **(PREMIUM)**
-In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), and
-**Can override approvers and approvals required per merge request** is checked, there are a few more
-restrictions (compared to [GitLab Starter](#overriding-the-merge-request-approvals-default-settings)):
-
-- Approval rules can be added to an MR with no restriction.
-- For project sourced approval rules, editing and removing approvers is not allowed.
-- The approvals required of all approval rules is configurable, but if a rule is backed by a project rule, then it is restricted
- to the minimum approvals required set in the project's corresponding rule.
+In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium),
+and **Can override approvers and approvals required per merge request** is checked,
+approval rules can be added to an MR with no restriction.
## Resetting approvals on push
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 4e25d8545e9..f3e9c950efd 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -6,7 +6,9 @@ type: reference, howto
> - Introduced in GitLab 9.1 as [Trigger Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533).
> - [Renamed to Pipeline Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853) in GitLab 9.2.
-> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit).
+
+NOTE: **Note:**
+Cron notation is parsed by [Fugit](https://github.com/floraison/fugit).
Pipelines are normally run based on certain conditions being met. For example, when a branch is pushed to repository.
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index cc62ce22a1b..6c1acc3963f 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -4,6 +4,7 @@ module API
class Discussions < Grape::API
include PaginationParams
helpers ::API::Helpers::NotesHelpers
+ helpers ::RendersNotes
before { authenticate! }
@@ -23,21 +24,15 @@ module API
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
use :pagination
end
- # rubocop: disable CodeReuse/ActiveRecord
+
get ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
- notes = noteable.notes
- .inc_relations_for_view
- .includes(:noteable)
- .fresh
-
- notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ notes = readable_discussion_notes(noteable)
discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable))
present paginate(discussions), with: Entities::Discussion
end
- # rubocop: enable CodeReuse/ActiveRecord
desc "Get a single #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
@@ -226,13 +221,24 @@ module API
helpers do
# rubocop: disable CodeReuse/ActiveRecord
- def readable_discussion_notes(noteable, discussion_id)
+ def readable_discussion_notes(noteable, discussion_id = nil)
notes = noteable.notes
- .where(discussion_id: discussion_id)
+ notes = notes.where(discussion_id: discussion_id) if discussion_id
+ notes = notes
.inc_relations_for_view
.includes(:noteable)
.fresh
+ # Without RendersActions#prepare_notes_for_rendering,
+ # Note#cross_reference_not_visible_for? will attempt to render
+ # Markdown references mentioned in the note to see whether they
+ # should be redacted. For notes that reference a commit, this
+ # would also incur a Gitaly call to verify the commit exists.
+ #
+ # With prepare_notes_for_rendering, we can avoid Gitaly calls
+ # because notes are redacted if they point to projects that
+ # cannot be accessed by the user.
+ notes = prepare_notes_for_rendering(notes)
notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 196ef1fcdfa..c36ee5af63f 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -125,6 +125,12 @@ module API
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated"
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
+ optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
+ given snowplow_enabled: ->(val) { val } do
+ requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname'
+ optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
+ optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic'
+ end
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 7260ecfb5ee..404675bfaec 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -13,6 +13,13 @@ module API
'issues' => ->(iid) { find_project_issue(iid) }
}.freeze
+ helpers do
+ # EE::API::Todos would override this method
+ def find_todos
+ TodosFinder.new(current_user, params).execute
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -41,10 +48,6 @@ module API
resource :todos do
helpers do
- def find_todos
- TodosFinder.new(current_user, params).execute
- end
-
def issuable_and_awardable?(type)
obj_type = Object.const_get(type)
@@ -107,3 +110,5 @@ module API
end
end
end
+
+API::Todos.prepend_if_ee('EE::API::Todos')
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
index 0120cc37d6f..c5a328c21b2 100644
--- a/lib/banzai/filter/inline_metrics_filter.rb
+++ b/lib/banzai/filter/inline_metrics_filter.rb
@@ -15,17 +15,6 @@ module Banzai
)
end
- # Endpoint FE should hit to collect the appropriate
- # chart information
- def metrics_dashboard_url(params)
- Gitlab::Metrics::Dashboard::Url.build_dashboard_url(
- params['namespace'],
- params['project'],
- params['environment'],
- embedded: true
- )
- end
-
# Search params for selecting metrics links. A few
# simple checks is enough to boost performance without
# the cost of doing a full regex match.
@@ -38,6 +27,28 @@ module Banzai
def link_pattern
Gitlab::Metrics::Dashboard::Url.regex
end
+
+ private
+
+ # Endpoint FE should hit to collect the appropriate
+ # chart information
+ def metrics_dashboard_url(params)
+ Gitlab::Metrics::Dashboard::Url.build_dashboard_url(
+ params['namespace'],
+ params['project'],
+ params['environment'],
+ embedded: true,
+ **query_params(params['url'])
+ )
+ end
+
+ # Parses query params out from full url string into hash.
+ #
+ # Ex) 'https://<root>/<project>/<environment>/metrics?title=Title&group=Group'
+ # --> { title: 'Title', group: 'Group' }
+ def query_params(url)
+ Gitlab::Metrics::Dashboard::Url.parse_query(url)
+ end
end
end
end
diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb
index 1b8dc27793a..127598a76ab 100644
--- a/lib/bitbucket/representation/comment.rb
+++ b/lib/bitbucket/representation/comment.rb
@@ -4,7 +4,7 @@ module Bitbucket
module Representation
class Comment < Representation::Base
def author
- user['username']
+ user['nickname']
end
def note
diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb
index a88797cdab9..3f6db9cb75b 100644
--- a/lib/bitbucket/representation/issue.rb
+++ b/lib/bitbucket/representation/issue.rb
@@ -14,7 +14,7 @@ module Bitbucket
end
def author
- raw.dig('reporter', 'username')
+ raw.dig('reporter', 'nickname')
end
def description
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
index 6a0e8b354bf..a498c9bc213 100644
--- a/lib/bitbucket/representation/pull_request.rb
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -4,7 +4,7 @@ module Bitbucket
module Representation
class PullRequest < Representation::Base
def author
- raw.fetch('author', {}).fetch('username', nil)
+ raw.fetch('author', {}).fetch('nickname', nil)
end
def description
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index ef41dc560c9..ebea84fa1ca 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -6,6 +6,9 @@ module ContainerRegistry
attr_reader :repository, :name
+ # https://github.com/docker/distribution/commit/3150937b9f2b1b5b096b2634d0e7c44d4a0f89fb
+ TAG_NAME_REGEX = /^[\w][\w.-]{0,127}$/.freeze
+
delegate :registry, :client, to: :repository
delegate :revision, :short_revision, to: :config_blob, allow_nil: true
@@ -13,6 +16,10 @@ module ContainerRegistry
@repository, @name = repository, name
end
+ def valid_name?
+ !name.match(TAG_NAME_REGEX).nil?
+ end
+
def valid?
manifest.present?
end
diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb
index c83cec9dc4a..45af30f46dc 100644
--- a/lib/expand_variables.rb
+++ b/lib/expand_variables.rb
@@ -3,6 +3,20 @@
module ExpandVariables
class << self
def expand(value, variables)
+ variables_hash = nil
+
+ value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
+ variables_hash ||= transform_variables(variables)
+ variables_hash[$1 || $2]
+ end
+ end
+
+ private
+
+ def transform_variables(variables)
+ # Lazily initialise variables
+ variables = variables.call if variables.is_a?(Proc)
+
# Convert hash array to variables
if variables.is_a?(Array)
variables = variables.reduce({}) do |hash, variable|
@@ -11,9 +25,7 @@ module ExpandVariables
end
end
- value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
- variables[$1 || $2]
- end
+ variables
end
end
end
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index edfd2fb17f3..9ded1aed4e3 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -7,7 +7,7 @@ class Feature
# Server feature flags should use '_' to separate words.
SERVER_FEATURE_FLAGS =
[
- 'get_commit_signatures'.freeze
+ # 'get_commit_signatures'.freeze
].freeze
DEFAULT_ON_FLAGS = Set.new([]).freeze
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 8047ef4fa15..24bc73e0de5 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -262,13 +262,19 @@ module Gitlab
def pull_request_comment_attributes(comment)
{
project: project,
- note: comment.note,
author_id: gitlab_user_id(project, comment.author),
+ note: comment_note(comment),
created_at: comment.created_at,
updated_at: comment.updated_at
}
end
+ def comment_note(comment)
+ author = @formatter.author_line(comment.author) unless find_user_id(comment.author)
+
+ author.to_s + comment.note.to_s
+ end
+
def log_error(details)
logger.error(log_base_data.merge(details))
end
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 382b8896dbd..b7886114e9c 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -218,7 +218,7 @@ module Gitlab
return if @sections.include?(section)
@sections << section
- write_raw %{<div class="js-section-start fa fa-caret-down pr-2 cursor-pointer" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>}
+ write_raw %{<div class="js-section-start section-start fa fa-caret-down pr-2 cursor-pointer" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>}
@lineno_in_section = 0
end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 2fd76bc3690..29a52b9da17 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -16,8 +16,11 @@ module Gitlab
dependencies needs before_script after_script variables
environment coverage retry parallel extends].freeze
+ REQUIRED_BY_NEEDS = %i[stage].freeze
+
validations do
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs?
validates :config, presence: true
validates :script, presence: true
validates :name, presence: true
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 0405292a25b..65029f5ce7f 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -23,12 +23,17 @@ module Gitlab
@command.seeds_block&.call(pipeline)
##
- # Populate pipeline with all stages, and stages with builds.
+ # Gather all runtime build/stage errors
#
- pipeline.stage_seeds.each do |stage|
- pipeline.stages << stage.to_resource
+ if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence
+ return error(seeds_errors.join("\n"))
end
+ ##
+ # Populate pipeline with all stages, and stages with builds.
+ #
+ pipeline.stages = pipeline.stage_seeds.map(&:to_resource)
+
if pipeline.stages.none?
return error('No stages / jobs for this pipeline.')
end
diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb
index 1fd3a61017f..e9e22569ae0 100644
--- a/lib/gitlab/ci/pipeline/seed/base.rb
+++ b/lib/gitlab/ci/pipeline/seed/base.rb
@@ -13,6 +13,10 @@ module Gitlab
raise NotImplementedError
end
+ def errors
+ raise NotImplementedError
+ end
+
def to_resource
raise NotImplementedError
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index ab0d4c38ab6..b0ce7457926 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -9,10 +9,15 @@ module Gitlab
delegate :dig, to: :@attributes
+ # When the `ci_dag_limit_needs` is enabled it uses the lower limit
+ LOW_NEEDS_LIMIT = 5
+ HARD_NEEDS_LIMIT = 50
+
def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
@attributes = attributes
@previous_stages = previous_stages
+ @needs_attributes = dig(:needs_attributes)
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@@ -27,8 +32,15 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
all_of_only? &&
- none_of_except? &&
- all_of_needs?
+ none_of_except?
+ end
+ end
+
+ def errors
+ return unless included?
+
+ strong_memoize(:errors) do
+ needs_errors
end
end
@@ -48,6 +60,18 @@ module Gitlab
@attributes.to_h.dig(:options, :trigger).present?
end
+ def to_resource
+ strong_memoize(:resource) do
+ if bridge?
+ ::Ci::Bridge.new(attributes)
+ else
+ ::Ci::Build.new(attributes)
+ end
+ end
+ end
+
+ private
+
def all_of_only?
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
end
@@ -56,24 +80,30 @@ module Gitlab
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
end
- def all_of_needs?
- return true unless Feature.enabled?(:ci_dag_support, @pipeline.project)
- return true if dig(:needs_attributes).nil?
+ def needs_errors
+ return if @needs_attributes.nil?
+
+ if @needs_attributes.size > max_needs_allowed
+ return [
+ "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \
+ "See needs keyword documentation for more details"
+ ]
+ end
- dig(:needs_attributes).all? do |need|
- @previous_stages.any? do |stage|
+ @needs_attributes.flat_map do |need|
+ result = @previous_stages.any? do |stage|
stage.seeds_names.include?(need[:name])
end
- end
+
+ "#{name}: needs '#{need[:name]}'" unless result
+ end.compact
end
- def to_resource
- strong_memoize(:resource) do
- if bridge?
- ::Ci::Bridge.new(attributes)
- else
- ::Ci::Build.new(attributes)
- end
+ def max_needs_allowed
+ if Feature.enabled?(:ci_dag_limit_needs, @project, default_enabled: true)
+ LOW_NEEDS_LIMIT
+ else
+ HARD_NEEDS_LIMIT
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 7c737027445..b600df2f656 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -33,6 +33,12 @@ module Gitlab
end
end
+ def errors
+ strong_memoize(:errors) do
+ seeds.flat_map(&:errors).compact
+ end
+ end
+
def seeds_names
strong_memoize(:seeds_names) do
seeds.map(&:name).to_set
diff --git a/lib/gitlab/cluster/puma_worker_killer_observer.rb b/lib/gitlab/cluster/puma_worker_killer_observer.rb
index 3b4ebc3fbae..f53051c32ff 100644
--- a/lib/gitlab/cluster/puma_worker_killer_observer.rb
+++ b/lib/gitlab/cluster/puma_worker_killer_observer.rb
@@ -15,9 +15,7 @@ module Gitlab
private
def log_termination(worker)
- labels = { worker: "worker_#{worker.index}" }
-
- @counter.increment(labels)
+ @counter.increment
end
end
end
diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb
index 560fe63df0e..87bd257f69a 100644
--- a/lib/gitlab/config/entry/attributable.rb
+++ b/lib/gitlab/config/entry/attributable.rb
@@ -18,6 +18,10 @@ module Gitlab
config[attribute]
end
+
+ define_method("has_#{attribute}?") do
+ config.is_a?(Hash) && config.key?(attribute)
+ end
end
end
end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 6796fcce75f..0289e675c6b 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -26,6 +26,17 @@ module Gitlab
end
end
+ class RequiredKeysValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ present_keys = options[:in] - value.try(:keys).to_a
+
+ if present_keys.any?
+ record.errors.add(attribute, "missing required keys: " +
+ present_keys.join(', '))
+ end
+ end
+ end
+
class AllowedValuesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless options[:in].include?(value.to_s)
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index c0a12318990..332ca8bf9b8 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -113,7 +113,7 @@ module Gitlab
yarn\.lock
)\z}x => :frontend,
- %r{\A(ee/)?db/} => :database,
+ %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database,
%r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database,
%r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
%r{\Arubocop/cop/migration(/|\.rb)} => :database,
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 40bda3410e1..37fadb47736 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -60,7 +60,8 @@ module Gitlab
# rubocop:disable Metrics/ParameterLists
def build(
project:, user:, ref:, oldrev: nil, newrev: nil,
- commits: [], commits_count: nil, message: nil, push_options: {})
+ commits: [], commits_count: nil, message: nil, push_options: {},
+ with_changed_files: true)
commits = Array(commits)
@@ -75,7 +76,7 @@ module Gitlab
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259
commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do
commits_limited.map do |commit|
- commit.hook_attrs(with_changed_files: true)
+ commit.hook_attrs(with_changed_files: with_changed_files)
end
end
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index d98b85fecc4..24d752b8a4b 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -27,6 +27,18 @@ module Gitlab
end
end
+ def includes_branches?
+ enum_for(:changes_refs).any? do |_oldrev, _newrev, ref|
+ Gitlab::Git.branch_ref?(ref)
+ end
+ end
+
+ def includes_tags?
+ enum_for(:changes_refs).any? do |_oldrev, _newrev, ref|
+ Gitlab::Git.tag_ref?(ref)
+ end
+ end
+
private
def deserialize_changes(changes)
diff --git a/lib/gitlab/gitaly_client/notification_service.rb b/lib/gitlab/gitaly_client/notification_service.rb
deleted file mode 100644
index 873c3e4086d..00000000000
--- a/lib/gitlab/gitaly_client/notification_service.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module GitalyClient
- class NotificationService
- # 'repository' is a Gitlab::Git::Repository
- def initialize(repository)
- @gitaly_repo = repository.gitaly_repository
- @storage = repository.storage
- end
-
- def post_receive
- GitalyClient.call(
- @storage,
- :notification_service,
- :post_receive,
- Gitaly::PostReceiveRequest.new(repository: @gitaly_repo)
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index f3888857bb6..bd0f3e70749 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -37,11 +37,11 @@ project_tree:
- :user
- merge_requests:
- :metrics
- - :suggestions
- notes:
- :author
- events:
- :push_event_payload
+ - :suggestions
- merge_request_diff:
- :merge_request_diff_commits
- :merge_request_diff_files
@@ -137,6 +137,7 @@ excluded_attributes:
- :packages_enabled
- :mirror_last_update_at
- :mirror_last_successful_update_at
+ - :emails_disabled
namespaces:
- :runners_token
- :runners_token_encrypted
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 42c4745ff98..6e4286589c1 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -3,8 +3,8 @@
module Gitlab
module Kubernetes
module Helm
- HELM_VERSION = '2.12.3'.freeze
- KUBECTL_VERSION = '1.11.7'.freeze
+ HELM_VERSION = '2.14.3'.freeze
+ KUBECTL_VERSION = '1.11.10'.freeze
NAMESPACE = 'gitlab-managed-apps'.freeze
SERVICE_ACCOUNT = 'tiller'.freeze
CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze
diff --git a/lib/gitlab/kubernetes/helm/reset_command.rb b/lib/gitlab/kubernetes/helm/reset_command.rb
index 37e1d8573ab..a35ffa34c58 100644
--- a/lib/gitlab/kubernetes/helm/reset_command.rb
+++ b/lib/gitlab/kubernetes/helm/reset_command.rb
@@ -38,9 +38,9 @@ module Gitlab
# Tracking this method to be removed here:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/52791#note_199374155
def delete_tiller_replicaset
- command = %w[kubectl delete replicaset -n gitlab-managed-apps -l name=tiller]
+ delete_args = %w[replicaset -n gitlab-managed-apps -l name=tiller]
- command.shelljoin
+ Gitlab::Kubernetes::KubectlCmd.delete(*delete_args)
end
def reset_helm_command
diff --git a/lib/gitlab/kubernetes/kubectl_cmd.rb b/lib/gitlab/kubernetes/kubectl_cmd.rb
new file mode 100644
index 00000000000..981eb5681dc
--- /dev/null
+++ b/lib/gitlab/kubernetes/kubectl_cmd.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ module KubectlCmd
+ class << self
+ def delete(*args)
+ %w(kubectl delete).concat(args).shelljoin
+ end
+
+ def apply_file(filename, *args)
+ raise ArgumentError, "filename is not present" unless filename.present?
+
+ %w(kubectl apply -f).concat([filename], args).shelljoin
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index b197e7ca86b..94f8b2e02b1 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -21,14 +21,26 @@ module Gitlab
\/(?<environment>\d+)
\/metrics
(?<query>
- \?[a-z0-9_=-]+
- (&[a-z0-9_=-]+)*
+ \?[a-zA-Z0-9%.()+_=-]+
+ (&[a-zA-Z0-9%.()+_=-]+)*
)?
(?<anchor>\#[a-z0-9_-]+)?
)
}x
end
+ # Parses query params out from full url string into hash.
+ #
+ # Ex) 'https://<root>/<project>/<environment>/metrics?title=Title&group=Group'
+ # --> { title: 'Title', group: 'Group' }
+ def parse_query(url)
+ query_string = URI.parse(url).query.to_s
+
+ CGI.parse(query_string)
+ .transform_values { |value| value.first }
+ .symbolize_keys
+ end
+
# Builds a metrics dashboard url based on the passed in arguments
def build_dashboard_url(*args)
Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args)
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index 4e835f37c04..8a24d4f3663 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -15,7 +15,6 @@ module Gitlab
puma_workers: ::Gitlab::Metrics.gauge(:puma_workers, 'Total number of workers'),
puma_running_workers: ::Gitlab::Metrics.gauge(:puma_running_workers, 'Number of active workers'),
puma_stale_workers: ::Gitlab::Metrics.gauge(:puma_stale_workers, 'Number of stale workers'),
- puma_phase: ::Gitlab::Metrics.gauge(:puma_phase, 'Phase number (increased during phased restarts)'),
puma_running: ::Gitlab::Metrics.gauge(:puma_running, 'Number of running threads'),
puma_queued_connections: ::Gitlab::Metrics.gauge(:puma_queued_connections, 'Number of connections in that worker\'s "todo" set waiting for a worker thread'),
puma_active_connections: ::Gitlab::Metrics.gauge(:puma_active_connections, 'Number of threads processing a request'),
@@ -54,7 +53,6 @@ module Gitlab
last_status = worker['last_status']
labels = { worker: "worker_#{worker['index']}" }
- metrics[:puma_phase].set(labels, worker['phase'])
set_worker_metrics(last_status, labels) if last_status.present?
end
end
@@ -76,7 +74,6 @@ module Gitlab
metrics[:puma_workers].set(labels, stats['workers'])
metrics[:puma_running_workers].set(labels, stats['booted_workers'])
metrics[:puma_stale_workers].set(labels, stats['old_workers'])
- metrics[:puma_phase].set(labels, stats['phase'])
end
def set_worker_metrics(stats, labels = {})
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 5e77d31760d..2669adb8455 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -29,6 +29,21 @@ module Gitlab
end
end
+ def formatted_count(scope)
+ case scope
+ when 'blobs'
+ blobs_count.to_s
+ when 'notes'
+ formatted_limited_count(limited_notes_count)
+ when 'wiki_blobs'
+ wiki_blobs_count.to_s
+ when 'commits'
+ commits_count.to_s
+ else
+ super
+ end
+ end
+
def users
super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord
end
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index dbf469a44c1..fa1d1203842 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -24,6 +24,14 @@ module Gitlab
"#{preview}.git"
end
+ def project_path
+ URI.parse(preview).path.sub(%r{\A/}, '')
+ end
+
+ def uri_encoded_project_path
+ ERB::Util.url_encode(project_path)
+ end
+
def ==(other)
name == other.name && title == other.title
end
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index b975a967d03..e5d99ebee35 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -145,9 +145,9 @@ module Gitlab
run_label_command(labels: find_labels(labels_param), command: :relabel, updates_key: :label_ids)
end
- desc _('Add a todo')
- explanation _('Adds a todo.')
- execution_message _('Added a todo.')
+ desc _('Add a To Do')
+ explanation _('Adds a To Do.')
+ execution_message _('Added a To Do.')
types Issuable
condition do
quick_action_target.persisted? &&
@@ -157,9 +157,9 @@ module Gitlab
@updates[:todo_event] = 'add'
end
- desc _('Mark to do as done')
- explanation _('Marks to do as done.')
- execution_message _('Marked to do as done.')
+ desc _('Mark To Do as done')
+ explanation _('Marks To Do as done.')
+ execution_message _('Marked To Do as done.')
types Issuable
condition do
quick_action_target.persisted? &&
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index 0868bd85600..da28fbf5be0 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -57,19 +57,18 @@ module Gitlab
labels = find_labels(target_list_name)
label_ids = labels.map(&:id)
- if label_ids.size == 1
+ if label_ids.size > 1
+ message = _('Failed to move this issue because only a single label can be provided.')
+ elsif !Label.on_project_board?(quick_action_target.project_id, label_ids.first)
+ message = _('Failed to move this issue because label was not found.')
+ else
label_id = label_ids.first
- # Ensure this label corresponds to a list on the board
- next unless Label.on_project_board?(quick_action_target.project_id, label_id)
-
@updates[:remove_label_ids] =
quick_action_target.labels.on_project_boards(quick_action_target.project_id).where.not(id: label_id).pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
@updates[:add_label_ids] = [label_id]
message = _("Moved issue to %{label} column in the board.") % { label: labels_to_reference(labels).first }
- else
- message = _('Move this issue failed because you need to specify only one label.')
end
@execution_message[:board_move] = message
@@ -93,7 +92,7 @@ module Gitlab
message = _("Marked this issue as a duplicate of %{duplicate_param}.") % { duplicate_param: duplicate_param }
else
- message = _('Mark as duplicate failed because referenced issue was not found')
+ message = _('Failed to mark this issue as a duplicate because referenced issue was not found.')
end
@execution_message[:duplicate] = message
@@ -117,18 +116,18 @@ module Gitlab
message = _("Moved this issue to %{path_to_project}.") % { path_to_project: target_project_path }
else
- message = _("Move this issue failed because target project doesn't exists")
+ message = _("Failed to move this issue because target project doesn't exist.")
end
@execution_message[:move] = message
end
- desc _('Make issue confidential.')
+ desc _('Make issue confidential')
explanation do
- _('Makes this issue confidential')
+ _('Makes this issue confidential.')
end
execution_message do
- _('Made this issue confidential')
+ _('Made this issue confidential.')
end
types Issue
condition do
@@ -138,19 +137,19 @@ module Gitlab
@updates[:confidential] = true
end
- desc _('Create a merge request.')
+ desc _('Create a merge request')
explanation do |branch_name = nil|
if branch_name
- _("Creates branch '%{branch_name}' and a merge request to resolve this issue") % { branch_name: branch_name }
+ _("Creates branch '%{branch_name}' and a merge request to resolve this issue.") % { branch_name: branch_name }
else
- _('Creates a branch and a merge request to resolve this issue')
+ _('Creates a branch and a merge request to resolve this issue.')
end
end
execution_message do |branch_name = nil|
if branch_name
- _("Created branch '%{branch_name}' and a merge request to resolve this issue") % { branch_name: branch_name }
+ _("Created branch '%{branch_name}' and a merge request to resolve this issue.") % { branch_name: branch_name }
else
- _('Created a branch and a merge request to resolve this issue')
+ _('Created a branch and a merge request to resolve this issue.')
end
end
params "<branch name>"
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index 41ffd51cde8..533c74ba9b4 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -24,7 +24,7 @@ module Gitlab
end
command :assign do |users|
if users.empty?
- @execution_message[:assign] = _("Assign command failed because no user was found")
+ @execution_message[:assign] = _("Failed to assign a user because no user was found.")
next
end
@@ -211,8 +211,8 @@ module Gitlab
end
desc _("Lock the discussion")
- explanation _("Locks the discussion")
- execution_message _("Locked the discussion")
+ explanation _("Locks the discussion.")
+ execution_message _("Locked the discussion.")
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
@@ -224,8 +224,8 @@ module Gitlab
end
desc _("Unlock the discussion")
- explanation _("Unlocks the discussion")
- execution_message _("Unlocked the discussion")
+ explanation _("Unlocks the discussion.")
+ execution_message _("Unlocked the discussion.")
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 7c1e6b1baff..ce4c1611687 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -43,6 +43,29 @@ module Gitlab
without_count ? collection.without_count : collection
end
+ def formatted_count(scope)
+ case scope
+ when 'projects'
+ formatted_limited_count(limited_projects_count)
+ when 'issues'
+ formatted_limited_count(limited_issues_count)
+ when 'merge_requests'
+ formatted_limited_count(limited_merge_requests_count)
+ when 'milestones'
+ formatted_limited_count(limited_milestones_count)
+ when 'users'
+ formatted_limited_count(limited_users_count)
+ end
+ end
+
+ def formatted_limited_count(count)
+ if count >= COUNT_LIMIT
+ "#{COUNT_LIMIT - 1}+"
+ else
+ count.to_s
+ end
+ end
+
def limited_projects_count
@limited_projects_count ||= limited_count(projects)
end
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index e360b552f89..ac3b219e0c7 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -22,6 +22,17 @@ module Gitlab
end
end
+ def formatted_count(scope)
+ case scope
+ when 'snippet_titles'
+ snippet_titles_count.to_s
+ when 'snippet_blobs'
+ snippet_blobs_count.to_s
+ else
+ super
+ end
+ end
+
def snippet_titles_count
@snippet_titles_count ||= snippet_titles.count
end
diff --git a/lib/gitlab/snowplow_tracker.rb b/lib/gitlab/snowplow_tracker.rb
new file mode 100644
index 00000000000..9f12513e09e
--- /dev/null
+++ b/lib/gitlab/snowplow_tracker.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'snowplow-tracker'
+
+module Gitlab
+ module SnowplowTracker
+ NAMESPACE = 'cf'
+
+ class << self
+ def track_event(category, action, label: nil, property: nil, value: nil, context: nil)
+ tracker&.track_struct_event(category, action, label, property, value, context, Time.now.to_i)
+ end
+
+ private
+
+ def tracker
+ return unless enabled?
+
+ @tracker ||= ::SnowplowTracker::Tracker.new(emitter, subject, NAMESPACE, Gitlab::CurrentSettings.snowplow_site_id)
+ end
+
+ def subject
+ ::SnowplowTracker::Subject.new
+ end
+
+ def emitter
+ ::SnowplowTracker::Emitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname)
+ end
+
+ def enabled?
+ Gitlab::CurrentSettings.snowplow_enabled?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 7e3a695e52a..038553c5dd7 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -142,7 +142,9 @@ module Gitlab
Gitlab::UsageDataCounters::WebIdeCounter,
Gitlab::UsageDataCounters::NoteCounter,
Gitlab::UsageDataCounters::SnippetCounter,
- Gitlab::UsageDataCounters::SearchCounter
+ Gitlab::UsageDataCounters::SearchCounter,
+ Gitlab::UsageDataCounters::CycleAnalyticsCounter,
+ Gitlab::UsageDataCounters::SourceCodeCounter
]
end
diff --git a/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb
new file mode 100644
index 00000000000..1ff4296ef65
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Gitlab::UsageDataCounters
+ class CycleAnalyticsCounter < BaseCounter
+ KNOWN_EVENTS = %w[views].freeze
+ PREFIX = 'cycle_analytics'
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/source_code_counter.rb b/lib/gitlab/usage_data_counters/source_code_counter.rb
new file mode 100644
index 00000000000..8a1771a7bd1
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/source_code_counter.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Gitlab::UsageDataCounters
+ class SourceCodeCounter < BaseCounter
+ KNOWN_EVENTS = %w[pushes].freeze
+ PREFIX = 'source_code'
+ end
+end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index 8267c235a7f..fdcd34320b1 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -40,7 +40,6 @@ namespace :gitlab do
templates.each do |template|
params = {
- import_url: template.clone_url,
namespace_id: tmp_namespace.id,
path: template.name,
skip_wiki: true
@@ -53,22 +52,46 @@ namespace :gitlab do
raise "Failed to create project: #{project.errors.messages}"
end
- loop do
- if project.import_finished?
- puts "Import finished for #{template.name}"
- break
+ uri_encoded_project_path = template.uri_encoded_project_path
+
+ # extract a concrete commit for signing off what we actually downloaded
+ # this way we do the right thing even if the repository gets updated in the meantime
+ get_commits_response = Gitlab::HTTP.get("https://gitlab.com/api/v4/projects/#{uri_encoded_project_path}/repository/commits",
+ query: { page: 1, per_page: 1 }
+ )
+ raise "Failed to retrieve latest commit for template '#{template.name}'" unless get_commits_response.success?
+
+ commit_sha = get_commits_response.parsed_response.dig(0, 'id')
+
+ project_archive_uri = "https://gitlab.com/api/v4/projects/#{uri_encoded_project_path}/repository/archive.tar.gz?sha=#{commit_sha}"
+ commit_message = <<~MSG
+ Initialized from '#{template.title}' project template
+
+ Template repository: #{template.preview}
+ Commit SHA: #{commit_sha}
+ MSG
+
+ Dir.mktmpdir do |tmpdir|
+ Dir.chdir(tmpdir) do
+ Gitlab::TaskHelpers.run_command!(['wget', project_archive_uri, '-O', 'archive.tar.gz'])
+ Gitlab::TaskHelpers.run_command!(['tar', 'xf', 'archive.tar.gz'])
+ extracted_project_basename = Dir['*/'].first
+ Dir.chdir(extracted_project_basename) do
+ Gitlab::TaskHelpers.run_command!(%w(git init))
+ Gitlab::TaskHelpers.run_command!(%w(git add .))
+ Gitlab::TaskHelpers.run_command!(['git', 'commit', '--author', 'GitLab <root@localhost>', '--message', commit_message])
+
+ # Hacky workaround to push to the project in a way that works with both GDK and the test environment
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ Gitlab::TaskHelpers.run_command!(['git', 'remote', 'add', 'origin', "file://#{project.repository.raw.path}"])
+ end
+ Gitlab::TaskHelpers.run_command!(['git', 'push', '-u', 'origin', 'master'])
+ end
end
-
- if project.import_failed?
- raise "Failed to import from #{project_params[:import_url]}"
- end
-
- puts "Waiting for the import to finish"
-
- sleep(5)
- project.reset
end
+ project.reset
+
Projects::ImportExport::ExportService.new(project, admin).execute
downloader.call(project.export_file, template.archive_path)
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
index 56b81106c5f..4ec4fdd281f 100644
--- a/lib/tasks/services.rake
+++ b/lib/tasks/services.rake
@@ -86,7 +86,7 @@ namespace :services do
doc_start = Time.now
doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
- result = ERB.new(services_template, 0, '>')
+ result = ERB.new(services_template, trim_mode: '>')
.result(OpenStruct.new(services: services).instance_eval { binding })
File.open(doc_path, 'w') do |f|
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8aba22fff14..6227ca9afc3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -300,6 +300,9 @@ msgstr ""
msgid "'%{source}' is not a import source"
msgstr ""
+msgid "(Show all)"
+msgstr ""
+
msgid "(external source)"
msgstr ""
@@ -672,9 +675,6 @@ msgstr ""
msgid "Add a task list"
msgstr ""
-msgid "Add a todo"
-msgstr ""
-
msgid "Add an SSH key"
msgstr ""
@@ -750,7 +750,7 @@ msgstr ""
msgid "Added %{label_references} %{label_text}."
msgstr ""
-msgid "Added a todo."
+msgid "Added a To Do."
msgstr ""
msgid "Added at"
@@ -765,7 +765,7 @@ msgstr ""
msgid "Adds %{labels} %{label_text}."
msgstr ""
-msgid "Adds a todo."
+msgid "Adds a To Do."
msgstr ""
msgid "Admin Area"
@@ -1362,9 +1362,6 @@ msgstr ""
msgid "Assign"
msgstr ""
-msgid "Assign command failed because no user was found"
-msgstr ""
-
msgid "Assign custom color like #FF0000"
msgstr ""
@@ -1948,6 +1945,9 @@ msgstr ""
msgid "CICD|instance enabled"
msgstr ""
+msgid "CLOSED"
+msgstr ""
+
msgid "CONTRIBUTING"
msgstr ""
@@ -2008,6 +2008,15 @@ msgstr ""
msgid "Certificate (PEM)"
msgstr ""
+msgid "Change Label"
+msgstr ""
+
+msgid "Change assignee"
+msgstr ""
+
+msgid "Change milestone"
+msgstr ""
+
msgid "Change path"
msgstr ""
@@ -2944,6 +2953,9 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "Collector hostname"
+msgstr ""
+
msgid "ComboSearch is not defined"
msgstr ""
@@ -2953,9 +2965,6 @@ msgstr ""
msgid "Commands applied"
msgstr ""
-msgid "Commands did not apply"
-msgstr ""
-
msgid "Comment"
msgstr ""
@@ -2988,6 +2997,9 @@ msgstr[1] ""
msgid "Commit %{commit_id}"
msgstr ""
+msgid "Commit (when editing commit message)"
+msgstr ""
+
msgid "Commit Message"
msgstr ""
@@ -3111,6 +3123,9 @@ msgstr ""
msgid "Configure storage path settings."
msgstr ""
+msgid "Configure the %{link} integration."
+msgstr ""
+
msgid "Configure the way a user creates a new account."
msgstr ""
@@ -3163,14 +3178,19 @@ msgid "ContainerRegistry|Quick Start"
msgstr ""
msgid "ContainerRegistry|Remove image"
-msgstr ""
+msgid_plural "ContainerRegistry|Remove images"
+msgstr[0] ""
+msgstr[1] ""
-msgid "ContainerRegistry|Remove image and tags"
+msgid "ContainerRegistry|Remove image(s) and tags"
msgstr ""
msgid "ContainerRegistry|Remove repository"
msgstr ""
+msgid "ContainerRegistry|Remove selected images"
+msgstr ""
+
msgid "ContainerRegistry|Size"
msgstr ""
@@ -3192,6 +3212,9 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
+msgid "ContainerRegistry|You are about to delete <b>%{count}</b> images. This will delete the images and all tags pointing to them."
+msgstr ""
+
msgid "ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will delete the image and all tags pointing to this image."
msgstr ""
@@ -3252,6 +3275,9 @@ msgstr ""
msgid "ConvDev Index"
msgstr ""
+msgid "Cookie domain"
+msgstr ""
+
msgid "Copied"
msgstr ""
@@ -3378,7 +3404,7 @@ msgstr ""
msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr ""
-msgid "Create a merge request."
+msgid "Create a merge request"
msgstr ""
msgid "Create a new branch"
@@ -3486,10 +3512,10 @@ msgstr ""
msgid "Created At"
msgstr ""
-msgid "Created a branch and a merge request to resolve this issue"
+msgid "Created a branch and a merge request to resolve this issue."
msgstr ""
-msgid "Created branch '%{branch_name}' and a merge request to resolve this issue"
+msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
msgstr ""
msgid "Created by me"
@@ -3501,10 +3527,10 @@ msgstr ""
msgid "Created on:"
msgstr ""
-msgid "Creates a branch and a merge request to resolve this issue"
+msgid "Creates a branch and a merge request to resolve this issue."
msgstr ""
-msgid "Creates branch '%{branch_name}' and a merge request to resolve this issue"
+msgid "Creates branch '%{branch_name}' and a merge request to resolve this issue."
msgstr ""
msgid "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available."
@@ -4100,12 +4126,24 @@ msgstr ""
msgid "Edit identity for %{user_name}"
msgstr ""
+msgid "Edit issue"
+msgstr ""
+
msgid "Edit issues"
msgstr ""
+msgid "Edit last comment (when focused on an empty textarea)"
+msgstr ""
+
+msgid "Edit merge request"
+msgstr ""
+
msgid "Edit public deploy key"
msgstr ""
+msgid "Edit wiki page"
+msgstr ""
+
msgid "Email"
msgstr ""
@@ -4232,6 +4270,9 @@ msgstr ""
msgid "Enable shared Runners"
msgstr ""
+msgid "Enable snowplow tracking"
+msgstr ""
+
msgid "Enable two-factor authentication"
msgstr ""
@@ -4769,6 +4810,12 @@ msgstr ""
msgid "Failed create wiki"
msgstr ""
+msgid "Failed to apply commands."
+msgstr ""
+
+msgid "Failed to assign a user because no user was found."
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
@@ -4811,6 +4858,18 @@ msgstr ""
msgid "Failed to load related branches"
msgstr ""
+msgid "Failed to mark this issue as a duplicate because referenced issue was not found."
+msgstr ""
+
+msgid "Failed to move this issue because label was not found."
+msgstr ""
+
+msgid "Failed to move this issue because only a single label can be provided."
+msgstr ""
+
+msgid "Failed to move this issue because target project doesn't exist."
+msgstr ""
+
msgid "Failed to promote label due to internal error. Please contact administrators."
msgstr ""
@@ -4957,6 +5016,9 @@ msgstr ""
msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file."
msgstr ""
+msgid "Finding Project File"
+msgstr ""
+
msgid "Fingerprint"
msgstr ""
@@ -4981,6 +5043,12 @@ msgstr ""
msgid "FlowdockService|Flowdock is a collaboration web app for technical teams."
msgstr ""
+msgid "Focus Filter"
+msgstr ""
+
+msgid "Focus Search"
+msgstr ""
+
msgid "FogBugz Email"
msgstr ""
@@ -5017,6 +5085,9 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
+msgid "Forgot your password?"
+msgstr ""
+
msgid "Fork"
msgstr ""
@@ -5167,6 +5238,9 @@ msgstr ""
msgid "GitLab User"
msgstr ""
+msgid "GitLab member or Email address"
+msgstr ""
+
msgid "GitLab project export"
msgstr ""
@@ -5200,6 +5274,9 @@ msgstr ""
msgid "Given access %{time_ago}"
msgstr ""
+msgid "Global Shortcuts"
+msgstr ""
+
msgid "Global notification settings"
msgstr ""
@@ -5224,12 +5301,81 @@ msgstr ""
msgid "Go to %{link_to_google_takeout}."
msgstr ""
+msgid "Go to commits"
+msgstr ""
+
+msgid "Go to environments"
+msgstr ""
+
+msgid "Go to file"
+msgstr ""
+
+msgid "Go to file permalink"
+msgstr ""
+
+msgid "Go to files"
+msgstr ""
+
+msgid "Go to finding file"
+msgstr ""
+
+msgid "Go to groups"
+msgstr ""
+
+msgid "Go to issue boards"
+msgstr ""
+
+msgid "Go to issues"
+msgstr ""
+
+msgid "Go to jobs"
+msgstr ""
+
+msgid "Go to kubernetes"
+msgstr ""
+
+msgid "Go to merge requests"
+msgstr ""
+
+msgid "Go to metrics"
+msgstr ""
+
+msgid "Go to milestones"
+msgstr ""
+
+msgid "Go to network graph"
+msgstr ""
+
msgid "Go to parent"
msgstr ""
msgid "Go to project"
msgstr ""
+msgid "Go to projects"
+msgstr ""
+
+msgid "Go to repository charts"
+msgstr ""
+
+msgid "Go to snippets"
+msgstr ""
+
+msgid "Go to the activity feed"
+msgstr ""
+
+msgid "Go to the project's activity feed"
+msgstr ""
+
+msgid "Go to the project's overview page"
+msgstr ""
+
+msgid "Go to todos"
+msgstr ""
+
+msgid "Go to wiki"
+msgstr ""
+
msgid "Go to your fork"
msgstr ""
@@ -6165,6 +6311,9 @@ msgstr ""
msgid "Key: %{key}"
msgstr ""
+msgid "Keyboard Shortcuts"
+msgstr ""
+
msgid "Kubernetes"
msgstr ""
@@ -6296,9 +6445,15 @@ msgstr ""
msgid "Last seen"
msgstr ""
+msgid "Last successful update"
+msgstr ""
+
msgid "Last update"
msgstr ""
+msgid "Last update attempt"
+msgstr ""
+
msgid "Last updated"
msgstr ""
@@ -6448,13 +6603,13 @@ msgstr ""
msgid "Locked by %{fileLockUserName}"
msgstr ""
-msgid "Locked the discussion"
+msgid "Locked the discussion."
msgstr ""
msgid "Locked to current projects"
msgstr ""
-msgid "Locks the discussion"
+msgid "Locks the discussion."
msgstr ""
msgid "Logo was successfully removed."
@@ -6463,19 +6618,22 @@ msgstr ""
msgid "Logs"
msgstr ""
+msgid "MERGED"
+msgstr ""
+
msgid "MRDiff|Show changes only"
msgstr ""
msgid "MRDiff|Show full file"
msgstr ""
-msgid "Made this issue confidential"
+msgid "Made this issue confidential."
msgstr ""
msgid "Make and review changes in the browser with the Web IDE"
msgstr ""
-msgid "Make issue confidential."
+msgid "Make issue confidential"
msgstr ""
msgid "Make sure you save it - you won't be able to access it again."
@@ -6484,7 +6642,7 @@ msgstr ""
msgid "Make sure you're logged into the account that owns the projects you'd like to import."
msgstr ""
-msgid "Makes this issue confidential"
+msgid "Makes this issue confidential."
msgstr ""
msgid "Manage"
@@ -6550,10 +6708,10 @@ msgstr ""
msgid "March"
msgstr ""
-msgid "Mark as done"
+msgid "Mark To Do as done"
msgstr ""
-msgid "Mark as duplicate failed because referenced issue was not found"
+msgid "Mark as done"
msgstr ""
msgid "Mark as resolved"
@@ -6565,9 +6723,6 @@ msgstr ""
msgid "Mark this issue as a duplicate of another issue"
msgstr ""
-msgid "Mark to do as done"
-msgstr ""
-
msgid "Markdown"
msgstr ""
@@ -6580,13 +6735,16 @@ msgstr ""
msgid "Markdown is supported"
msgstr ""
+msgid "Marked To Do as done."
+msgstr ""
+
msgid "Marked this %{noun} as Work In Progress."
msgstr ""
msgid "Marked this issue as a duplicate of %{duplicate_param}."
msgstr ""
-msgid "Marked to do as done."
+msgid "Marks To Do as done."
msgstr ""
msgid "Marks this %{noun} as Work In Progress."
@@ -6595,9 +6753,6 @@ msgstr ""
msgid "Marks this issue as a duplicate of %{duplicate_reference}."
msgstr ""
-msgid "Marks to do as done."
-msgstr ""
-
msgid "Max access level"
msgstr ""
@@ -6958,18 +7113,24 @@ msgstr ""
msgid "Move issue from one column of the board to another"
msgstr ""
-msgid "Move this issue failed because target project doesn't exists"
+msgid "Move selection down"
msgstr ""
-msgid "Move this issue failed because you need to specify only one label."
+msgid "Move selection up"
msgstr ""
msgid "Move this issue to another project."
msgstr ""
+msgid "Move to next file"
+msgstr ""
+
msgid "Move to next unresolved discussion"
msgstr ""
+msgid "Move to previous file"
+msgstr ""
+
msgid "Move to previous unresolved discussion"
msgstr ""
@@ -7033,6 +7194,9 @@ msgstr ""
msgid "Network"
msgstr ""
+msgid "Network Graph"
+msgstr ""
+
msgid "Never"
msgstr ""
@@ -7493,6 +7657,9 @@ msgstr ""
msgid "Open Documentation"
msgstr ""
+msgid "Open Selection"
+msgstr ""
+
msgid "Open comment type dropdown"
msgstr ""
@@ -8498,6 +8665,12 @@ msgstr ""
msgid "Project Badges"
msgstr ""
+msgid "Project File"
+msgstr ""
+
+msgid "Project Files browsing"
+msgstr ""
+
msgid "Project ID"
msgstr ""
@@ -9250,6 +9423,9 @@ msgstr ""
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
+msgid "Reply (quoting selected text)"
+msgstr ""
+
msgid "Reply by email"
msgstr ""
@@ -9612,15 +9788,27 @@ msgstr ""
msgid "Scopes"
msgstr ""
+msgid "Scroll down"
+msgstr ""
+
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
msgstr ""
+msgid "Scroll left"
+msgstr ""
+
+msgid "Scroll right"
+msgstr ""
+
msgid "Scroll to bottom"
msgstr ""
msgid "Scroll to top"
msgstr ""
+msgid "Scroll up"
+msgstr ""
+
msgid "Search"
msgstr ""
@@ -9750,9 +9938,6 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
-msgid "Select members to invite"
-msgstr ""
-
msgid "Select merge moment"
msgstr ""
@@ -10065,6 +10250,9 @@ msgstr ""
msgid "Show whitespace changes"
msgstr ""
+msgid "Show/hide this dialog"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
@@ -10121,6 +10309,9 @@ msgstr ""
msgid "Similar issues"
msgstr ""
+msgid "Site ID"
+msgstr ""
+
msgid "Size and domain settings for static websites"
msgstr ""
@@ -10151,6 +10342,9 @@ msgstr ""
msgid "SnippetsEmptyState|They can be either public or private."
msgstr ""
+msgid "Snowplow"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -11780,6 +11974,9 @@ msgstr ""
msgid "Today"
msgstr ""
+msgid "Toggle Markdown preview"
+msgstr ""
+
msgid "Toggle Sidebar"
msgstr ""
@@ -11804,6 +12001,9 @@ msgstr ""
msgid "Toggle sidebar"
msgstr ""
+msgid "Toggle the Performance Bar"
+msgstr ""
+
msgid "Toggle thread"
msgstr ""
@@ -12023,10 +12223,10 @@ msgstr ""
msgid "Unlocked"
msgstr ""
-msgid "Unlocked the discussion"
+msgid "Unlocked the discussion."
msgstr ""
-msgid "Unlocks the discussion"
+msgid "Unlocks the discussion."
msgstr ""
msgid "Unmarked this %{noun} as Work In Progress."
@@ -12338,6 +12538,9 @@ msgstr ""
msgid "Username is available."
msgstr ""
+msgid "Username or email"
+msgstr ""
+
msgid "Users"
msgstr ""
@@ -12565,6 +12768,9 @@ msgstr ""
msgid "Wiki"
msgstr ""
+msgid "Wiki pages"
+msgstr ""
+
msgid "Wiki was successfully updated."
msgstr ""
@@ -13780,6 +13986,9 @@ msgstr ""
msgid "updated"
msgstr ""
+msgid "updated %{time_ago}"
+msgstr ""
+
msgid "user avatar"
msgstr ""
diff --git a/package.json b/package.json
index 7ff4e74137b..2b9a00d1cbd 100644
--- a/package.json
+++ b/package.json
@@ -38,8 +38,8 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.4.4",
"@gitlab/csslab": "^1.9.0",
- "@gitlab/svgs": "^1.67.0",
- "@gitlab/ui": "5.14.0",
+ "@gitlab/svgs": "^1.68.0",
+ "@gitlab/ui": "5.15.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-link": "^1.2.11",
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index c2b0482d789..94245bbfcba 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -52,13 +52,11 @@ module QA
raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given?
if Runtime::User.ldap_user?
- sign_in_using_ldap_credentials
+ sign_in_using_ldap_credentials(user || Runtime::User)
else
sign_in_using_gitlab_credentials(user || Runtime::User)
end
end
-
- Page::Main::Menu.perform(&:has_personal_area?)
end
def sign_in_using_admin_credentials
@@ -76,6 +74,25 @@ module QA
Page::Main::Menu.perform(&:has_personal_area?)
end
+ def sign_in_using_ldap_credentials(user)
+ # Log out if already logged in
+ Page::Main::Menu.perform do |menu|
+ menu.sign_out if menu.has_personal_area?(wait: 0)
+ end
+
+ using_wait_time 0 do
+ set_initial_password_if_present
+
+ switch_to_ldap_tab
+
+ fill_element :username_field, user.ldap_username
+ fill_element :password_field, user.ldap_password
+ click_element :sign_in_button
+ end
+
+ Page::Main::Menu.perform(&:has_personal_area?)
+ end
+
def self.path
'/users/sign_in'
end
@@ -133,14 +150,6 @@ module QA
private
- def sign_in_using_ldap_credentials
- switch_to_ldap_tab
-
- fill_element :username_field, Runtime::User.ldap_username
- fill_element :password_field, Runtime::User.ldap_password
- click_element :sign_in_button
- end
-
def sign_in_using_gitlab_credentials(user)
switch_to_sign_in_tab if has_sign_in_tab?
switch_to_standard_tab if has_standard_tab?
diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb
index 0d138417176..65c02801d67 100644
--- a/qa/qa/page/project/issue/new.rb
+++ b/qa/qa/page/project/issue/new.rb
@@ -6,7 +6,7 @@ module QA
module Issue
class New < Page::Base
view 'app/views/shared/issuable/_form.html.haml' do
- element :submit_issue_button, 'form.submit "Submit' # rubocop:disable QA/ElementWithPattern
+ element :issuable_create_button
end
view 'app/views/shared/issuable/form/_title.html.haml' do
@@ -26,7 +26,7 @@ module QA
end
def create_new_issue
- click_on 'Submit issue'
+ click_element :issuable_create_button, Page::Project::Issue::Show
end
end
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 507dccb52d0..45dad9bc0ae 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -14,7 +14,7 @@ module QA
end
view 'app/assets/javascripts/notes/components/discussion_filter.vue' do
- element :discussion_filter
+ element :discussion_filter, required: true
element :filter_options
end
diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb
index 3fe048f752a..838d59b59cb 100644
--- a/qa/qa/page/project/menu.rb
+++ b/qa/qa/page/project/menu.rb
@@ -5,7 +5,7 @@ module QA
module Project
class Menu < Page::Base
include SubMenus::Common
-
+ include SubMenus::Project
include SubMenus::CiCd
include SubMenus::Issues
include SubMenus::Operations
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 7969de726e4..45ab2396a04 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -5,7 +5,8 @@ require 'securerandom'
module QA
module Resource
class MergeRequest < Base
- attr_accessor :title,
+ attr_accessor :id,
+ :title,
:description,
:source_branch,
:target_branch,
@@ -74,6 +75,28 @@ module QA
page.create_merge_request
end
end
+
+ def fabricate_via_api!
+ populate(:target, :source)
+ super
+ end
+
+ def api_get_path
+ "/projects/#{project.id}/merge_requests/#{id}"
+ end
+
+ def api_post_path
+ "/projects/#{project.id}/merge_requests"
+ end
+
+ def api_post_body
+ {
+ description: @description,
+ source_branch: @source_branch,
+ target_branch: @target_branch,
+ title: @title
+ }
+ end
end
end
end
diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb
index b4d70fc191a..632a0f5f2a9 100644
--- a/qa/qa/scenario/test/sanity/selectors.rb
+++ b/qa/qa/scenario/test/sanity/selectors.rb
@@ -7,11 +7,13 @@ module QA
class Selectors < Scenario::Template
include Scenario::Bootable
- PAGES = [QA::Page].freeze
+ def pages
+ @pages ||= [QA::Page]
+ end
def perform(*)
- validators = PAGES.map do |pages|
- Page::Validator.new(pages)
+ validators = pages.map do |page|
+ Page::Validator.new(page)
end
validators.flat_map(&:errors).tap do |errors|
diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb
index b54fd5628f2..c5cddff56cd 100644
--- a/qa/qa/service/omnibus.rb
+++ b/qa/qa/service/omnibus.rb
@@ -11,11 +11,12 @@ module QA
end
def gitlab_ctl(command, input: nil)
- if input.nil?
- shell "docker exec #{@name} gitlab-ctl #{command}"
- else
- shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'"
- end
+ docker_exec("gitlab-ctl #{command}", input: input)
+ end
+
+ def docker_exec(command, input: nil)
+ command = "#{input} | #{command}" if input
+ shell "docker exec #{@name} bash -c '#{command}'"
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
index 9e48ee7ca2a..891cef6c420 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
@@ -7,13 +7,18 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- @merge_request = Resource::MergeRequest.fabricate! do |merge_request|
+ project = Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project'
+ end
+
+ @merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request|
+ merge_request.project = project
merge_request.title = 'This is a merge request'
- merge_request.description = 'For downloading patches and diffs'
+ merge_request.description = '... for downloading patches and diffs'
end
end
- it 'user views merge request email patches' do
+ it 'views the merge request email patches' do
@merge_request.visit!
Page::MergeRequest::Show.perform(&:view_email_patches)
@@ -22,7 +27,7 @@ module QA
expect(page).to have_content('diff --git a/added_file.txt b/added_file.txt')
end
- it 'user views merge request plain diff' do
+ it 'views the merge request plain diff' do
@merge_request.visit!
Page::MergeRequest::Show.perform(&:view_plain_diff)
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index 2fe4e4d9d1f..f6411d8c5ad 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -3,36 +3,29 @@
require 'digest/sha1'
module QA
- # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/70
- context 'Release', :docker, :quarantine do
+ context 'Release', :docker do
describe 'Git clone using a deploy key' do
- def login
+ before do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
- end
-
- before(:all) do
- login
@runner_name = "qa-runner-#{Time.now.to_i}"
- @project = Resource::Project.fabricate! do |resource|
+ @project = Resource::Project.fabricate_via_api! do |resource|
resource.name = 'deploy-key-clone-project'
end
@repository_location = @project.repository_ssh_location
- Resource::Runner.fabricate! do |resource|
+ Resource::Runner.fabricate_via_browser_ui! do |resource|
resource.project = @project
resource.name = @runner_name
resource.tags = %w[qa docker]
resource.image = 'gitlab/gitlab-runner:ubuntu'
end
-
- Page::Main::Menu.perform(&:sign_out)
end
- after(:all) do
+ after do
Service::Runner.new(@runner_name).remove!
end
@@ -46,9 +39,7 @@ module QA
it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do
key = key_class.new(*bits)
- login
-
- Resource::DeployKey.fabricate! do |resource|
+ Resource::DeployKey.fabricate_via_browser_ui! do |resource|
resource.project = @project
resource.title = "deploy key #{key.name}(#{key.bits})"
resource.key = key.public_key
@@ -56,7 +47,7 @@ module QA
deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
- Resource::CiVariable.fabricate! do |resource|
+ Resource::CiVariable.fabricate_via_browser_ui! do |resource|
resource.project = @project
resource.key = deploy_key_name
resource.value = key.private_key
diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb
index 9c1f9904a7a..1b8c926532a 100644
--- a/qa/qa/vendor/saml_idp/page/login.rb
+++ b/qa/qa/vendor/saml_idp/page/login.rb
@@ -12,6 +12,14 @@ module QA
fill_in 'password', with: 'user1pass'
click_on 'Login'
end
+
+ def login_if_required
+ login if login_required?
+ end
+
+ def login_required?
+ page.has_text?('Enter your username and password')
+ end
end
end
end
diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb
index bf3ebce0cfe..2f9e4958ae1 100644
--- a/qa/spec/resource/repository/push_spec.rb
+++ b/qa/spec/resource/repository/push_spec.rb
@@ -19,7 +19,11 @@ describe QA::Resource::Repository::Push do
expect { subject.files = [] }.to raise_error(ArgumentError)
end
- it 'does not raise if files is an array' do
+ it 'raises an error if files is not an array of hashes with :name and :content keys' do
+ expect { subject.files = [{ foo: 'foo' }] }.to raise_error(ArgumentError)
+ end
+
+ it 'does not raise if files is an array of hashes with :name and :content keys' do
expect { subject.files = files }.not_to raise_error
end
end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index caf96a213e1..340831aa06d 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -192,6 +192,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/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index 84bbe0525e5..c3a5abf2b7a 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -15,7 +15,7 @@ describe 'bin/changelog' do
allow(entry).to receive(:branch_name).and_return('long-branch-' * 100)
file_path = entry.send(:file_path)
- expect(file_path.length).to eq(140)
+ expect(file_path.length).to eq(99)
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 84bbbac39b0..0b3833e6515 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -641,24 +641,32 @@ describe ApplicationController do
end
end
- it 'does not set a custom header' do
+ it 'sets a custom header' do
get :index, format: :json
- expect(response.headers['X-GitLab-Custom-Error']).to be_nil
+ expect(response.headers['X-GitLab-Custom-Error']).to eq '1'
end
- end
- context 'given a json response for an html request' do
- controller do
- def index
- render json: {}, status: :unprocessable_entity
+ context 'for html request' do
+ it 'sets a custom header' do
+ get :index
+
+ expect(response.headers['X-GitLab-Custom-Error']).to eq '1'
end
end
- it 'does not set a custom header' do
- get :index
+ context 'for 200 response' do
+ controller do
+ def index
+ render json: {}, status: :ok
+ end
+ end
- expect(response.headers['X-GitLab-Custom-Error']).to be_nil
+ it 'does not set a custom header' do
+ get :index, format: :json
+
+ expect(response.headers['X-GitLab-Custom-Error']).to be_nil
+ end
end
end
end
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 2dc97e18113..5e6ceef2517 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -11,6 +11,20 @@ describe Projects::CycleAnalyticsController do
project.add_maintainer(user)
end
+ context "counting page views for 'show'" do
+ it 'increases the counter' do
+ expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views)
+
+ get(:show,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project
+ })
+
+ expect(response).to be_success
+ end
+ end
+
describe 'cycle analytics not set up flag' do
context 'with no data' do
it 'is true' do
diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb
index bf099e8deeb..88fa2236e33 100644
--- a/spec/controllers/projects/git_http_controller_spec.rb
+++ b/spec/controllers/projects/git_http_controller_spec.rb
@@ -12,4 +12,15 @@ describe Projects::GitHttpController do
expect(response.status).to eq(403)
end
end
+
+ describe 'GET #info_refs' do
+ it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
+ project = create(:project, :public, :repository)
+
+ get :info_refs, params: { service: 'git-upload-pack', namespace_id: project.namespace.to_param, project_id: project.path + '.git' }
+
+ expect(response.status).to eq(401)
+ end
+ end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 8ee3168273f..b958f419a19 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -60,7 +60,7 @@ describe Projects::RawController do
execute_raw_requests(requests: 6, project: project, file_path: file_path)
expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.')
- expect(response).to redirect_to(project_blob_path(project, file_path))
+ expect(response).to have_gitlab_http_status(429)
end
it 'logs the event on auth.log' do
@@ -92,7 +92,7 @@ describe Projects::RawController do
execute_raw_requests(requests: 3, project: project, file_path: modified_path)
expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.')
- expect(response).to redirect_to(project_blob_path(project, modified_path))
+ expect(response).to have_gitlab_http_status(429)
end
end
@@ -120,7 +120,7 @@ describe Projects::RawController do
execute_raw_requests(requests: 6, project: project, file_path: file_path)
expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.')
- expect(response).to redirect_to(project_blob_path(project, file_path))
+ expect(response).to have_gitlab_http_status(429)
# Accessing upcase version of readme
file_path = "#{commit_sha}/README.md"
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index ff35139ae2e..c6e063d8229 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -113,4 +113,37 @@ describe Projects::Registry::TagsController do
format: :json
end
end
+
+ describe 'POST bulk_destroy' do
+ context 'when user has access to registry' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when there is matching tag present' do
+ before do
+ stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
+ end
+
+ it 'makes it possible to delete tags in bulk' do
+ allow_any_instance_of(ContainerRegistry::Tag).to receive(:delete) { |*args| ContainerRegistry::Tag.delete(*args) }
+ expect(ContainerRegistry::Tag).to receive(:delete).exactly(2).times
+
+ bulk_destroy_tags(['rc1', 'test.'])
+ end
+ end
+ end
+
+ private
+
+ def bulk_destroy_tags(names)
+ post :bulk_destroy, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ repository_id: repository,
+ ids: names
+ },
+ format: :json
+ end
+ end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index faf3c990cb2..d05482f095e 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
describe RegistrationsController do
include TermsHelper
+ before do
+ stub_feature_flags(invisible_captcha: false)
+ end
+
describe '#create' do
let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } }
let(:user_params) { { user: base_user_params } }
@@ -88,6 +92,88 @@ describe RegistrationsController do
end
end
+ context 'when invisible captcha is enabled' do
+ before do
+ stub_feature_flags(invisible_captcha: true)
+ InvisibleCaptcha.timestamp_threshold = treshold
+ end
+
+ let(:treshold) { 4 }
+ let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } }
+ let(:form_rendered_time) { Time.current }
+ let(:submit_time) { form_rendered_time + treshold }
+ let(:auth_log_attributes) do
+ {
+ message: auth_log_message,
+ env: :invisible_captcha_signup_bot_detected,
+ ip: '0.0.0.0',
+ request_method: 'POST',
+ fullpath: '/users'
+ }
+ end
+
+ describe 'the honeypot has not been filled and the signup form has not been submitted too quickly' do
+ it 'creates an account' do
+ travel_to(submit_time) do
+ expect { post(:create, params: user_params, session: session_params) }.to change(User, :count).by(1)
+ end
+ end
+ end
+
+ describe 'honeypot spam detection' do
+ let(:user_params) { super().merge(firstname: 'Roy', lastname: 'Batty') }
+ let(:auth_log_message) { 'Invisible_Captcha_Honeypot_Request' }
+
+ it 'logs the request, refuses to create an account and renders an empty body' do
+ travel_to(submit_time) do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:bot_blocked_by_invisible_captcha_honeypot, 'Counter of blocked sign up attempts with filled honeypot')
+ .and_call_original
+ expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once
+ expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count)
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
+ describe 'timestamp spam detection' do
+ let(:auth_log_message) { 'Invisible_Captcha_Timestamp_Request' }
+
+ context 'the sign up form has been submitted without the invisible_captcha_timestamp parameter' do
+ let(:session_params) { nil }
+
+ it 'logs the request, refuses to create an account and displays a flash alert' do
+ travel_to(submit_time) do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp')
+ .and_call_original
+ expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once
+ expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count)
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.'
+ end
+ end
+ end
+
+ context 'the sign up form has been submitted too quickly' do
+ let(:submit_time) { form_rendered_time }
+
+ it 'logs the request, refuses to create an account and displays a flash alert' do
+ travel_to(submit_time) do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp')
+ .and_call_original
+ expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once
+ expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count)
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.'
+ end
+ end
+ end
+ end
+ end
+
context 'when terms are enforced' do
before do
enforce_terms
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 5a5c0a1f6ac..3e0d53a6573 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -11,151 +11,173 @@ describe SearchController do
sign_in(user)
end
- context 'uses the right partials depending on scope' do
- using RSpec::Parameterized::TableSyntax
- render_views
-
- set(:project) { create(:project, :public, :repository, :wiki_repo) }
-
+ shared_examples_for 'when the user cannot read cross project' do |action, params|
before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :read_cross_project, :global) { false }
end
- subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+ it 'blocks access without a project_id' do
+ get action, params: params
- where(:partial, :scope) do
- '_blob' | :blobs
- '_wiki_blob' | :wiki_blobs
- '_commit' | :commits
+ expect(response).to have_gitlab_http_status(403)
end
- with_them do
- it do
- project_wiki = create(:project_wiki, project: project, user: user)
- create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+ it 'allows access with a project_id' do
+ get action, params: params.merge(project_id: create(:project, :public).id)
- expect(subject).to render_template("search/results/#{partial}")
- end
+ expect(response).to have_gitlab_http_status(200)
end
end
- context 'global search' do
- render_views
-
- it 'omits pipeline status from load' do
- project = create(:project, :public)
- expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
-
- get :show, params: { scope: 'projects', search: project.name }
+ shared_examples_for 'with external authorization service enabled' do |action, params|
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:note) { create(:note_on_issue, project: project) }
- expect(assigns[:search_objects].first).to eq project
+ before do
+ enable_external_authorization_service_check
end
- end
-
- it 'finds issue comments' do
- project = create(:project, :public)
- note = create(:note_on_issue, project: project)
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ it 'renders a 403 when no project is given' do
+ get action, params: params
- expect(assigns[:search_objects].first).to eq note
- end
-
- context 'when the user cannot read cross project' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?)
- .with(user, :read_cross_project, :global) { false }
+ expect(response).to have_gitlab_http_status(403)
end
- it 'still allows accessing the search page' do
- get :show
+ it 'renders a 200 when a project was set' do
+ get action, params: params.merge(project_id: project.id)
expect(response).to have_gitlab_http_status(200)
end
+ end
- it 'still blocks searches without a project_id' do
- get :show, params: { search: 'hello' }
+ describe 'GET #show' do
+ it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
+ it 'still allows accessing the search page' do
+ get :show
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_gitlab_http_status(200)
+ end
end
- it 'allows searches with a project_id' do
- get :show, params: { search: 'hello', project_id: create(:project, :public).id }
+ it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
- expect(response).to have_gitlab_http_status(200)
- end
- end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ set(:project) { create(:project, :public, :repository, :wiki_repo) }
- context 'on restricted projects' do
- context 'when signed out' do
before do
- sign_out(user)
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
end
- it "doesn't expose comments on issues" do
- project = create(:project, :public, :issues_private)
- note = create(:note_on_issue, project: project)
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
+ end
- expect(assigns[:search_objects].count).to eq(0)
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+
+ expect(subject).to render_template("search/results/#{partial}")
+ end
end
end
- it "doesn't expose comments on merge_requests" do
- project = create(:project, :public, :merge_requests_private)
- note = create(:note_on_merge_request, project: project)
+ context 'global search' do
+ render_views
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ it 'omits pipeline status from load' do
+ project = create(:project, :public)
+ expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
+
+ get :show, params: { scope: 'projects', search: project.name }
- expect(assigns[:search_objects].count).to eq(0)
+ expect(assigns[:search_objects].first).to eq project
+ end
end
- it "doesn't expose comments on snippets" do
- project = create(:project, :public, :snippets_private)
- note = create(:note_on_project_snippet, project: project)
+ it 'finds issue comments' do
+ project = create(:project, :public)
+ note = create(:note_on_issue, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- expect(assigns[:search_objects].count).to eq(0)
+ expect(assigns[:search_objects].first).to eq note
end
- end
- context 'with external authorization service enabled' do
- let(:project) { create(:project, namespace: user.namespace) }
- let(:note) { create(:note_on_issue, project: project) }
+ context 'on restricted projects' do
+ context 'when signed out' do
+ before do
+ sign_out(user)
+ end
- before do
- enable_external_authorization_service_check
- end
+ it "doesn't expose comments on issues" do
+ project = create(:project, :public, :issues_private)
+ note = create(:note_on_issue, project: project)
- describe 'GET #show' do
- it 'renders a 403 when no project is given' do
- get :show, params: { scope: 'notes', search: note.note }
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- expect(response).to have_gitlab_http_status(403)
+ expect(assigns[:search_objects].count).to eq(0)
+ end
end
- it 'renders a 200 when a project was set' do
+ it "doesn't expose comments on merge_requests" do
+ project = create(:project, :public, :merge_requests_private)
+ note = create(:note_on_merge_request, project: project)
+
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- expect(response).to have_gitlab_http_status(200)
+ expect(assigns[:search_objects].count).to eq(0)
end
- end
- describe 'GET #autocomplete' do
- it 'renders a 403 when no project is given' do
- get :autocomplete, params: { term: 'hello' }
+ it "doesn't expose comments on snippets" do
+ project = create(:project, :public, :snippets_private)
+ note = create(:note_on_project_snippet, project: project)
- expect(response).to have_gitlab_http_status(403)
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+
+ expect(assigns[:search_objects].count).to eq(0)
end
+ end
+ end
- it 'renders a 200 when a project was set' do
- get :autocomplete, params: { project_id: project.id, term: 'hello' }
+ describe 'GET #count' do
+ it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
+ it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
- expect(response).to have_gitlab_http_status(200)
- end
+ it 'returns the result count for the given term and scope' do
+ create(:project, :public, name: 'hello world')
+ create(:project, :public, name: 'foo bar')
+
+ get :count, params: { search: 'hello', scope: 'projects' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to eq({ 'count' => '1' })
+ end
+
+ it 'raises an error if search term is missing' do
+ expect do
+ get :count, params: { scope: 'projects' }
+ end.to raise_error(ActionController::ParameterMissing)
end
+
+ it 'raises an error if search scope is missing' do
+ expect do
+ get :count, params: { search: 'hello' }
+ end.to raise_error(ActionController::ParameterMissing)
+ end
+ end
+
+ describe 'GET #autocomplete' do
+ it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
+ it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index f3e662ad4f5..b2d6ada91fa 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -16,6 +16,19 @@ FactoryBot.define do
)
end
+ factory :emails_on_push_service do
+ project
+ type 'EmailsOnPushService'
+ active true
+ push_events true
+ tag_push_events true
+ properties(
+ recipients: 'test@example.com',
+ disable_diffs: true,
+ send_from_committer_email: true
+ )
+ end
+
factory :mock_deployment_service do
project
type 'MockDeploymentService'
diff --git a/spec/features/boards/multiple_boards_spec.rb b/spec/features/boards/multiple_boards_spec.rb
new file mode 100644
index 00000000000..9a2b7a80498
--- /dev/null
+++ b/spec/features/boards/multiple_boards_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Multiple Issue Boards', :js do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :public) }
+ set(:planning) { create(:label, project: project, name: 'Planning') }
+ set(:board) { create(:board, name: 'board1', project: project) }
+ set(:board2) { create(:board, name: 'board2', project: project) }
+ let(:parent) { project }
+ let(:boards_path) { project_boards_path(project) }
+
+ it_behaves_like 'multiple issue boards'
+end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 89dece97a35..aefdc4d6d4f 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe "Container Registry", :js do
+describe 'Container Registry', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -40,8 +40,7 @@ describe "Container Registry", :js do
it 'user removes entire container repository' do
visit_container_registry
- expect_any_instance_of(ContainerRepository)
- .to receive(:delete_tags!).and_return(true)
+ expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true)
click_on(class: 'js-remove-repo')
expect(find('.modal .modal-title')).to have_content 'Remove repository'
@@ -54,10 +53,9 @@ describe "Container Registry", :js do
find('.js-toggle-repo').click
wait_for_requests
- expect_any_instance_of(ContainerRegistry::Tag)
- .to receive(:delete).and_return(true)
+ expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
- click_on(class: 'js-delete-registry')
+ click_on(class: 'js-delete-registry-row', visible: false)
expect(find('.modal .modal-title')).to have_content 'Remove image'
find('.modal .modal-footer .btn-danger').click
end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 855cf22642e..832c4a57aa3 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -10,6 +10,7 @@ describe 'Invites' do
let(:group_invite) { group.group_members.invite.last }
before do
+ stub_feature_flags(invisible_captcha: false)
project.add_maintainer(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index aa53ac50c78..4de67cfcdbe 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -26,13 +26,31 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching do
restore_host
end
- context 'with deployments and related deployable present' do
- it 'shows embedded metrics' do
+ it 'shows embedded metrics' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_css('div.prometheus-graph')
+ expect(page).to have_text('Memory Usage (Total)')
+ expect(page).to have_text('Core Usage (Total)')
+ end
+
+ context 'when dashboard params are in included the url' do
+ let(:metrics_url) { metrics_project_environment_url(project, environment, **chart_params) }
+
+ let(:chart_params) do
+ {
+ group: 'System metrics (Kubernetes)',
+ title: 'Memory Usage (Pod average)',
+ y_label: 'Memory Used per Pod (MB)'
+ }
+ end
+
+ it 'shows embedded metrics for the specifiec chart' do
visit project_issue_path(project, issue)
expect(page).to have_css('div.prometheus-graph')
- expect(page).to have_text('Memory Usage (Total)')
- expect(page).to have_text('Core Usage (Total)')
+ expect(page).to have_text(chart_params[:title])
+ expect(page).to have_text(chart_params[:y_label])
end
end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 5e52c82a234..4dbdea02e27 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -38,7 +38,7 @@ describe 'User visits the profile preferences page' do
describe 'User changes their default dashboard', :js do
it 'creates a flash message' do
- select 'Starred Projects', from: 'user_dashboard'
+ select2('stars', from: '#user_dashboard')
click_button 'Save'
wait_for_requests
@@ -47,7 +47,7 @@ describe 'User visits the profile preferences page' do
end
it 'updates their preference' do
- select 'Starred Projects', from: 'user_dashboard'
+ select2('stars', from: '#user_dashboard')
click_button 'Save'
wait_for_requests
diff --git a/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb
new file mode 100644
index 00000000000..6d587053b4f
--- /dev/null
+++ b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Projects > Raw > User interacts with raw endpoint' do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, :public) }
+ let(:file_path) { 'master/README.md' }
+
+ before do
+ stub_application_setting(raw_blob_request_limit: 3)
+ project.add_developer(user)
+ create_file_in_repo(project, 'master', 'master', 'README.md', 'readme content')
+
+ sign_in(user)
+ end
+
+ context 'when user access a raw file' do
+ it 'renders the page successfully' do
+ visit project_raw_url(project, file_path)
+
+ expect(source).to eq('') # Body is filled in by gitlab-workhorse
+ end
+ end
+
+ context 'when user goes over the rate requests limit' do
+ it 'returns too many requests' do
+ 4.times do
+ visit project_raw_url(project, file_path)
+ end
+
+ expect(source).to have_content('You are being redirected')
+ click_link('redirected')
+ expect(page).to have_content('You cannot access the raw file. Please wait a minute.')
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index 2517a843c62..e10c1afc0b8 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe 'User searches for users' do
context 'when on the dashboard' do
- it 'finds the user' do
+ it 'finds the user', :js do
create(:user, username: 'gob_bluth', name: 'Gob Bluth')
sign_in(create(:user))
@@ -12,7 +12,7 @@ describe 'User searches for users' do
visit dashboard_projects_path
fill_in 'search', with: 'gob'
- click_button 'Go'
+ find('#search').send_keys(:enter)
expect(page).to have_content('Users 1')
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index c781048d06d..5006631cc14 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -96,6 +96,23 @@ describe 'User uses header search field', :js do
let(:url) { root_path }
let(:scope_name) { 'All GitLab' }
end
+
+ context 'when searching through the search field' do
+ before do
+ create(:issue, project: project, title: 'project issue')
+
+ fill_in('search', with: 'project')
+ find('#search').send_keys(:enter)
+ end
+
+ it 'displays result counts for all categories' do
+ expect(page).to have_content('Projects 1')
+ expect(page).to have_content('Issues 1')
+ expect(page).to have_content('Merge requests 0')
+ expect(page).to have_content('Milestones 0')
+ expect(page).to have_content('Users 0')
+ end
+ end
end
context 'when user is in a project scope' do
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index dac8c8e7a29..1d8c9e7e426 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -95,6 +95,42 @@ describe 'Login' do
end
end
+ describe 'with an unconfirmed email address' do
+ let!(:user) { create(:user, confirmed_at: nil) }
+ let(:grace_period) { 2.days }
+
+ before do
+ stub_application_setting(send_user_confirmation_email: true)
+ allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
+ end
+
+ context 'within the grace period' do
+ it 'allows to login' do
+ expect(authentication_metrics).to increment(:user_authenticated_counter)
+
+ gitlab_sign_in(user)
+
+ expect(page).not_to have_content('You have to confirm your email address before continuing.')
+ expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path)
+ end
+ end
+
+ context 'when the confirmation grace period is expired' do
+ it 'prevents the user from logging in and renders a resend confirmation email link' do
+ travel_to((grace_period + 1.day).from_now) do
+ expect(authentication_metrics)
+ .to increment(:user_unauthenticated_counter)
+ .and increment(:user_session_destroyed_counter).twice
+
+ gitlab_sign_in(user)
+
+ expect(page).to have_content('You have to confirm your email address before continuing.')
+ expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path)
+ end
+ end
+ end
+ end
+
describe 'with the ghost user' do
it 'disallows login' do
expect(authentication_metrics)
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index f5897bffaf0..cf57fafc4f5 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
describe 'Signup' do
include TermsHelper
+ before do
+ stub_feature_flags(invisible_captcha: false)
+ end
+
let(:new_user) { build_stubbed(:user) }
describe 'username validation', :js do
diff --git a/spec/javascripts/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index 05715bce38f..a1a22274e8f 100644
--- a/spec/javascripts/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -61,7 +61,7 @@ describe('Confirm Rollback Modal Component', () => {
environment,
},
});
- const eventHubSpy = spyOn(eventHub, '$emit');
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
const modal = component.find(GlModal);
modal.vm.$emit('ok');
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index 8c47f6a12c0..fb62a096c3d 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -1,46 +1,38 @@
-import Vue from 'vue';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import eventHub from '~/environments/event_hub';
-import rollbackComp from '~/environments/components/environment_rollback.vue';
+import RollbackComponent from '~/environments/components/environment_rollback.vue';
describe('Rollback Component', () => {
const retryUrl = 'https://gitlab.com/retry';
- let RollbackComponent;
-
- beforeEach(() => {
- RollbackComponent = Vue.extend(rollbackComp);
- });
it('Should render Re-deploy label when isLastDeployment is true', () => {
- const component = new RollbackComponent({
- el: document.querySelector('.test-dom-element'),
+ const wrapper = mount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: true,
environment: {},
},
- }).$mount();
+ });
- expect(component.$el).toHaveSpriteIcon('repeat');
+ expect(wrapper.element).toHaveSpriteIcon('repeat');
});
it('Should render Rollback label when isLastDeployment is false', () => {
- const component = new RollbackComponent({
- el: document.querySelector('.test-dom-element'),
+ const wrapper = mount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: false,
environment: {},
},
- }).$mount();
+ });
- expect(component.$el).toHaveSpriteIcon('redo');
+ expect(wrapper.element).toHaveSpriteIcon('redo');
});
it('should emit a "rollback" event on button click', () => {
- const eventHubSpy = spyOn(eventHub, '$emit');
- const component = shallowMount(RollbackComponent, {
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+ const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
environment: {
@@ -48,7 +40,7 @@ describe('Rollback Component', () => {
},
},
});
- const button = component.find(GlButton);
+ const button = wrapper.find(GlButton);
button.vm.$emit('click');
diff --git a/spec/frontend/matchers.js b/spec/frontend/matchers.js
new file mode 100644
index 00000000000..35c362d0bf5
--- /dev/null
+++ b/spec/frontend/matchers.js
@@ -0,0 +1,38 @@
+export default {
+ toHaveSpriteIcon: (element, iconName) => {
+ if (!iconName) {
+ throw new Error('toHaveSpriteIcon is missing iconName argument!');
+ }
+
+ if (!(element instanceof HTMLElement)) {
+ throw new Error(`${element} is not a DOM element!`);
+ }
+
+ const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
+ const matchingIcon = iconReferences.find(reference =>
+ reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
+ );
+
+ const pass = Boolean(matchingIcon);
+
+ let message;
+ if (pass) {
+ message = `${element.outerHTML} contains the sprite icon "${iconName}"!`;
+ } else {
+ message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`;
+
+ const existingIcons = iconReferences.map(reference => {
+ const iconUrl = reference.getAttribute('xlink:href');
+ return `"${iconUrl.replace(/^.+#/, '')}"`;
+ });
+ if (existingIcons.length > 0) {
+ message += ` (only found ${existingIcons.join(',')})`;
+ }
+ }
+
+ return {
+ pass,
+ message: () => message,
+ };
+ },
+};
diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
index 6d50713999d..8881bedf3cc 100644
--- a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
@@ -74,4 +74,31 @@ describe('notes/components/discussion_keyboard_navigator', () => {
expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId);
});
});
+
+ describe('on destroy', () => {
+ beforeEach(() => {
+ jest.spyOn(Mousetrap, 'unbind');
+
+ createComponent();
+
+ wrapper.destroy();
+ });
+
+ it('unbinds keys', () => {
+ expect(Mousetrap.unbind).toHaveBeenCalledWith('n');
+ expect(Mousetrap.unbind).toHaveBeenCalledWith('p');
+ });
+
+ it('does not call jumpToNextDiscussion when pressing `n`', () => {
+ Mousetrap.trigger('n');
+
+ expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled();
+ });
+
+ it('does not call jumpToNextDiscussion when pressing `p`', () => {
+ Mousetrap.trigger('p');
+
+ expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap
new file mode 100644
index 00000000000..ce456d6c899
--- /dev/null
+++ b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = `
+"<div class=\\"badge\\">22</div>
+<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=issues\\">4</div>
+<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=merge_requests\\">5</div>"
+`;
diff --git a/spec/frontend/pages/search/show/refresh_counts_spec.js b/spec/frontend/pages/search/show/refresh_counts_spec.js
new file mode 100644
index 00000000000..ead268b3971
--- /dev/null
+++ b/spec/frontend/pages/search/show/refresh_counts_spec.js
@@ -0,0 +1,35 @@
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import refreshCounts from '~/pages/search/show/refresh_counts';
+
+const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`;
+const urlWithScope = scope => `${URL}&scope=${scope}`;
+const counts = [{ scope: 'issues', count: 4 }, { scope: 'merge_requests', count: 5 }];
+const fixture = `<div class="badge">22</div>
+<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div>
+<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`;
+
+describe('pages/search/show/refresh_counts', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ setFixtures(fixture);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('fetches and displays search counts', () => {
+ counts.forEach(({ scope, count }) => {
+ mock.onGet(urlWithScope(scope)).reply(200, { count });
+ });
+
+ // assert before act behavior
+ return refreshCounts().then(() => {
+ expect(document.body.innerHTML).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 8b6f7802b15..df8a625319b 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -6,6 +6,7 @@ import { config as testUtilsConfig } from '@vue/test-utils';
import { initializeTestTimeout } from './helpers/timeout';
import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
import { setupManualMocks } from './mocks/mocks_helper';
+import customMatchers from './matchers';
// Expose jQuery so specs using jQuery plugins can be imported nicely.
// Here is an issue to explore better alternatives:
@@ -67,6 +68,8 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
});
});
+expect.extend(customMatchers);
+
// Tech debt issue TBD
testUtilsConfig.logModifiedComponents = false;
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index c69493b579f..2ab72679ee7 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -177,4 +177,48 @@ describe SearchHelper do
end
end
end
+
+ describe 'search_filter_link' do
+ it 'renders a search filter link for the current scope' do
+ @scope = 'projects'
+ @search_results = double
+
+ expect(@search_results).to receive(:formatted_count).with('projects').and_return('23')
+
+ link = search_filter_link('projects', 'Projects')
+
+ expect(link).to have_css('li.active')
+ expect(link).to have_link('Projects', href: search_path(scope: 'projects'))
+ expect(link).to have_css('span.badge.badge-pill:not(.js-search-count):not(.hidden):not([data-url])', text: '23')
+ end
+
+ it 'renders a search filter link for another scope' do
+ link = search_filter_link('projects', 'Projects')
+ count_path = search_count_path(scope: 'projects')
+
+ expect(link).to have_css('li:not([class="active"])')
+ expect(link).to have_link('Projects', href: search_path(scope: 'projects'))
+ expect(link).to have_css("span.badge.badge-pill.js-search-count.hidden[data-url='#{count_path}']", text: '')
+ end
+
+ it 'merges in the current search params and given params' do
+ expect(self).to receive(:params).and_return(
+ ActionController::Parameters.new(
+ search: 'hello',
+ scope: 'ignored',
+ other_param: 'ignored'
+ )
+ )
+
+ link = search_filter_link('projects', 'Projects', search: { project_id: 23 })
+
+ expect(link).to have_link('Projects', href: search_path(scope: 'projects', search: 'hello', project_id: 23))
+ end
+
+ it 'assigns given data attributes on the list container' do
+ link = search_filter_link('projects', 'Projects', data: { foo: 'bar' })
+
+ expect(link).to have_css('li[data-foo="bar"]')
+ end
+ end
end
diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb
new file mode 100644
index 00000000000..647771ace92
--- /dev/null
+++ b/spec/helpers/sessions_helper_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SessionsHelper do
+ describe '#unconfirmed_email?' do
+ it 'returns true when the flash alert contains a devise failure unconfirmed message' do
+ flash[:alert] = t(:unconfirmed, scope: [:devise, :failure])
+ expect(helper.unconfirmed_email?).to be_truthy
+ end
+
+ it 'returns false when the flash alert does not contain a devise failure unconfirmed message' do
+ flash[:alert] = 'something else'
+ expect(helper.unconfirmed_email?).to be_falsey
+ end
+ end
+end
diff --git a/spec/helpers/tracking_helper_spec.rb b/spec/helpers/tracking_helper_spec.rb
index 71505e8ea69..b0c98be4130 100644
--- a/spec/helpers/tracking_helper_spec.rb
+++ b/spec/helpers/tracking_helper_spec.rb
@@ -4,8 +4,32 @@ require 'spec_helper'
describe TrackingHelper do
describe '#tracking_attrs' do
- it 'returns an empty hash' do
- expect(helper.tracking_attrs('a', 'b', 'c')).to eq({})
+ using RSpec::Parameterized::TableSyntax
+
+ let(:input) { %w(a b c) }
+ let(:results) do
+ {
+ no_data: {},
+ with_data: { data: { track_label: 'a', track_event: 'b', track_property: 'c' } }
+ }
+ end
+
+ where(:snowplow_enabled, :environment, :result) do
+ true | 'production' | :with_data
+ false | 'production' | :no_data
+ true | 'development' | :no_data
+ false | 'development' | :no_data
+ true | 'test' | :no_data
+ false | 'test' | :no_data
+ end
+
+ with_them do
+ it 'returns a hash' do
+ stub_application_setting(snowplow_enabled: snowplow_enabled)
+ allow(Rails).to receive(:env).and_return(environment.inquiry)
+
+ expect(helper.tracking_attrs(*input)).to eq(results[result])
+ end
end
end
end
diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js
index 3485eec7763..24bb6b9a48b 100644
--- a/spec/javascripts/jobs/components/job_log_spec.js
+++ b/spec/javascripts/jobs/components/job_log_spec.js
@@ -11,7 +11,7 @@ describe('Job Log', () => {
let vm;
const trace =
- 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>';
+ '<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section js-section-header section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>';
beforeEach(() => {
store = createStore();
@@ -32,7 +32,7 @@ describe('Job Log', () => {
});
expect(vm.$el.querySelector('code').textContent).toContain(
- 'Running with gitlab-runner 11.1.0 (081978aa)',
+ 'Running with gitlab-runner 12.1.0 (de7731dd)',
);
});
diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js
index 4541119dd2e..57f99a09002 100644
--- a/spec/javascripts/monitoring/charts/area_spec.js
+++ b/spec/javascripts/monitoring/charts/area_spec.js
@@ -24,7 +24,6 @@ describe('Area component', () => {
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
- store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
[mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
areaChart = shallowMount(Area, {
@@ -109,16 +108,6 @@ describe('Area component', () => {
});
});
- describe('when exportMetricsToCsvEnabled is disabled', () => {
- beforeEach(() => {
- store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
- });
-
- it('does not render the Download CSV button', () => {
- expect(areaChart.contains('glbutton-stub')).toBe(false);
- });
- });
-
describe('methods', () => {
describe('formatTooltipText', () => {
const mockDate = deploymentData[0].created_at;
@@ -264,23 +253,5 @@ describe('Area component', () => {
expect(areaChart.vm.yAxisLabel).toBe('CPU');
});
});
-
- describe('csvText', () => {
- it('converts data from json to csv', () => {
- const header = `timestamp,${mockGraphData.y_label}`;
- const data = mockGraphData.queries[0].result[0].values;
- const firstRow = `${data[0][0]},${data[0][1]}`;
-
- expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
- });
- });
-
- describe('downloadLink', () => {
- it('produces a link to download metrics as csv', () => {
- const link = areaChart.vm.downloadLink;
-
- expect(link).toContain('blob:');
- });
- });
});
});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 36f650d5933..b78896c45fc 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -5,7 +5,7 @@ import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
-import {
+import MonitoringMock, {
metricsGroupsAPIResponse,
mockApiEndpoint,
environmentData,
@@ -40,6 +40,7 @@ describe('Dashboard', () => {
let mock;
let store;
let component;
+ let mockGraphData;
beforeEach(() => {
setFixtures(`
@@ -482,4 +483,36 @@ describe('Dashboard', () => {
});
});
});
+
+ describe('when downloading metrics data as CSV', () => {
+ beforeEach(() => {
+ component = new DashboardComponent({
+ propsData: {
+ ...propsData,
+ },
+ store,
+ });
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ MonitoringMock.data,
+ );
+ [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics;
+ });
+
+ describe('csvText', () => {
+ it('converts metrics data from json to csv', () => {
+ const header = `timestamp,${mockGraphData.y_label}`;
+ const data = mockGraphData.queries[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+
+ expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`);
+ });
+ });
+
+ describe('downloadCsv', () => {
+ it('produces a link with a Blob', () => {
+ expect(component.downloadCsv(mockGraphData)).toContain(`blob:`);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js
index 31ac970378e..9c7439206ef 100644
--- a/spec/javascripts/registry/components/table_registry_spec.js
+++ b/spec/javascripts/registry/components/table_registry_spec.js
@@ -1,61 +1,159 @@
import Vue from 'vue';
import tableRegistry from '~/registry/components/table_registry.vue';
import store from '~/registry/stores';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { repoPropsData } from '../mock_data';
-const [firstImage] = repoPropsData.list;
+const [firstImage, secondImage] = repoPropsData.list;
describe('table registry', () => {
let vm;
- let Component;
+ const Component = Vue.extend(tableRegistry);
+ const bulkDeletePath = 'path';
const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry');
+ const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row');
+ const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input');
+ const findAllRowCheckboxes = () =>
+ Array.from(vm.$el.querySelectorAll('.js-select-checkbox input'));
+ const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`);
- beforeEach(() => {
- Component = Vue.extend(tableRegistry);
- vm = new Component({
+ const createComponent = () => {
+ vm = mountComponentWithStore(Component, {
store,
- propsData: {
+ props: {
repo: repoPropsData,
},
- }).$mount();
+ });
+ };
+
+ const selectAllCheckboxes = () => vm.selectAll();
+ const deselectAllCheckboxes = () => vm.deselectAll();
+
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
vm.$destroy();
});
- it('should render a table with the registry list', () => {
- expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length);
+ describe('rendering', () => {
+ it('should render a table with the registry list', () => {
+ expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length);
+ });
+
+ it('should render registry tag', () => {
+ const textRendered = vm.$el
+ .querySelector('.table tbody tr')
+ .textContent.trim()
+ // replace additional whitespace characters (e.g. new lines) with a single empty space
+ .replace(/\s\s+/g, ' ');
+
+ expect(textRendered).toContain(repoPropsData.list[0].tag);
+ expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
+ expect(textRendered).toContain(repoPropsData.list[0].layers);
+ expect(textRendered).toContain(repoPropsData.list[0].size);
+ });
});
- it('should render registry tag', () => {
- const textRendered = vm.$el
- .querySelector('.table tbody tr')
- .textContent.trim()
- .replace(/\s\s+/g, ' ');
+ describe('multi select', () => {
+ it('should support multiselect and selecting a row should enable delete button', done => {
+ findSelectAllCheckbox().click();
+ selectAllCheckboxes();
+
+ expect(findSelectAllCheckbox().checked).toBe(true);
+
+ Vue.nextTick(() => {
+ expect(findDeleteBtn().disabled).toBe(false);
+ done();
+ });
+ });
+
+ it('selecting all checkbox should select all rows and enable delete button', done => {
+ selectAllCheckboxes();
+
+ Vue.nextTick(() => {
+ const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
+
+ expect(checkedValues.length).toBe(repoPropsData.list.length);
+ done();
+ });
+ });
+
+ it('deselecting select all checkbox should deselect all rows and disable delete button', done => {
+ selectAllCheckboxes();
+ deselectAllCheckboxes();
+
+ Vue.nextTick(() => {
+ const checkedValues = findAllRowCheckboxes().filter(x => x.checked);
+
+ expect(checkedValues.length).toBe(0);
+ done();
+ });
+ });
+
+ it('should delete multiple items when multiple items are selected', done => {
+ selectAllCheckboxes();
+
+ Vue.nextTick(() => {
+ expect(vm.itemsToBeDeleted).toEqual([0, 1]);
+ expect(findDeleteBtn().disabled).toBe(false);
+
+ findDeleteBtn().click();
+ spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
+
+ Vue.nextTick(() => {
+ const modal = confirmationModal();
+ confirmationModal('.btn-danger').click();
+
+ expect(modal).toExist();
- expect(textRendered).toContain(repoPropsData.list[0].tag);
- expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
- expect(textRendered).toContain(repoPropsData.list[0].layers);
- expect(textRendered).toContain(repoPropsData.list[0].size);
+ Vue.nextTick(() => {
+ expect(vm.itemsToBeDeleted).toEqual([]);
+ expect(vm.multiDeleteItems).toHaveBeenCalledWith({
+ path: bulkDeletePath,
+ items: [firstImage.tag, secondImage.tag],
+ });
+ done();
+ });
+ });
+ });
+ });
});
describe('delete registry', () => {
- it('should be possible to delete a registry', () => {
- expect(findDeleteBtn()).toBeDefined();
+ beforeEach(() => {
+ vm.itemsToBeDeleted = [0];
});
- it('should call deleteItem and reset itemToBeDeleted when confirming deletion', done => {
- findDeleteBtn().click();
- spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve());
+ it('should be possible to delete a registry', done => {
+ Vue.nextTick(() => {
+ expect(vm.itemsToBeDeleted).toEqual([0]);
+ expect(findDeleteBtn()).toBeDefined();
+ expect(findDeleteBtn().disabled).toBe(false);
+ expect(findDeleteBtnRow()).toBeDefined();
+ done();
+ });
+ });
+ it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => {
Vue.nextTick(() => {
- document.querySelector(`#${vm.modalId} .btn-danger`).click();
+ expect(vm.itemsToBeDeleted).toEqual([0]);
+ expect(findDeleteBtn().disabled).toBe(false);
+ findDeleteBtn().click();
+ spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve());
- expect(vm.deleteItem).toHaveBeenCalledWith(firstImage);
- expect(vm.itemToBeDeleted).toBeNull();
- done();
+ Vue.nextTick(() => {
+ confirmationModal('.btn-danger').click();
+
+ expect(vm.itemsToBeDeleted).toEqual([]);
+ expect(vm.multiDeleteItems).toHaveBeenCalledWith({
+ path: bulkDeletePath,
+ items: [firstImage.tag],
+ });
+ done();
+ });
});
});
});
@@ -65,4 +163,27 @@ describe('table registry', () => {
expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
});
});
+
+ describe('modal content', () => {
+ it('should show the singular title and image name when deleting a single image', done => {
+ findDeleteBtnRow().click();
+
+ Vue.nextTick(() => {
+ expect(vm.modalTitle).toBe('Remove image');
+ expect(vm.modalDescription).toContain(firstImage.tag);
+ done();
+ });
+ });
+
+ it('should show the plural title and image count when deleting more than one image', done => {
+ selectAllCheckboxes();
+ vm.setModalDescription();
+
+ Vue.nextTick(() => {
+ expect(vm.modalTitle).toBe('Remove images');
+ expect(vm.modalDescription).toContain('<b>2</b> images');
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js
index 22db203e77f..130ab298e89 100644
--- a/spec/javascripts/registry/mock_data.js
+++ b/spec/javascripts/registry/mock_data.js
@@ -108,6 +108,17 @@ export const repoPropsData = {
destroyPath: 'path',
canDelete: true,
},
+ {
+ tag: 'test-image',
+ revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
+ shortRevision: 'b969de599',
+ size: 19,
+ layers: 10,
+ location: 'location-2',
+ createdAt: 1505828744434,
+ destroyPath: 'path-2',
+ canDelete: true,
+ },
],
location: 'location',
name: 'foo',
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 ba3ba01944d..53e1f077610 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
@@ -236,24 +236,26 @@ describe('ReadyToMerge', () => {
});
});
- describe('shouldShowMergeOptionsDropdown', () => {
- it('should return false when no auto merge strategies are available', () => {
- Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
+ describe('shouldShowMergeImmediatelyDropdown', () => {
+ it('should return false if no pipeline is active', () => {
+ Vue.set(vm.mr, 'isPipelineActive', false);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false);
- expect(vm.shouldShowMergeOptionsDropdown).toBe(false);
+ expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false);
});
- it('should return true when at least one auto merge strategy is available', () => {
- Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]);
+ it('should return false if "Pipelines must succeed" is enabled for the current project', () => {
+ Vue.set(vm.mr, 'isPipelineActive', true);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true);
- expect(vm.shouldShowMergeOptionsDropdown).toBe(true);
+ expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false);
});
- it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => {
- Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]);
- Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true);
+ it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => {
+ Vue.set(vm.mr, 'isPipelineActive', true);
+ Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false);
- expect(vm.shouldShowMergeOptionsDropdown).toBe(false);
+ expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true);
});
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 253413ae43e..a55d5537df7 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -233,6 +233,8 @@ export default {
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
+ merge_train_when_pipeline_succeeds_docs_path:
+ '/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds',
squash: true,
visual_review_app_available: true,
merge_trains_enabled: true,
diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
index 542a9ced6d7..66bbcbf7292 100644
--- a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
@@ -12,7 +12,7 @@ describe Banzai::Filter::InlineMetricsFilter do
let(:url) { 'https://foo.com' }
it 'leaves regular non-metrics links unchanged' do
- expect(doc.to_s).to eq input
+ expect(doc.to_s).to eq(input)
end
end
@@ -21,7 +21,7 @@ describe Banzai::Filter::InlineMetricsFilter do
let(:url) { urls.metrics_namespace_project_environment_url(*params) }
it 'leaves the original link unchanged' do
- expect(doc.at_css('a').to_s).to eq input
+ expect(doc.at_css('a').to_s).to eq(input)
end
it 'appends a metrics charts placeholder with dashboard url after metrics links' do
@@ -29,7 +29,7 @@ describe Banzai::Filter::InlineMetricsFilter do
expect(node).to be_present
dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params, embedded: true)
- expect(node.attribute('data-dashboard-url').to_s).to eq dashboard_url
+ expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
end
context 'when the metrics dashboard link is part of a paragraph' do
@@ -37,9 +37,34 @@ describe Banzai::Filter::InlineMetricsFilter do
let(:input) { %(<p>#{paragraph}</p>) }
it 'appends the charts placeholder after the enclosing paragraph' do
- expect(doc.at_css('p').to_s).to include paragraph
+ expect(doc.at_css('p').to_s).to include(paragraph)
expect(doc.at_css('.js-render-metrics')).to be_present
end
end
+
+ context 'with dashboard params specified' do
+ let(:params) do
+ [
+ 'foo',
+ 'bar',
+ 12,
+ {
+ embedded: true,
+ dashboard: 'config/prometheus/common_metrics.yml',
+ group: 'System metrics (Kubernetes)',
+ title: 'Core Usage (Pod Average)',
+ y_label: 'Cores per Pod'
+ }
+ ]
+ end
+
+ it 'appends a metrics charts placeholder with dashboard url after metrics links' do
+ node = doc.at_css('.js-render-metrics')
+ expect(node).to be_present
+
+ dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params)
+ expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
+ end
+ end
end
end
diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb
index 2dcc933ee61..1874296df8c 100644
--- a/spec/lib/bitbucket/representation/comment_spec.rb
+++ b/spec/lib/bitbucket/representation/comment_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Bitbucket::Representation::Comment do
describe '#author' do
- it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') }
+ it { expect(described_class.new('user' => { 'nickname' => 'Ben' }).author).to eq('Ben') }
it { expect(described_class.new({}).author).to be_nil }
end
diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb
index c7d1ebdd597..655b9b78b47 100644
--- a/spec/lib/bitbucket/representation/issue_spec.rb
+++ b/spec/lib/bitbucket/representation/issue_spec.rb
@@ -17,7 +17,7 @@ describe Bitbucket::Representation::Issue do
end
describe '#author' do
- it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+ it { expect(described_class.new({ 'reporter' => { 'nickname' => 'Ben' } }).author).to eq('Ben') }
it { expect(described_class.new({}).author).to be_nil }
end
diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb
index 7cf43b2b6fd..70b51b8efec 100644
--- a/spec/lib/bitbucket/representation/pull_request_spec.rb
+++ b/spec/lib/bitbucket/representation/pull_request_spec.rb
@@ -8,7 +8,7 @@ describe Bitbucket::Representation::PullRequest do
end
describe '#author' do
- it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+ it { expect(described_class.new({ 'author' => { 'nickname' => 'Ben' } }).author).to eq('Ben') }
it { expect(described_class.new({}).author).to be_nil }
end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 099d7b6b67c..394efa85701 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -4,62 +4,131 @@ require 'spec_helper'
describe ExpandVariables do
describe '#expand' do
- subject { described_class.expand(value, variables) }
+ context 'table tests' do
+ using RSpec::Parameterized::TableSyntax
- tests = [
- { value: 'key',
- result: 'key',
- variables: [] },
- { value: 'key$variable',
- result: 'key',
- variables: [] },
- { value: 'key$variable',
- result: 'keyvalue',
- variables: [
- { key: 'variable', value: 'value' }
- ] },
- { value: 'key${variable}',
- result: 'keyvalue',
- variables: [
- { key: 'variable', value: 'value' }
- ] },
- { value: 'key$variable$variable2',
- result: 'keyvalueresult',
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' }
- ] },
- { value: 'key${variable}${variable2}',
- result: 'keyvalueresult',
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' }
- ] },
- { value: 'key$variable2$variable',
- result: 'keyresultvalue',
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' }
- ] },
- { value: 'key${variable2}${variable}',
- result: 'keyresultvalue',
- variables: [
- { key: 'variable', value: 'value' },
- { key: 'variable2', value: 'result' }
- ] },
- { value: 'review/$CI_COMMIT_REF_NAME',
- result: 'review/feature/add-review-apps',
- variables: [
- { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
- ] }
- ]
+ where do
+ {
+ "no expansion": {
+ value: 'key',
+ result: 'key',
+ variables: []
+ },
+ "missing variable": {
+ value: 'key$variable',
+ result: 'key',
+ variables: []
+ },
+ "simple expansion": {
+ value: 'key$variable',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ "simple with hash of variables": {
+ value: 'key$variable',
+ result: 'keyvalue',
+ variables: {
+ 'variable' => 'value'
+ }
+ },
+ "complex expansion": {
+ value: 'key${variable}',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ "simple expansions": {
+ value: 'key$variable$variable2',
+ result: 'keyvalueresult',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ "complex expansions": {
+ value: 'key${variable}${variable2}',
+ result: 'keyvalueresult',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ "complex expansions with missing variable": {
+ value: 'key${variable}${variable2}',
+ result: 'keyvalue',
+ variables: [
+ { key: 'variable', value: 'value' }
+ ]
+ },
+ "out-of-order expansion": {
+ value: 'key$variable2$variable',
+ result: 'keyresultvalue',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ "out-of-order complex expansion": {
+ value: 'key${variable2}${variable}',
+ result: 'keyresultvalue',
+ variables: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'result' }
+ ]
+ },
+ "review-apps expansion": {
+ value: 'review/$CI_COMMIT_REF_NAME',
+ result: 'review/feature/add-review-apps',
+ variables: [
+ { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
+ ]
+ },
+ "do not lazily access variables when no expansion": {
+ value: 'key',
+ result: 'key',
+ variables: -> { raise NotImplementedError }
+ },
+ "lazily access variables": {
+ value: 'key$variable',
+ result: 'keyvalue',
+ variables: -> { [{ key: 'variable', value: 'value' }] }
+ }
+ }
+ end
+
+ with_them do
+ subject { ExpandVariables.expand(value, variables) } # rubocop:disable RSpec/DescribedClass
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ context 'lazily inits variables' do
+ let(:variables) { -> { [{ key: 'variable', value: 'result' }] } }
+
+ subject { described_class.expand(value, variables) }
+
+ context 'when expanding variable' do
+ let(:value) { 'key$variable$variable2' }
+
+ it 'calls block at most once' do
+ expect(variables).to receive(:call).once.and_call_original
+
+ is_expected.to eq('keyresult')
+ end
+ end
+
+ context 'when no expansion is needed' do
+ let(:value) { 'key' }
- tests.each do |test|
- context "#{test[:value]} resolves to #{test[:result]}" do
- let(:value) { test[:value] }
- let(:variables) { test[:variables] }
+ it 'does not call block' do
+ expect(variables).not_to receive(:call)
- it { is_expected.to eq(test[:result]) }
+ is_expected.to eq('key')
+ end
end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 280941ff601..3d0d3f91859 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -25,12 +25,12 @@ describe Gitlab::BitbucketImport::Importer do
let(:reporters) do
[
nil,
- { "username" => "reporter1" },
+ { "nickname" => "reporter1" },
nil,
- { "username" => "reporter2" },
- { "username" => "reporter1" },
+ { "nickname" => "reporter2" },
+ { "nickname" => "reporter1" },
nil,
- { "username" => "reporter3" }
+ { "nickname" => "reporter3" }
]
end
@@ -115,6 +115,7 @@ describe Gitlab::BitbucketImport::Importer do
created_at: Time.now,
updated_at: Time.now)
end
+ let(:author_line) { "*Created by: someuser*\n\n" }
before do
allow(subject).to receive(:import_wiki)
@@ -128,7 +129,7 @@ describe Gitlab::BitbucketImport::Importer do
old_pos: nil,
new_pos: 4,
note: 'Hello world',
- author: 'root',
+ author: 'someuser',
created_at: Time.now,
updated_at: Time.now,
inline?: true,
@@ -139,7 +140,7 @@ describe Gitlab::BitbucketImport::Importer do
iid: 3,
file_path: '.gitmodules',
note: 'Hello world',
- author: 'root',
+ author: 'someuser',
created_at: Time.now,
updated_at: Time.now,
inline?: true,
@@ -163,11 +164,33 @@ describe Gitlab::BitbucketImport::Importer do
notes = merge_request.notes.order(:id).to_a
start_note = notes.first
expect(start_note).to be_a(DiffNote)
- expect(start_note.note).to eq(@inline_note.note)
+ expect(start_note.note).to include(@inline_note.note)
+ expect(start_note.note).to include(author_line)
reply_note = notes.last
expect(reply_note).to be_a(DiffNote)
- expect(reply_note.note).to eq(@reply.note)
+ expect(reply_note.note).to include(@reply.note)
+ expect(reply_note.note).to include(author_line)
+ end
+
+ context 'when user exists in GitLab' do
+ let!(:existing_user) { create(:user, username: 'someuser') }
+ let!(:identity) { create(:identity, provider: 'bitbucket', extern_uid: existing_user.username, user: existing_user) }
+
+ it 'does not add author line to comments' do
+ expect { subject.execute }.to change { MergeRequest.count }.by(1)
+
+ merge_request = MergeRequest.first
+
+ notes = merge_request.notes.order(:id).to_a
+ start_note = notes.first
+ expect(start_note.note).to eq(@inline_note.note)
+ expect(start_note.note).not_to include(author_line)
+
+ reply_note = notes.last
+ expect(reply_note.note).to eq(@reply.note)
+ expect(reply_note.note).not_to include(author_line)
+ end
end
context 'when importing a pull request throws an exception' do
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb
index b6b3de4bc4a..c8afcbd053d 100644
--- a/spec/lib/gitlab/ci/ansi2html_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2html_spec.rb
@@ -209,7 +209,7 @@ describe Gitlab::Ci::Ansi2html do
let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
let(:section_start_html) do
- '<div class="js-section-start fa fa-caret-down pr-2 cursor-pointer"' \
+ '<div class="js-section-start section-start fa fa-caret-down pr-2 cursor-pointer"' \
" data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{class_name(section_name)}\"" \
' role="button"></div>'
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 800ef122203..415ade7a096 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -89,14 +89,23 @@ describe Gitlab::Ci::Config::Entry::Job do
context 'when has needs' do
let(:config) do
- { script: 'echo', needs: ['another-job'] }
+ {
+ stage: 'test',
+ script: 'echo',
+ needs: ['another-job']
+ }
end
it { expect(entry).to be_valid }
context 'when has dependencies' do
let(:config) do
- { script: 'echo', dependencies: ['another-job'], needs: ['another-job'] }
+ {
+ stage: 'test',
+ script: 'echo',
+ dependencies: ['another-job'],
+ needs: ['another-job']
+ }
end
it { expect(entry).to be_valid }
@@ -256,7 +265,11 @@ describe Gitlab::Ci::Config::Entry::Job do
context 'when has needs' do
context 'that are not a array of strings' do
let(:config) do
- { script: 'echo', needs: 'build-job' }
+ {
+ stage: 'test',
+ script: 'echo',
+ needs: 'build-job'
+ }
end
it 'returns error about invalid type' do
@@ -267,7 +280,12 @@ describe Gitlab::Ci::Config::Entry::Job do
context 'when have dependencies that are not subset of needs' do
let(:config) do
- { script: 'echo', dependencies: ['another-job'], needs: ['build-job'] }
+ {
+ stage: 'test',
+ script: 'echo',
+ dependencies: ['another-job'],
+ needs: ['build-job']
+ }
end
it 'returns error about invalid data' do
@@ -275,6 +293,20 @@ describe Gitlab::Ci::Config::Entry::Job do
expect(entry.errors).to include 'job dependencies the another-job should be part of needs'
end
end
+
+ context 'when stage: is missing' do
+ let(:config) do
+ {
+ script: 'echo',
+ needs: ['build-job']
+ }
+ end
+
+ it 'returns error about invalid data' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job config missing required keys: stage'
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 762025f9bd9..5d4dec5899a 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -386,17 +386,28 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
describe 'applying needs: dependency' do
subject { seed_build }
+ let(:needs_count) { 1 }
+
+ let(:needs_attributes) do
+ Array.new(needs_count, name: 'build')
+ end
+
let(:attributes) do
{
name: 'rspec',
- needs_attributes: [{
- name: 'build'
- }]
+ needs_attributes: needs_attributes
}
end
context 'when build job is not present in prior stages' do
- it { is_expected.not_to be_included }
+ it "is included" do
+ is_expected.to be_included
+ end
+
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "rspec: needs 'build'")
+ end
end
context 'when build job is part of prior stages' do
@@ -414,7 +425,39 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
let(:previous_stages) { [stage_seed] }
- it { is_expected.to be_included }
+ it "is included" do
+ is_expected.to be_included
+ end
+
+ it "does not have errors" do
+ expect(subject.errors).to be_empty
+ end
+ end
+
+ context 'when lower limit of needs is reached' do
+ before do
+ stub_feature_flags(ci_dag_limit_needs: true)
+ end
+
+ let(:needs_count) { described_class::LOW_NEEDS_LIMIT + 1 }
+
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "rspec: one job can only need 5 others, but you have listed 6. See needs keyword documentation for more details")
+ end
+ end
+
+ context 'when upper limit of needs is reached' do
+ before do
+ stub_feature_flags(ci_dag_limit_needs: false)
+ end
+
+ let(:needs_count) { described_class::HARD_NEEDS_LIMIT + 1 }
+
+ it "returns an error" do
+ expect(subject.errors).to contain_exactly(
+ "rspec: one job can only need 50 others, but you have listed 51. See needs keyword documentation for more details")
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index 6fba9f37d91..a13335f63d5 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -121,6 +121,16 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end
end
+ describe '#seeds_errors' do
+ it 'returns all errors from seeds' do
+ expect(subject.seeds.first)
+ .to receive(:errors) { ["build error"] }
+
+ expect(subject.errors).to contain_exactly(
+ "build error")
+ end
+ end
+
describe '#to_resource' do
it 'builds a valid stage object with all builds' do
subject.to_resource.save!
diff --git a/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb b/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb
index 180520b27e7..6ed9dda08d7 100644
--- a/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb
+++ b/spec/lib/gitlab/cluster/puma_worker_killer_observer_spec.rb
@@ -17,9 +17,7 @@ describe Gitlab::Cluster::PumaWorkerKillerObserver do
it 'increments timeout counter' do
worker = double(index: 0)
- expect(counter)
- .to receive(:increment)
- .with({ worker: 'worker_0' })
+ expect(counter).to receive(:increment)
subject.callback.call(worker)
end
diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb
index 82614bb8238..6b548d5c4a8 100644
--- a/spec/lib/gitlab/config/entry/attributable_spec.rb
+++ b/spec/lib/gitlab/config/entry/attributable_spec.rb
@@ -25,7 +25,9 @@ describe Gitlab::Config::Entry::Attributable do
end
it 'returns the value of config' do
+ expect(instance).to have_name
expect(instance.name).to eq 'some name'
+ expect(instance).to have_test
expect(instance.test).to eq 'some test'
end
@@ -42,6 +44,7 @@ describe Gitlab::Config::Entry::Attributable do
end
it 'returns nil' do
+ expect(instance).not_to have_test
expect(instance.test).to be_nil
end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index f11f68ab3c2..2990594c538 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -101,13 +101,13 @@ describe Gitlab::Danger::Helper do
describe '#changes_by_category' do
it 'categorizes changed files' do
- expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] }
+ expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/migrate/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] }
allow(fake_git).to receive(:modified_files) { [] }
allow(fake_git).to receive(:renamed_files) { [] }
expect(helper.changes_by_category).to eq(
backend: %w[foo.rb],
- database: %w[db/foo lib/gitlab/database/foo.rb],
+ database: %w[db/migrate/foo lib/gitlab/database/foo.rb],
frontend: %w[foo.js],
none: %w[ee/changelogs/foo.yml foo.md],
qa: %w[qa/foo],
@@ -173,8 +173,13 @@ describe Gitlab::Danger::Helper do
'ee/FOO_VERSION' | :unknown
- 'db/foo' | :database
- 'ee/db/foo' | :database
+ 'db/schema.rb' | :database
+ 'db/migrate/foo' | :database
+ 'db/post_migrate/foo' | :database
+ 'ee/db/migrate/foo' | :database
+ 'ee/db/post_migrate/foo' | :database
+ 'ee/db/geo/migrate/foo' | :database
+ 'ee/db/geo/post_migrate/foo' | :database
'app/models/project_authorization.rb' | :database
'app/services/users/refresh_authorized_projects_service.rb' | :database
'lib/gitlab/background_migration.rb' | :database
@@ -188,6 +193,9 @@ describe Gitlab::Danger::Helper do
'lib/gitlab/sql/foo' | :database
'rubocop/cop/migration/foo' | :database
+ 'db/fixtures/foo.rb' | :backend
+ 'ee/db/fixtures/foo.rb' | :backend
+
'qa/foo' | :qa
'ee/qa/foo' | :qa
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index cc31f88d365..e8a9f0b06a8 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -3,9 +3,43 @@
require 'spec_helper'
describe Gitlab::DataBuilder::Push do
+ include RepoHelpers
+
let(:project) { create(:project, :repository) }
let(:user) { build(:user, public_email: 'public-email@example.com') }
+ describe '.build' do
+ let(:sample) { RepoHelpers.sample_compare }
+ let(:commits) { project.repository.commits_between(sample.commits.first, sample.commits.last) }
+ let(:subject) do
+ described_class.build(project: project,
+ user: user,
+ ref: sample.target_branch,
+ commits: commits,
+ commits_count: commits.length,
+ message: 'test message',
+ with_changed_files: with_changed_files)
+ end
+
+ context 'with changed files' do
+ let(:with_changed_files) { true }
+
+ it 'returns commit hook data' do
+ expect(subject[:project]).to eq(project.hook_attrs)
+ expect(subject[:commits].first.keys).to include(*%i(added removed modified))
+ end
+ end
+
+ context 'without changed files' do
+ let(:with_changed_files) { false }
+
+ it 'returns commit hook data without include deltas' do
+ expect(subject[:project]).to eq(project.hook_attrs)
+ expect(subject[:commits].first.keys).not_to include(*%i(added removed modified))
+ end
+ end
+ end
+
describe '.build_sample' do
let(:data) { described_class.build_sample(project, user) }
diff --git a/spec/lib/gitlab/git_post_receive_spec.rb b/spec/lib/gitlab/git_post_receive_spec.rb
new file mode 100644
index 00000000000..4c20d945585
--- /dev/null
+++ b/spec/lib/gitlab/git_post_receive_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::GitPostReceive do
+ let(:project) { create(:project) }
+
+ subject { described_class.new(project, "project-#{project.id}", changes.dup, {}) }
+
+ describe '#includes_branches?' do
+ context 'with no branches' do
+ let(:changes) do
+ <<~EOF
+ 654321 210987 refs/nobranches/tag1
+ 654322 210986 refs/tags/test1
+ 654323 210985 refs/merge-requests/mr1
+ EOF
+ end
+
+ it 'returns false' do
+ expect(subject.includes_branches?).to be_falsey
+ end
+ end
+
+ context 'with branches' do
+ let(:changes) do
+ <<~EOF
+ 654322 210986 refs/heads/test1
+ 654321 210987 refs/tags/tag1
+ 654323 210985 refs/merge-requests/mr1
+ EOF
+ end
+
+ it 'returns true' do
+ expect(subject.includes_branches?).to be_truthy
+ end
+ end
+
+ context 'with malformed changes' do
+ let(:changes) do
+ <<~EOF
+ ref/heads/1 a
+ somebranch refs/heads/2
+ EOF
+ end
+
+ it 'returns false' do
+ expect(subject.includes_branches?).to be_falsey
+ end
+ end
+ end
+
+ describe '#includes_tags?' do
+ context 'with no tags' do
+ let(:changes) do
+ <<~EOF
+ 654321 210987 refs/notags/tag1
+ 654322 210986 refs/heads/test1
+ 654323 210985 refs/merge-requests/mr1
+ EOF
+ end
+
+ it 'returns false' do
+ expect(subject.includes_tags?).to be_falsey
+ end
+ end
+
+ context 'with tags' do
+ let(:changes) do
+ <<~EOF
+ 654322 210986 refs/heads/test1
+ 654321 210987 refs/tags/tag1
+ 654323 210985 refs/merge-requests/mr1
+ EOF
+ end
+
+ it 'returns true' do
+ expect(subject.includes_tags?).to be_truthy
+ end
+ end
+
+ context 'with malformed changes' do
+ let(:changes) do
+ <<~EOF
+ ref/tags/1 a
+ sometag refs/tags/2
+ EOF
+ end
+
+ it 'returns false' do
+ expect(subject.includes_tags?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/notification_service_spec.rb b/spec/lib/gitlab/gitaly_client/notification_service_spec.rb
deleted file mode 100644
index ffc3a09be30..00000000000
--- a/spec/lib/gitlab/gitaly_client/notification_service_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitalyClient::NotificationService do
- describe '#post_receive' do
- let(:project) { create(:project) }
- let(:storage_name) { project.repository_storage }
- let(:relative_path) { project.disk_path + '.git' }
- subject { described_class.new(project.repository) }
-
- it 'sends a post_receive message' do
- expect_any_instance_of(Gitaly::NotificationService::Stub)
- .to receive(:post_receive).with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
-
- subject.post_receive
- end
- end
-end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index fddb5066d6f..3c6b17c10ec 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -242,6 +242,7 @@ project:
- cluster_project
- cluster_ingresses
- creator
+- cycle_analytics_stages
- group
- namespace
- boards
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 6d70b147666..a211675bbf2 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2450,7 +2450,21 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": []
+ "events": [],
+ "suggestions": [
+ {
+ "id": 1,
+ "note_id": 674,
+ "relative_order": 0,
+ "applied": false,
+ "commit_id": null,
+ "from_content": "Original line\n",
+ "to_content": "New line\n",
+ "lines_above": 0,
+ "lines_below": 0,
+ "outdated": false
+ }
+ ]
},
{
"id": 675,
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 baec24590b4..d6e1fbaa979 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -125,6 +125,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty
end
+ it 'restores suggestion' do
+ note = Note.find_by("note LIKE 'Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum%'")
+
+ expect(note.suggestions.count).to eq(1)
+ expect(note.suggestions.first.from_content).to eq("Original line\n")
+ end
+
context 'event at forth level of the tree' do
let(:event) { Event.where(action: 6).first }
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 06c8d127951..fce2aded786 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'generates the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
- expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.14.3-kube-1.11.10')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
new file mode 100644
index 00000000000..f24ab5579df
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Kubernetes::KubectlCmd do
+ describe '.delete' do
+ it 'constructs string properly' do
+ args = %w(resource_type type --flag-1 --flag-2)
+
+ expected_command = 'kubectl delete resource_type type --flag-1 --flag-2'
+
+ expect(described_class.delete(*args)).to eq expected_command
+ end
+ end
+
+ describe '.apply_file' do
+ context 'without optional args' do
+ it 'requires filename to be present' do
+ expect { described_class.apply_file(nil) }.to raise_error(ArgumentError, "filename is not present")
+ expect { described_class.apply_file(" ") }.to raise_error(ArgumentError, "filename is not present")
+ end
+
+ it 'constructs string properly' do
+ expected_command = 'kubectl apply -f filename'
+
+ expect(described_class.apply_file('filename')).to eq expected_command
+ end
+ end
+
+ context 'with optional args' do
+ it 'constructs command properly with many args' do
+ args = %w(arg-1 --flag-0-1 arg-2 --flag-0-2)
+
+ expected_command = 'kubectl apply -f filename arg-1 --flag-0-1 arg-2 --flag-0-2'
+
+ expect(described_class.apply_file('filename', *args)).to eq expected_command
+ end
+
+ it 'constructs command properly with single arg' do
+ args = "arg-1"
+
+ expected_command = 'kubectl apply -f filename arg-1'
+
+ expect(described_class.apply_file('filename', args)).to eq(expected_command)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
index 34bc6359414..e0dc6d98efc 100644
--- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -9,14 +9,22 @@ describe Gitlab::Metrics::Dashboard::Url do
end
it 'matches a metrics dashboard link with named params' do
- url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url('foo', 'bar', 1, start: 123345456, anchor: 'title')
+ url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(
+ 'foo',
+ 'bar',
+ 1,
+ start: '2019-08-02T05:43:09.000Z',
+ dashboard: 'config/prometheus/common_metrics.yml',
+ group: 'awesome group',
+ anchor: 'title'
+ )
expected_params = {
'url' => url,
'namespace' => 'foo',
'project' => 'bar',
'environment' => '1',
- 'query' => '?start=123345456',
+ 'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
'anchor' => '#title'
}
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
index f4a6e1fc7d9..b8add3c1324 100644
--- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -46,8 +46,6 @@ describe Gitlab::Metrics::Samplers::PumaSampler do
expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 2)
expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 2)
expect(subject.metrics[:puma_stale_workers]).to receive(:set).with(labels, 0)
- expect(subject.metrics[:puma_phase]).to receive(:set).once.with(labels, 2)
- expect(subject.metrics[:puma_phase]).to receive(:set).once.with({ worker: 'worker_0' }, 1)
subject.sample
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 4a41d5cf51e..c7462500c82 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -22,6 +22,28 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.query).to eq('hello world') }
end
+ describe '#formatted_count' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:results) { described_class.new(user, project, query) }
+
+ where(:scope, :count_method, :expected) do
+ 'blobs' | :blobs_count | '1234'
+ 'notes' | :limited_notes_count | '1000+'
+ 'wiki_blobs' | :wiki_blobs_count | '1234'
+ 'commits' | :commits_count | '1234'
+ 'projects' | :limited_projects_count | '1000+'
+ 'unknown' | nil | nil
+ end
+
+ with_them do
+ it 'returns the expected formatted count' do
+ expect(results).to receive(count_method).and_return(1234) if count_method
+ expect(results.formatted_count(scope)).to eq(expected)
+ end
+ end
+ end
+
shared_examples 'general blob search' do |entity_type, blob_kind|
let(:query) { 'files' }
subject(:results) { described_class.new(user, project, query).objects(blob_type) }
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 8b82ea7faa5..c7c82d07508 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -28,6 +28,18 @@ describe Gitlab::ProjectTemplate do
end
end
+ describe '#project_path' do
+ subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').project_path }
+
+ it { is_expected.to eq 'some/project/path' }
+ end
+
+ describe '#uri_encoded_project_path' do
+ subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').uri_encoded_project_path }
+
+ it { is_expected.to eq 'some%2Fproject%2Fpath' }
+ end
+
describe '.find' do
subject { described_class.find(query) }
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 3d27156b356..c287da19343 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -29,6 +29,43 @@ describe Gitlab::SearchResults do
end
end
+ describe '#formatted_count' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:scope, :count_method, :expected) do
+ 'projects' | :limited_projects_count | '1000+'
+ 'issues' | :limited_issues_count | '1000+'
+ 'merge_requests' | :limited_merge_requests_count | '1000+'
+ 'milestones' | :limited_milestones_count | '1000+'
+ 'users' | :limited_users_count | '1000+'
+ 'unknown' | nil | nil
+ end
+
+ with_them do
+ it 'returns the expected formatted count' do
+ expect(results).to receive(count_method).and_return(1234) if count_method
+ expect(results.formatted_count(scope)).to eq(expected)
+ end
+ end
+ end
+
+ describe '#formatted_limited_count' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:count, :expected) do
+ 23 | '23'
+ 1000 | '1000'
+ 1001 | '1000+'
+ 1234 | '1000+'
+ end
+
+ with_them do
+ it 'returns the expected formatted limited count' do
+ expect(results.formatted_limited_count(count)).to eq(expected)
+ end
+ end
+ end
+
context "when count_limit is lower than total amount" do
before do
allow(results).to receive(:count_limit).and_return(1)
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index b661a894c0c..35df38f052b 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -16,4 +16,22 @@ describe Gitlab::SnippetSearchResults do
expect(results.snippet_blobs_count).to eq(1)
end
end
+
+ describe '#formatted_count' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:scope, :count_method, :expected) do
+ 'snippet_titles' | :snippet_titles_count | '1234'
+ 'snippet_blobs' | :snippet_blobs_count | '1234'
+ 'projects' | :limited_projects_count | '1000+'
+ 'unknown' | nil | nil
+ end
+
+ with_them do
+ it 'returns the expected formatted count' do
+ expect(results).to receive(count_method).and_return(1234) if count_method
+ expect(results.formatted_count(scope)).to eq(expected)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/snowplow_tracker_spec.rb b/spec/lib/gitlab/snowplow_tracker_spec.rb
new file mode 100644
index 00000000000..073a33e5973
--- /dev/null
+++ b/spec/lib/gitlab/snowplow_tracker_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::SnowplowTracker do
+ let(:timestamp) { Time.utc(2017, 3, 22) }
+
+ around do |example|
+ Timecop.freeze(timestamp) { example.run }
+ end
+
+ subject { described_class.track_event('epics', 'action', property: 'what', value: 'doit') }
+
+ context '.track_event' do
+ context 'when Snowplow tracker is disabled' do
+ it 'does not track the event' do
+ expect(SnowplowTracker::Tracker).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ context 'when Snowplow tracker is enabled' do
+ before do
+ stub_application_setting(snowplow_enabled: true)
+ stub_application_setting(snowplow_site_id: 'awesome gitlab')
+ stub_application_setting(snowplow_collector_hostname: 'url.com')
+ end
+
+ it 'tracks the event' do
+ tracker = double
+
+ expect(::SnowplowTracker::Tracker).to receive(:new)
+ .with(
+ an_instance_of(::SnowplowTracker::Emitter),
+ an_instance_of(::SnowplowTracker::Subject),
+ 'cf', 'awesome gitlab'
+ ).and_return(tracker)
+ expect(tracker).to receive(:track_struct_event)
+ .with('epics', 'action', nil, 'what', 'doit', nil, timestamp.to_i)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb
new file mode 100644
index 00000000000..71be37692e2
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::UsageDataCounters::CycleAnalyticsCounter do
+ it_behaves_like 'a redis usage counter', 'CycleAnalytics', :views
+
+ it_behaves_like 'a redis usage counter with totals', :cycle_analytics, views: 3
+end
diff --git a/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb
new file mode 100644
index 00000000000..47077345e0c
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::UsageDataCounters::SourceCodeCounter do
+ it_behaves_like 'a redis usage counter', 'Source Code', :pushes
+
+ it_behaves_like 'a redis usage counter with totals', :source_code, pushes: 5
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index bf36273251b..588c68d1fb0 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -59,6 +59,7 @@ describe Gitlab::UsageData do
avg_cycle_analytics
influxdb_metrics_enabled
prometheus_metrics_enabled
+ cycle_analytics_views
))
expect(subject).to include(
@@ -71,7 +72,9 @@ describe Gitlab::UsageData do
web_ide_views: a_kind_of(Integer),
web_ide_commits: a_kind_of(Integer),
web_ide_merge_requests: a_kind_of(Integer),
- navbar_searches: a_kind_of(Integer)
+ navbar_searches: a_kind_of(Integer),
+ cycle_analytics_views: a_kind_of(Integer),
+ source_code_pushes: a_kind_of(Integer)
)
end
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
new file mode 100644
index 00000000000..4e3923e82b1
--- /dev/null
+++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Analytics::CycleAnalytics::ProjectStage do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+end
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index d4f8b552088..00b5c72a3d3 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -23,7 +23,7 @@ describe Clusters::Applications::Helm do
Clusters::Cluster::APPLICATIONS.keys.each do |application_name|
next if application_name == 'helm'
- it do
+ it "is false when #{application_name} is installed" do
cluster_application = create("clusters_applications_#{application_name}".to_sym)
helm = cluster_application.cluster.application_helm
@@ -31,6 +31,14 @@ describe Clusters::Applications::Helm do
expect(helm.allowed_to_uninstall?).to be_falsy
end
end
+
+ it 'executes a single query only' do
+ cluster_application = create(:clusters_applications_ingress)
+ helm = cluster_application.cluster.application_helm
+
+ query_count = ActiveRecord::QueryRecorder.new { helm.allowed_to_uninstall? }.count
+ expect(query_count).to eq(1)
+ end
end
context "without other existing applications" do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 53424204db7..d344a6d0f0d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -3015,9 +3015,6 @@ describe MergeRequest do
subject { merge_request.rebase_in_progress? }
it do
- # Stub out the legacy gitaly implementation
- allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false }
-
allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid }
merge_request.rebase_jid = rebase_jid
@@ -3027,42 +3024,6 @@ describe MergeRequest do
end
end
- describe '#gitaly_rebase_in_progress?' do
- let(:repo_path) do
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- subject.source_project.repository.path
- end
- end
- let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
-
- before do
- system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master))
- end
-
- it 'returns true when there is a current rebase directory' do
- expect(subject.rebase_in_progress?).to be_truthy
- end
-
- it 'returns false when there is no rebase directory' do
- FileUtils.rm_rf(rebase_path)
-
- expect(subject.rebase_in_progress?).to be_falsey
- end
-
- it 'returns false when the rebase directory has expired' do
- time = 20.minutes.ago.to_time
- File.utime(time, time, rebase_path)
-
- expect(subject.rebase_in_progress?).to be_falsey
- end
-
- it 'returns false when the source project has been removed' do
- allow(subject).to receive(:source_project).and_return(nil)
-
- expect(subject.rebase_in_progress?).to be_falsey
- end
- end
-
describe '#allow_collaboration' do
let(:merge_request) do
build(:merge_request, source_branch: 'fixes', allow_collaboration: true)
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 2b9c3c43af9..972f26ac745 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -853,4 +853,64 @@ describe Namespace do
it { is_expected.to be_falsy }
end
end
+
+ describe '#emails_disabled?' do
+ context 'when not a subgroup' do
+ it 'returns false' do
+ group = create(:group, emails_disabled: false)
+
+ expect(group.emails_disabled?).to be_falsey
+ end
+
+ it 'returns true' do
+ group = create(:group, emails_disabled: true)
+
+ expect(group.emails_disabled?).to be_truthy
+ end
+ end
+
+ context 'when a subgroup' do
+ let(:grandparent) { create(:group) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let(:group) { create(:group, parent: parent) }
+
+ it 'returns false' do
+ expect(group.emails_disabled?).to be_falsey
+ end
+
+ context 'when ancestor emails are disabled' do
+ it 'returns true' do
+ grandparent.update_attribute(:emails_disabled, true)
+
+ expect(group.emails_disabled?).to be_truthy
+ end
+ end
+ end
+
+ context 'when :emails_disabled feature flag is off' do
+ before do
+ stub_feature_flags(emails_disabled: false)
+ end
+
+ context 'when not a subgroup' do
+ it 'returns false' do
+ group = create(:group, emails_disabled: true)
+
+ expect(group.emails_disabled?).to be_falsey
+ end
+ end
+
+ context 'when a subgroup and ancestor emails are disabled' do
+ let(:grandparent) { create(:group) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let(:group) { create(:group, parent: parent) }
+
+ it 'returns false' do
+ grandparent.update_attribute(:emails_disabled, true)
+
+ expect(group.emails_disabled?).to be_falsey
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 4122736c148..2ba53818e54 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -9,6 +9,38 @@ describe NotificationRecipient do
subject(:recipient) { described_class.new(user, :watch, target: target, project: project) }
+ describe '#notifiable?' do
+ let(:recipient) { described_class.new(user, :mention, target: target, project: project) }
+
+ context 'when emails are disabled' do
+ it 'returns false if group disabled' do
+ expect(project.namespace).to receive(:emails_disabled?).and_return(true)
+ expect(recipient).to receive(:emails_disabled?).and_call_original
+ expect(recipient.notifiable?).to eq false
+ end
+
+ it 'returns false if project disabled' do
+ expect(project).to receive(:emails_disabled?).and_return(true)
+ expect(recipient).to receive(:emails_disabled?).and_call_original
+ expect(recipient.notifiable?).to eq false
+ end
+ end
+
+ context 'when emails are enabled' do
+ it 'returns true if group enabled' do
+ expect(project.namespace).to receive(:emails_disabled?).and_return(false)
+ expect(recipient).to receive(:emails_disabled?).and_call_original
+ expect(recipient.notifiable?).to eq true
+ end
+
+ it 'returns true if project enabled' do
+ expect(project).to receive(:emails_disabled?).and_return(false)
+ expect(recipient).to receive(:emails_disabled?).and_call_original
+ expect(recipient.notifiable?).to eq true
+ end
+ end
+ end
+
describe '#has_access?' do
before do
allow(user).to receive(:can?).and_call_original
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index 619ab96af94..cf7c7bf7e61 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -42,9 +42,9 @@ describe ChatMessage::PipelineMessage do
before do
test_commit = double("A test commit", committer: args[:user], title: "A test commit message")
- test_project = double("A test project",
- commit_by: test_commit, name: args[:project][:name],
- web_url: args[:project][:web_url], avatar_url: args[:project][:avatar_url])
+ test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url])
+ allow(test_project).to receive(:avatar_url).with(no_args).and_return("/avatar")
+ allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url])
allow(Project).to receive(:find) { test_project }
test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors,
diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb
index 0a58eb367e3..ffe241aa880 100644
--- a/spec/models/project_services/emails_on_push_service_spec.rb
+++ b/spec/models/project_services/emails_on_push_service_spec.rb
@@ -20,4 +20,24 @@ describe EmailsOnPushService do
it { is_expected.not_to validate_presence_of(:recipients) }
end
end
+
+ context 'project emails' do
+ let(:push_data) { { object_kind: 'push' } }
+ let(:project) { create(:project, :repository) }
+ let(:service) { create(:emails_on_push_service, project: project) }
+
+ it 'does not send emails when disabled' do
+ expect(project).to receive(:emails_disabled?).and_return(true)
+ expect(EmailsOnPushWorker).not_to receive(:perform_async)
+
+ service.execute(push_data)
+ end
+
+ it 'does send emails when enabled' do
+ expect(project).to receive(:emails_disabled?).and_return(false)
+ expect(EmailsOnPushWorker).to receive(:perform_async)
+
+ service.execute(push_data)
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 29a589eba20..ff9e94afc12 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -98,6 +98,7 @@ describe Project do
it { is_expected.to have_many(:lfs_file_locks) }
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
+ it { is_expected.to have_many(:cycle_analytics_stages) }
it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
@@ -2252,6 +2253,21 @@ describe Project do
end
end
+ describe '#mark_stuck_remote_mirrors_as_failed!' do
+ it 'fails stuck remote mirrors' do
+ project = create(:project, :repository, :remote_mirror)
+
+ project.remote_mirrors.first.update(
+ update_status: :started,
+ last_update_started_at: 2.days.ago
+ )
+
+ expect do
+ project.mark_stuck_remote_mirrors_as_failed!
+ end.to change { project.remote_mirrors.stuck.count }.from(1).to(0)
+ end
+ end
+
describe '#ancestors_upto' do
let(:parent) { create(:group) }
let(:child) { create(:group, parent: parent) }
@@ -2300,6 +2316,57 @@ describe Project do
end
end
+ describe '#emails_disabled?' do
+ let(:project) { create(:project, emails_disabled: false) }
+
+ context 'emails disabled in group' do
+ it 'returns true' do
+ allow(project.namespace).to receive(:emails_disabled?) { true }
+
+ expect(project.emails_disabled?).to be_truthy
+ end
+ end
+
+ context 'emails enabled in group' do
+ before do
+ allow(project.namespace).to receive(:emails_disabled?) { false }
+ end
+
+ it 'returns false' do
+ expect(project.emails_disabled?).to be_falsey
+ end
+
+ it 'returns true' do
+ project.update_attribute(:emails_disabled, true)
+
+ expect(project.emails_disabled?).to be_truthy
+ end
+ end
+
+ context 'when :emails_disabled feature flag is off' do
+ before do
+ stub_feature_flags(emails_disabled: false)
+ end
+
+ context 'emails disabled in group' do
+ it 'returns false' do
+ allow(project.namespace).to receive(:emails_disabled?) { true }
+
+ expect(project.emails_disabled?).to be_falsey
+ end
+ end
+
+ context 'emails enabled in group' do
+ it 'returns false' do
+ allow(project.namespace).to receive(:emails_disabled?) { false }
+ project.update_attribute(:emails_disabled, true)
+
+ expect(project.emails_disabled?).to be_falsey
+ end
+ end
+ end
+ end
+
describe '#lfs_enabled?' do
let(:project) { create(:project) }
@@ -4297,6 +4364,39 @@ describe Project do
end
end
+ describe '#has_active_hooks?' do
+ set(:project) { create(:project) }
+
+ it { expect(project.has_active_hooks?).to be_falsey }
+
+ it 'returns true when a matching push hook exists' do
+ create(:project_hook, push_events: true, project: project)
+
+ expect(project.has_active_hooks?(:merge_request_events)).to be_falsey
+ expect(project.has_active_hooks?).to be_truthy
+ end
+
+ it 'returns true when a matching system hook exists' do
+ create(:system_hook, push_events: true)
+
+ expect(project.has_active_hooks?(:merge_request_events)).to be_falsey
+ expect(project.has_active_hooks?).to be_truthy
+ end
+ end
+
+ describe '#has_active_services?' do
+ set(:project) { create(:project) }
+
+ it { expect(project.has_active_services?).to be_falsey }
+
+ it 'returns true when a matching service exists' do
+ create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project)
+
+ expect(project.has_active_services?(:merge_request_hooks)).to be_falsey
+ expect(project.has_active_services?).to be_truthy
+ end
+ end
+
describe '#badges' do
let(:project_group) { create(:group) }
let(:project) { create(:project, path: 'avatar', namespace: project_group) }
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 687b0935c55..7edeb56efe2 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -153,14 +153,14 @@ describe RemoteMirror, :mailer do
end
end
- describe '#mark_as_failed' do
+ describe '#mark_as_failed!' do
let(:remote_mirror) { create(:remote_mirror) }
let(:error_message) { 'http://user:pass@test.com/root/repoC.git/' }
let(:sanitized_error_message) { 'http://*****:*****@test.com/root/repoC.git/' }
subject do
remote_mirror.update_start
- remote_mirror.mark_as_failed(error_message)
+ remote_mirror.mark_as_failed!(error_message)
end
it 'sets the update_status to failed' do
@@ -204,8 +204,8 @@ describe RemoteMirror, :mailer do
it 'includes mirrors that were started over an hour ago' do
mirror = create_mirror(url: 'http://cantbeblank',
update_status: 'started',
- last_update_at: 3.hours.ago,
- updated_at: 2.hours.ago)
+ last_update_started_at: 3.hours.ago,
+ last_update_at: 2.hours.ago)
expect(described_class.stuck.last).to eq(mirror)
end
@@ -214,7 +214,7 @@ describe RemoteMirror, :mailer do
mirror = create_mirror(url: 'http://cantbeblank',
update_status: 'started',
last_update_at: nil,
- updated_at: 4.hours.ago)
+ last_update_started_at: 4.hours.ago)
expect(described_class.stuck.last).to eq(mirror)
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 12dff440ce2..e68de2e73a8 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1744,12 +1744,23 @@ describe Repository do
end
end
- describe '#before_push_tag' do
+ describe '#expires_caches_for_tags' do
it 'flushes the cache' do
expect(repository).to receive(:expire_statistics_caches)
expect(repository).to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_tags_cache)
+ repository.expire_caches_for_tags
+ end
+ end
+
+ describe '#before_push_tag' do
+ it 'logs an event' do
+ expect(repository).not_to receive(:expire_statistics_caches)
+ expect(repository).not_to receive(:expire_emptiness_caches)
+ expect(repository).not_to receive(:expire_tags_cache)
+ expect(repository).to receive(:repository_event).with(:push_tag)
+
repository.before_push_tag
end
end
@@ -1781,6 +1792,12 @@ describe Repository do
repository.after_create_branch
end
+
+ it 'does not expire the branch caches when specified' do
+ expect(repository).not_to receive(:expire_branches_cache)
+
+ repository.after_create_branch(expire_cache: false)
+ end
end
describe '#after_remove_branch' do
@@ -1789,6 +1806,12 @@ describe Repository do
repository.after_remove_branch
end
+
+ it 'does not expire the branch caches when specified' do
+ expect(repository).not_to receive(:expire_branches_cache)
+
+ repository.after_remove_branch(expire_cache: false)
+ end
end
describe '#after_create' do
diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb
index 7b61819e079..2373fef8aa6 100644
--- a/spec/policies/clusters/instance_policy_spec.rb
+++ b/spec/policies/clusters/instance_policy_spec.rb
@@ -9,6 +9,8 @@ describe Clusters::InstancePolicy do
describe 'rules' do
context 'when user' do
it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_disallowed :add_cluster }
+ it { expect(policy).to be_disallowed :create_cluster }
it { expect(policy).to be_disallowed :update_cluster }
it { expect(policy).to be_disallowed :admin_cluster }
end
@@ -17,6 +19,8 @@ describe Clusters::InstancePolicy do
let(:user) { create(:admin) }
it { expect(policy).to be_allowed :read_cluster }
+ it { expect(policy).to be_allowed :add_cluster }
+ it { expect(policy).to be_allowed :create_cluster }
it { expect(policy).to be_allowed :update_cluster }
it { expect(policy).to be_allowed :admin_cluster }
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 12be3927e18..df6cc526eb0 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -226,4 +226,32 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:read_instance_statistics) }
end
end
+
+ describe 'slash commands' do
+ context 'regular user' do
+ it { is_expected.to be_allowed(:use_slash_commands) }
+ end
+
+ context 'when internal' do
+ let(:current_user) { User.ghost }
+
+ it { is_expected.not_to be_allowed(:use_slash_commands) }
+ end
+
+ context 'when blocked' do
+ before do
+ current_user.block
+ end
+
+ it { is_expected.not_to be_allowed(:use_slash_commands) }
+ end
+
+ context 'when access locked' do
+ before do
+ current_user.lock_access!
+ end
+
+ it { is_expected.not_to be_allowed(:use_slash_commands) }
+ end
+ end
end
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index ca1ffe3c524..ef09c6effbb 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -9,6 +9,61 @@ describe API::Discussions do
project.add_developer(user)
end
+ context 'with cross-reference system notes', :request_store do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:new_merge_request) { create(:merge_request) }
+ let(:commit) { new_merge_request.project.commit }
+ let!(:note) { create(:system_note, noteable: merge_request, project: project, note: cross_reference) }
+ let!(:note_metadata) { create(:system_note_metadata, note: note, action: 'cross_reference') }
+ let(:cross_reference) { "test commit #{commit.to_reference(project)}" }
+ let(:pat) { create(:personal_access_token, user: user) }
+
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/discussions" }
+
+ before do
+ project.add_developer(user)
+ new_merge_request.project.add_developer(user)
+ end
+
+ it 'returns only the note that the user should see' do
+ hidden_merge_request = create(:merge_request)
+ new_cross_reference = "test commit #{hidden_merge_request.project.commit}"
+ new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference)
+ create(:system_note_metadata, note: new_note, action: 'cross_reference')
+
+ get api(url, user, personal_access_token: pat)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['notes'].count).to eq(1)
+
+ parsed_note = json_response.first['notes'].first
+ expect(parsed_note['id']).to eq(note.id)
+ expect(parsed_note['body']).to eq(cross_reference)
+ expect(parsed_note['system']).to be true
+ end
+
+ it 'avoids Git calls and N+1 SQL queries' do
+ expect_any_instance_of(Repository).not_to receive(:find_commit).with(commit.id)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api(url, user, personal_access_token: pat)
+ end
+
+ expect(response).to have_gitlab_http_status(200)
+
+ RequestStore.clear!
+
+ new_note = create(:system_note, noteable: merge_request, project: project, note: cross_reference)
+ create(:system_note_metadata, note: new_note, action: 'cross_reference')
+
+ RequestStore.clear!
+
+ expect { get api(url, user, personal_access_token: pat) }.not_to exceed_query_limit(control)
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
context 'when noteable is an Issue' do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 184c00a356a..590107d5161 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -144,6 +144,7 @@ describe API::Settings, 'Settings' do
external_auth_client_key_pass: "5iveL!fe"
}
end
+
let(:attribute_names) { settings.keys.map(&:to_s) }
it 'includes the attributes in the API' do
@@ -165,6 +166,56 @@ describe API::Settings, 'Settings' do
end
end
+ context "snowplow tracking settings" do
+ let(:settings) do
+ {
+ snowplow_collector_hostname: "snowplow.example.com",
+ snowplow_cookie_domain: ".example.com",
+ snowplow_enabled: true,
+ snowplow_site_id: "site_id"
+ }
+ end
+
+ let(:attribute_names) { settings.keys.map(&:to_s) }
+
+ it "includes the attributes in the API" do
+ get api("/application/settings", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ attribute_names.each do |attribute|
+ expect(json_response.keys).to include(attribute)
+ end
+ end
+
+ it "allows updating the settings" do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(200)
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+
+ context "missing snowplow_collector_hostname value when snowplow_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), params: { snowplow_enabled: true }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to eq("snowplow_collector_hostname is missing")
+ end
+
+ it "handles validation errors" do
+ put api("/application/settings", admin), params: settings.merge({
+ snowplow_collector_hostname: nil
+ })
+
+ expect(response).to have_gitlab_http_status(400)
+ message = json_response["message"]
+ expect(message["snowplow_collector_hostname"]).to include("can't be blank")
+ end
+ end
+ end
+
context "missing plantuml_url value when plantuml_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true }
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 7e2f311a065..deb68899309 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1113,7 +1113,7 @@ describe Ci::CreatePipelineService do
test_a: {
stage: "test",
script: "ls",
- only: %w[master feature tags],
+ only: %w[master feature],
needs: %w[build_a]
},
deploy: {
@@ -1143,6 +1143,7 @@ describe Ci::CreatePipelineService do
it 'does not create a pipeline as test_a depends on build_a' do
expect(pipeline).not_to be_persisted
expect(pipeline.builds).to be_empty
+ expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'")
end
end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index 4a2ec769116..874df9a68cd 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -14,6 +14,78 @@ describe Git::BaseHooksService do
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
+ describe '#execute_project_hooks' do
+ class TestService < described_class
+ def hook_name
+ :push_hooks
+ end
+
+ def commits
+ []
+ end
+ end
+
+ let(:project) { create(:project, :repository) }
+
+ subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+
+ context '#execute_hooks' do
+ before do
+ expect(project).to receive(:has_active_hooks?).and_return(active)
+ end
+
+ context 'active hooks' do
+ let(:active) { true }
+
+ it 'executes the hooks' do
+ expect(subject).to receive(:push_data).at_least(:once).and_call_original
+ expect(project).to receive(:execute_hooks)
+
+ subject.execute
+ end
+ end
+
+ context 'inactive hooks' do
+ let(:active) { false }
+
+ it 'does not execute the hooks' do
+ expect(subject).not_to receive(:push_data)
+ expect(project).not_to receive(:execute_hooks)
+
+ subject.execute
+ end
+ end
+ end
+
+ context '#execute_services' do
+ before do
+ expect(project).to receive(:has_active_services?).and_return(active)
+ end
+
+ context 'active services' do
+ let(:active) { true }
+
+ it 'executes the services' do
+ expect(subject).to receive(:push_data).at_least(:once).and_call_original
+ expect(project).to receive(:execute_services)
+
+ subject.execute
+ end
+ end
+
+ context 'inactive services' do
+ let(:active) { false }
+
+ it 'does not execute the services' do
+ expect(subject).not_to receive(:push_data)
+ expect(project).not_to receive(:execute_services)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
describe 'with remote mirrors' do
class TestService < described_class
def commits
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 23be400059e..8af51848b7b 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -25,7 +25,7 @@ describe Git::BranchHooksService do
end
describe "Git Push Data" do
- subject(:push_data) { service.execute }
+ subject(:push_data) { service.send(:push_data) }
it 'has expected push data attributes' do
is_expected.to match a_hash_including(
@@ -109,6 +109,7 @@ describe Git::BranchHooksService do
expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
expect(event.push_event_payload.commit_from).to eq(oldrev)
expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.commit_title).to eq('Change some files')
expect(event.push_event_payload.ref).to eq('master')
expect(event.push_event_payload.commit_count).to eq(1)
end
@@ -124,6 +125,7 @@ describe Git::BranchHooksService do
expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
expect(event.push_event_payload.commit_from).to be_nil
expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.commit_title).to eq('Initial commit')
expect(event.push_event_payload.ref).to eq('master')
expect(event.push_event_payload.commit_count).to be > 1
end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index 6e39fa6b3c0..ad5d296f5c1 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -78,7 +78,10 @@ describe Git::BranchPushService, services: true do
it "creates a new pipeline" do
expect { subject }.to change { Ci::Pipeline.count }
- expect(Ci::Pipeline.last).to be_push
+
+ pipeline = Ci::Pipeline.last
+ expect(pipeline).to be_push
+ expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref)
end
end
@@ -123,6 +126,10 @@ describe Git::BranchPushService, services: true do
describe "Webhooks" do
context "execute webhooks" do
+ before do
+ create(:project_hook, push_events: true, project: project)
+ end
+
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
index f5938a5c708..e362577d289 100644
--- a/spec/services/git/tag_hooks_service_spec.rb
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -26,7 +26,8 @@ describe Git::TagHooksService, :service do
describe 'System hooks' do
it 'Executes system hooks' do
- push_data = service.execute
+ push_data = service.send(:push_data)
+ expect(project).to receive(:has_active_hooks?).and_return(true)
expect_next_instance_of(SystemHooksService) do |system_hooks_service|
expect(system_hooks_service)
@@ -40,6 +41,7 @@ describe Git::TagHooksService, :service do
describe "Webhooks" do
it "executes hooks on the project" do
+ expect(project).to receive(:has_active_hooks?).and_return(true)
expect(project).to receive(:execute_hooks)
service.execute
@@ -61,7 +63,7 @@ describe Git::TagHooksService, :service do
describe 'Push data' do
shared_examples_for 'tag push data expectations' do
- subject(:push_data) { service.execute }
+ subject(:push_data) { service.send(:push_data) }
it 'has expected push data attributes' do
is_expected.to match a_hash_including(
object_kind: 'tag_push',
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index 418952b52da..7e008637182 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -26,8 +26,8 @@ describe Git::TagPushService do
subject
end
- it 'flushes the tags cache' do
- expect(project.repository).to receive(:expire_tags_cache)
+ it 'does not flush the tags cache' do
+ expect(project.repository).not_to receive(:expire_tags_cache)
subject
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 5d4576139f7..12e9c2b2f3a 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -86,6 +86,7 @@ describe Groups::UpdateService do
context "unauthorized visibility_level validation" do
let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
+
before do
internal_group.add_user(user, Gitlab::Access::MAINTAINER)
end
@@ -96,6 +97,20 @@ describe Groups::UpdateService do
end
end
+ context 'when updating #emails_disabled' do
+ let(:service) { described_class.new(internal_group, user, emails_disabled: true) }
+
+ it 'updates the attribute' do
+ internal_group.add_user(user, Gitlab::Access::OWNER)
+
+ expect { service.execute }.to change { internal_group.emails_disabled }.to(true)
+ end
+
+ it 'does not update when not group owner' do
+ expect { service.execute }.not_to change { internal_group.emails_disabled }
+ end
+ end
+
context 'rename group' do
let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index ee9caaf2f47..7b8c94c86fe 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -25,7 +25,7 @@ describe MergeRequests::RebaseService do
describe '#execute' do
context 'when another rebase is already in progress' do
before do
- allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true)
+ allow(repository).to receive(:rebase_in_progress?).with(merge_request.id).and_return(true)
end
it 'saves the error message' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 1dcade1de0d..d925aa2b6c3 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -240,45 +240,50 @@ describe NotificationService, :mailer do
end
describe '#new_note' do
- it do
- add_users(project)
- add_user_subscriptions(issue)
- reset_delivered_emails!
+ context do
+ before do
+ add_users(project)
+ add_user_subscriptions(issue)
+ reset_delivered_emails!
+ end
- expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times
+ it do
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times
- notification.new_note(note)
+ notification.new_note(note)
- should_email(@u_watcher)
- should_email(note.noteable.author)
- should_email(note.noteable.assignees.first)
- should_email(@u_custom_global)
- should_email(@u_mentioned)
- should_email(@subscriber)
- should_email(@watcher_and_subscriber)
- should_email(@subscribed_participant)
- should_email(@u_custom_off)
- should_email(@unsubscribed_mentioned)
- should_not_email(@u_guest_custom)
- should_not_email(@u_guest_watcher)
- should_not_email(note.author)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@unsubscriber)
- should_not_email(@u_outsider_mentioned)
- should_not_email(@u_lazy_participant)
- end
+ should_email(@u_watcher)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignees.first)
+ should_email(@u_custom_global)
+ should_email(@u_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_email(@subscribed_participant)
+ should_email(@u_custom_off)
+ should_email(@unsubscribed_mentioned)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_guest_watcher)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_outsider_mentioned)
+ should_not_email(@u_lazy_participant)
+ end
- it "emails the note author if they've opted into notifications about their activity" do
- add_users(project)
- add_user_subscriptions(issue)
- reset_delivered_emails!
+ it "emails the note author if they've opted into notifications about their activity" do
+ note.author.notified_of_own_activity = true
- note.author.notified_of_own_activity = true
+ notification.new_note(note)
- notification.new_note(note)
+ should_email(note.author)
+ end
- should_email(note.author)
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { note }
+ let(:notification_trigger) { notification.new_note(note) }
+ end
end
it 'filters out "mentioned in" notes' do
@@ -337,6 +342,11 @@ describe NotificationService, :mailer do
it_behaves_like 'new note notifications'
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { note }
+ let(:notification_trigger) { notification.new_note(note) }
+ end
+
context 'which is a subgroup' do
let!(:parent) { create(:group) }
let!(:group) { create(:group, parent: parent) }
@@ -472,6 +482,11 @@ describe NotificationService, :mailer do
expect(Notify).not_to receive(:note_issue_email)
notification.new_note(mentioned_note)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { note }
+ let(:notification_trigger) { notification.new_note(note) }
+ end
end
end
@@ -619,6 +634,11 @@ describe NotificationService, :mailer do
notification.new_note(note)
should_not_email(@u_committer)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { note }
+ let(:notification_trigger) { notification.new_note(note) }
+ end
end
end
@@ -645,6 +665,11 @@ describe NotificationService, :mailer do
.to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id)
expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { note }
+ let(:notification_trigger) { notification.new_note(note) }
+ end
end
end
end
@@ -819,6 +844,11 @@ describe NotificationService, :mailer do
should_email(user_4)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.new_issue(issue, @u_disabled) }
+ end
+
context 'confidential issues' do
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -861,6 +891,11 @@ describe NotificationService, :mailer do
let(:mentionable) { issue }
include_examples 'notifications for new mentions'
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) }
+ end
end
describe '#reassigned_issue' do
@@ -969,6 +1004,11 @@ describe NotificationService, :mailer do
let(:issuable) { issue }
let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) }
+ end
end
describe '#relabeled_issue' do
@@ -1028,6 +1068,11 @@ describe NotificationService, :mailer do
should_email(subscriber_to_both)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) }
+ end
+
context 'confidential issues' do
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -1065,12 +1110,19 @@ describe NotificationService, :mailer do
end
describe '#removed_milestone_issue' do
- it_behaves_like 'altered milestone notification on issue' do
+ context do
let(:milestone) { create(:milestone, project: project, issues: [issue]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
- before do
- notification.removed_milestone_issue(issue, issue.author)
+ it_behaves_like 'altered milestone notification on issue' do
+ before do
+ notification.removed_milestone_issue(issue, issue.author)
+ end
+ end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) }
end
end
@@ -1110,12 +1162,19 @@ describe NotificationService, :mailer do
end
describe '#changed_milestone_issue' do
- it_behaves_like 'altered milestone notification on issue' do
+ context do
let(:new_milestone) { create(:milestone, project: project, issues: [issue]) }
let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } }
- before do
- notification.changed_milestone_issue(issue, new_milestone, issue.author)
+ it_behaves_like 'altered milestone notification on issue' do
+ before do
+ notification.changed_milestone_issue(issue, new_milestone, issue.author)
+ end
+ end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.changed_milestone_issue(issue, new_milestone, issue.author) }
end
end
@@ -1183,6 +1242,11 @@ describe NotificationService, :mailer do
let(:issuable) { issue }
let(:notification_trigger) { notification.close_issue(issue, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.close_issue(issue, @u_disabled) }
+ end
end
describe '#reopen_issue' do
@@ -1214,6 +1278,11 @@ describe NotificationService, :mailer do
let(:issuable) { issue }
let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
+ end
end
describe '#issue_moved' do
@@ -1240,6 +1309,11 @@ describe NotificationService, :mailer do
let(:issuable) { issue }
let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
+ end
end
describe '#issue_due' do
@@ -1280,6 +1354,11 @@ describe NotificationService, :mailer do
let(:issuable) { issue }
let(:notification_trigger) { notification.issue_due(issue) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { issue }
+ let(:notification_trigger) { notification.issue_due(issue) }
+ end
end
end
@@ -1374,6 +1453,11 @@ describe NotificationService, :mailer do
should_email(user_4)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) }
+ end
+
context 'participating' do
it_should_behave_like 'participating by assignee notification' do
let(:participant) { create(:user, username: 'user-participant')}
@@ -1406,6 +1490,11 @@ describe NotificationService, :mailer do
let(:mentionable) { merge_request }
include_examples 'notifications for new mentions'
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) }
+ end
end
describe '#reassigned_merge_request' do
@@ -1449,6 +1538,11 @@ describe NotificationService, :mailer do
let(:issuable) { merge_request }
let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) }
+ end
end
describe '#push_to_merge_request' do
@@ -1479,6 +1573,11 @@ describe NotificationService, :mailer do
let(:issuable) { merge_request }
let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) }
+ end
end
describe '#relabel_merge_request' do
@@ -1512,28 +1611,43 @@ describe NotificationService, :mailer do
should_not_email(@u_participating)
should_not_email(@u_lazy_participant)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled) }
+ end
end
describe '#removed_milestone_merge_request' do
- it_behaves_like 'altered milestone notification on merge request' do
- let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
- let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+ let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+ it_behaves_like 'altered milestone notification on merge request' do
before do
notification.removed_milestone_merge_request(merge_request, merge_request.author)
end
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.removed_milestone_merge_request(merge_request, merge_request.author) }
+ end
end
describe '#changed_milestone_merge_request' do
- it_behaves_like 'altered milestone notification on merge request' do
- let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
- let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+ let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) }
+ let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } }
+ it_behaves_like 'altered milestone notification on merge request' do
before do
notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author)
end
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) }
+ end
end
describe '#merge_request_unmergeable' do
@@ -1544,6 +1658,11 @@ describe NotificationService, :mailer do
expect(email_recipients.size).to eq(1)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.merge_request_unmergeable(merge_request) }
+ end
+
describe 'when merge_when_pipeline_succeeds is true' do
before do
merge_request.update(
@@ -1590,6 +1709,11 @@ describe NotificationService, :mailer do
let(:issuable) { merge_request }
let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) }
+ end
end
describe '#merged_merge_request' do
@@ -1642,6 +1766,11 @@ describe NotificationService, :mailer do
let(:issuable) { merge_request }
let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) }
+ end
end
describe '#reopen_merge_request' do
@@ -1672,6 +1801,11 @@ describe NotificationService, :mailer do
let(:issuable) { merge_request }
let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) }
+ end
end
describe "#resolve_all_discussions" do
@@ -1695,6 +1829,11 @@ describe NotificationService, :mailer do
let(:issuable) { merge_request }
let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) }
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) }
+ end
end
end
@@ -1719,6 +1858,11 @@ describe NotificationService, :mailer do
should_not_email(@u_disabled)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.project_was_moved(project, "gitlab/gitlab") }
+ end
+
context 'users not having access to the new location' do
it 'does not send email' do
old_user = create(:user)
@@ -1762,6 +1906,11 @@ describe NotificationService, :mailer do
should_only_email(@u_participating)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.project_exported(project, @u_participating) }
+ end
end
describe '#project_not_exported' do
@@ -1770,6 +1919,11 @@ describe NotificationService, :mailer do
should_only_email(@u_participating)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.project_not_exported(project, @u_participating, ['error']) }
+ end
end
end
end
@@ -1800,6 +1954,11 @@ describe NotificationService, :mailer do
should_email(maintainer)
should_not_email(developer)
end
+
+ it_behaves_like 'group emails are disabled' do
+ let(:notification_target) { group }
+ let(:notification_trigger) { group.request_access(added_user) }
+ end
end
describe '#decline_group_invite' do
@@ -1839,6 +1998,11 @@ describe NotificationService, :mailer do
should_not_email_anyone
end
end
+
+ it_behaves_like 'group emails are disabled' do
+ let(:notification_target) { group }
+ let(:notification_trigger) { group.add_guest(added_user) }
+ end
end
end
@@ -1859,6 +2023,11 @@ describe NotificationService, :mailer do
should_only_email(project.owner)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { project.request_access(added_user) }
+ end
end
context 'for a project in a group' do
@@ -1878,7 +2047,7 @@ describe NotificationService, :mailer do
end
end
- describe '#decline_group_invite' do
+ describe '#decline_project_invite' do
let(:member) { create(:user) }
before do
@@ -1900,6 +2069,11 @@ describe NotificationService, :mailer do
should_only_email(added_user)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { create_member! }
+ end
+
context 'when notifications are disabled' do
before do
create_global_setting_for(added_user, :disabled)
@@ -2071,6 +2245,11 @@ describe NotificationService, :mailer do
should_only_email(u_custom_notification_enabled, kind: :bcc)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { pipeline }
+ let(:notification_trigger) { notification.pipeline_finished(pipeline) }
+ end
+
context 'when the creator has group notification email set' do
let(:group_notification_email) { 'user+group@example.com' }
@@ -2100,6 +2279,11 @@ describe NotificationService, :mailer do
should_only_email(u_member, kind: :bcc)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { pipeline }
+ let(:notification_trigger) { notification.pipeline_finished(pipeline) }
+ end
+
context 'when the creator has group notification email set' do
let(:group_notification_email) { 'user+group@example.com' }
@@ -2215,6 +2399,11 @@ describe NotificationService, :mailer do
should_only_email(u_maintainer1, u_maintainer2, u_owner)
end
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { domain }
+ let(:notification_trigger) { notify! }
+ end
+
it 'emails nobody if the project is missing' do
domain.project = nil
@@ -2224,30 +2413,6 @@ describe NotificationService, :mailer do
end
end
end
-
- describe '#pages_domain_verification_failed' do
- it 'emails current watching maintainers' do
- notification.pages_domain_verification_failed(domain)
-
- should_only_email(u_maintainer1, u_maintainer2, u_owner)
- end
- end
-
- describe '#pages_domain_enabled' do
- it 'emails current watching maintainers' do
- notification.pages_domain_enabled(domain)
-
- should_only_email(u_maintainer1, u_maintainer2, u_owner)
- end
- end
-
- describe '#pages_domain_disabled' do
- it 'emails current watching maintainers' do
- notification.pages_domain_disabled(domain)
-
- should_only_email(u_maintainer1, u_maintainer2, u_owner)
- end
- end
end
context 'Auto DevOps notifications' do
@@ -2266,6 +2431,11 @@ describe NotificationService, :mailer do
should_email(owner, times: 1) # Once for the disable pipeline.
should_email(pipeline_user, times: 2) # Once for the new permission, once for the disable.
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.autodevops_disabled(pipeline, [owner.email, pipeline_user.email]) }
+ end
end
end
@@ -2279,6 +2449,11 @@ describe NotificationService, :mailer do
should_email(user)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.repository_cleanup_success(project, user) }
+ end
end
describe '#repository_cleanup_failure' do
@@ -2287,6 +2462,11 @@ describe NotificationService, :mailer do
should_email(user)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.repository_cleanup_failure(project, user, 'Some error') }
+ end
end
end
@@ -2320,6 +2500,11 @@ describe NotificationService, :mailer do
should_only_email(u_maintainer1, u_maintainer2, u_owner)
end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { project }
+ let(:notification_trigger) { notification.remote_mirror_update_failed(remote_mirror) }
+ end
end
end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index be2811ab1e7..4396ccab584 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -10,49 +10,91 @@ describe Projects::UpdateRemoteMirrorService do
subject(:service) { described_class.new(project, project.creator) }
- describe "#execute" do
+ describe '#execute' do
+ subject(:execute!) { service.execute(remote_mirror, 0) }
+
before do
project.repository.add_branch(project.owner, 'existing-branch', 'master')
allow(remote_mirror).to receive(:update_repository).and_return(true)
end
- it "ensures the remote exists" do
+ it 'ensures the remote exists' do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
expect(remote_mirror).to receive(:ensure_remote!)
- service.execute(remote_mirror)
+ execute!
end
- it "fetches the remote repository" do
+ it 'fetches the remote repository' do
expect(project.repository)
.to receive(:fetch_remote)
- .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror)
+ .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror)
- service.execute(remote_mirror)
+ execute!
end
- it "returns success when updated succeeds" do
+ it 'marks the mirror as started when beginning' do
+ expect(remote_mirror).to receive(:update_start!).and_call_original
+
+ execute!
+ end
+
+ it 'marks the mirror as successfully finished' do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
- result = service.execute(remote_mirror)
+ result = execute!
expect(result[:status]).to eq(:success)
+ expect(remote_mirror).to be_finished
+ end
+
+ it 'marks the mirror as failed and raises the error when an unexpected error occurs' do
+ allow(project.repository).to receive(:fetch_remote).and_raise('Badly broken')
+
+ expect { execute! }.to raise_error /Badly broken/
+
+ expect(remote_mirror).to be_failed
+ expect(remote_mirror.last_error).to include('Badly broken')
+ end
+
+ context 'when the update fails because of a `Gitlab::Git::CommandError`' do
+ before do
+ allow(project.repository).to receive(:fetch_remote).and_raise(Gitlab::Git::CommandError.new('fetch failed'))
+ end
+
+ it 'wraps `Gitlab::Git::CommandError`s in a service error' do
+ expect(execute!).to eq(status: :error, message: 'fetch failed')
+ end
+
+ it 'marks the mirror as to be retried' do
+ execute!
+
+ expect(remote_mirror).to be_to_retry
+ expect(remote_mirror.last_error).to include('fetch failed')
+ end
+
+ it "marks the mirror as failed after #{described_class::MAX_TRIES} tries" do
+ service.execute(remote_mirror, described_class::MAX_TRIES)
+
+ expect(remote_mirror).to be_failed
+ expect(remote_mirror.last_error).to include('fetch failed')
+ end
end
context 'when syncing all branches' do
- it "push all the branches the first time" do
+ it 'push all the branches the first time' do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
expect(remote_mirror).to receive(:update_repository).with({})
- service.execute(remote_mirror)
+ execute!
end
end
context 'when only syncing protected branches' do
- it "sync updated protected branches" do
+ it 'sync updated protected branches' do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
protected_branch = create_protected_branch(project)
remote_mirror.only_protected_branches = true
@@ -61,7 +103,7 @@ describe Projects::UpdateRemoteMirrorService do
.to receive(:update_repository)
.with(only_branches_matching: [protected_branch.name])
- service.execute(remote_mirror)
+ execute!
end
def create_protected_branch(project)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 82010dd283c..31bd0f0f836 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -369,9 +369,28 @@ describe Projects::UpdateService do
end
end
+ context 'when updating #emails_disabled' do
+ it 'updates the attribute for the project owner' do
+ expect { update_project(project, user, emails_disabled: true) }
+ .to change { project.emails_disabled }
+ .to(true)
+ end
+
+ it 'does not update when not project owner' do
+ maintainer = create(:user)
+ project.add_user(maintainer, :maintainer)
+
+ expect { update_project(project, maintainer, emails_disabled: true) }
+ .not_to change { project.emails_disabled }
+ end
+ end
+
context 'with external authorization enabled' do
before do
enable_external_authorization_service_check
+
+ allow(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label', project.full_path).and_call_original
end
it 'does not save the project with an error if the service denies access' do
@@ -402,8 +421,7 @@ describe Projects::UpdateService do
end
it 'does not check the label when it does not change' do
- expect(::Gitlab::ExternalAuthorization)
- .not_to receive(:access_allowed?)
+ expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).once
update_project(project, user, { name: 'New name' })
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index bf5f211b11c..c9714964fc9 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -226,7 +226,7 @@ describe QuickActions::InterpretService do
it 'returns the todo message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq('Added a todo.')
+ expect(message).to eq('Added a To Do.')
end
end
@@ -242,7 +242,7 @@ describe QuickActions::InterpretService do
TodoService.new.mark_todo(issuable, developer)
_, _, message = service.execute(content, issuable)
- expect(message).to eq('Marked to do as done.')
+ expect(message).to eq('Marked To Do as done.')
end
end
@@ -453,7 +453,7 @@ describe QuickActions::InterpretService do
it 'returns the lock discussion message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq('Locked the discussion')
+ expect(message).to eq('Locked the discussion.')
end
end
@@ -470,7 +470,7 @@ describe QuickActions::InterpretService do
it 'returns the unlock discussion message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq('Unlocked the discussion')
+ expect(message).to eq('Unlocked the discussion.')
end
end
@@ -570,7 +570,7 @@ describe QuickActions::InterpretService do
it 'returns move issue failure message when the referenced issue is not found' do
_, _, message = service.execute('/move invalid', issue)
- expect(message).to eq("Move this issue failed because target project doesn't exists")
+ expect(message).to eq("Failed to move this issue because target project doesn't exist.")
end
end
@@ -584,7 +584,7 @@ describe QuickActions::InterpretService do
it 'returns the confidential message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq('Made this issue confidential')
+ expect(message).to eq('Made this issue confidential.')
end
end
@@ -783,7 +783,7 @@ describe QuickActions::InterpretService do
end
end
- it_behaves_like 'empty command', "Assign command failed because no user was found" do
+ it_behaves_like 'empty command', "Failed to assign a user because no user was found." do
let(:content) { '/assign @abcd1234' }
let(:issuable) { issue }
end
@@ -1217,12 +1217,12 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
- it_behaves_like 'empty command', 'Mark as duplicate failed because referenced issue was not found' do
+ it_behaves_like 'empty command', 'Failed to mark this issue as a duplicate because referenced issue was not found.' do
let(:content) { "/duplicate imaginary#1234" }
let(:issuable) { issue }
end
- it_behaves_like 'empty command', 'Mark as duplicate failed because referenced issue was not found' do
+ it_behaves_like 'empty command', 'Failed to mark this issue as a duplicate because referenced issue was not found.' do
let(:other_project) { create(:project, :private) }
let(:issue_duplicate) { create(:issue, project: other_project) }
@@ -1287,7 +1287,7 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
- it_behaves_like 'empty command', 'Mark as duplicate failed because referenced issue was not found' do
+ it_behaves_like 'empty command', 'Failed to mark this issue as a duplicate because referenced issue was not found.' do
let(:content) { '/duplicate #{issue.to_reference}' }
let(:issuable) { issue }
end
@@ -1463,19 +1463,19 @@ describe QuickActions::InterpretService do
context 'if the given label does not exist' do
let(:issuable) { issue }
let(:content) { '/board_move ~"Fake Label"' }
- it_behaves_like 'empty command', 'Move this issue failed because you need to specify only one label.'
+ it_behaves_like 'empty command', 'Failed to move this issue because label was not found.'
end
context 'if multiple labels are given' do
let(:issuable) { issue }
let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
- it_behaves_like 'empty command', 'Move this issue failed because you need to specify only one label.'
+ it_behaves_like 'empty command', 'Failed to move this issue because only a single label can be provided.'
end
context 'if the given label is not a list on the board' do
let(:issuable) { issue }
let(:content) { %{/board_move ~"#{bug.title}"} }
- it_behaves_like 'empty command'
+ it_behaves_like 'empty command', 'Failed to move this issue because label was not found.'
end
context 'if issuable is not an Issue' do
@@ -1552,7 +1552,7 @@ describe QuickActions::InterpretService do
it 'returns the create_merge_request message' do
_, _, message = service.execute(content, issuable)
- expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue")
+ expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue.")
end
end
end
@@ -1814,13 +1814,13 @@ describe QuickActions::InterpretService do
it 'uses the default branch name' do
_, explanations = service.explain(content, issue)
- expect(explanations).to eq(['Creates a branch and a merge request to resolve this issue'])
+ expect(explanations).to eq(['Creates a branch and a merge request to resolve this issue.'])
end
it 'returns the execution message using the default branch name' do
_, _, message = service.execute(content, issue)
- expect(message).to eq('Created a branch and a merge request to resolve this issue')
+ expect(message).to eq('Created a branch and a merge request to resolve this issue.')
end
end
@@ -1830,13 +1830,13 @@ describe QuickActions::InterpretService do
it 'uses the given branch name' do
_, explanations = service.explain(content, issue)
- expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue"])
+ expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue."])
end
it 'returns the execution message using the given branch name' do
_, _, message = service.execute(content, issue)
- expect(message).to eq("Created branch 'foo' and a merge request to resolve this issue")
+ expect(message).to eq("Created branch 'foo' and a merge request to resolve this issue.")
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index bcc133790d1..bd504f1553b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -48,6 +48,9 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
quality_level = Quality::TestLevel.new
RSpec.configure do |config|
+ config.filter_run focus: true
+ config.run_all_when_everything_filtered = true
+
config.use_transactional_fixtures = true
config.use_instantiated_fixtures = false
config.fixture_path = Rails.root
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 83ba654fab3..024340310a1 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -31,6 +31,10 @@ module EmailHelpers
expect(ActionMailer::Base.deliveries).to be_empty
end
+ def should_email_anyone
+ expect(ActionMailer::Base.deliveries).not_to be_empty
+ end
+
def email_recipients(kind: :to)
ActionMailer::Base.deliveries.flat_map(&kind)
end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
new file mode 100644
index 00000000000..76d82649c5f
--- /dev/null
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+shared_examples_for 'multiple issue boards' do
+ dropdown_selector = '.js-boards-selector .dropdown-menu'
+
+ context 'authorized user' do
+ before do
+ parent.add_maintainer(user)
+
+ login_as(user)
+
+ visit boards_path
+ wait_for_requests
+ end
+
+ it 'shows current board name' do
+ page.within('.boards-switcher') do
+ expect(page).to have_content(board.name)
+ end
+ end
+
+ it 'shows a list of boards' do
+ click_button board.name
+
+ page.within(dropdown_selector) do
+ expect(page).to have_content(board.name)
+ expect(page).to have_content(board2.name)
+ end
+ end
+
+ it 'switches current board' do
+ click_button board.name
+
+ page.within(dropdown_selector) do
+ click_link board2.name
+ end
+
+ wait_for_requests
+
+ page.within('.boards-switcher') do
+ expect(page).to have_content(board2.name)
+ end
+ end
+
+ it 'creates new board without detailed configuration' do
+ click_button board.name
+
+ page.within(dropdown_selector) do
+ click_button 'Create new board'
+ end
+
+ fill_in 'board-new-name', with: 'This is a new board'
+ click_button 'Create board'
+ wait_for_requests
+
+ expect(page).to have_button('This is a new board')
+ end
+
+ it 'deletes board' do
+ click_button board.name
+
+ wait_for_requests
+
+ page.within(dropdown_selector) do
+ click_button 'Delete board'
+ end
+
+ expect(page).to have_content('Are you sure you want to delete this board?')
+ click_button 'Delete'
+
+ click_button board2.name
+ page.within(dropdown_selector) do
+ expect(page).not_to have_content(board.name)
+ expect(page).to have_content(board2.name)
+ end
+ end
+
+ it 'adds a list to the none default board' do
+ click_button board.name
+
+ page.within(dropdown_selector) do
+ click_link board2.name
+ end
+
+ wait_for_requests
+
+ page.within('.boards-switcher') do
+ expect(page).to have_content(board2.name)
+ end
+
+ click_button 'Add list'
+
+ wait_for_requests
+
+ page.within '.dropdown-menu-issues-board-new' do
+ click_link planning.title
+ end
+
+ wait_for_requests
+
+ expect(page).to have_selector('.board', count: 3)
+
+ click_button board2.name
+
+ page.within(dropdown_selector) do
+ click_link board.name
+ end
+
+ wait_for_requests
+
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'maintains sidebar state over board switch' do
+ assert_boards_nav_active
+
+ find('.boards-switcher').click
+ wait_for_requests
+ click_link board2.name
+
+ assert_boards_nav_active
+ end
+ end
+
+ context 'unauthorized user' do
+ before do
+ visit boards_path
+ wait_for_requests
+ end
+
+ it 'does not show action links' do
+ click_button board.name
+
+ page.within(dropdown_selector) do
+ expect(page).not_to have_content('Create new board')
+ expect(page).not_to have_content('Delete board')
+ end
+ end
+ end
+
+ def assert_boards_nav_active
+ expect(find('.nav-sidebar .active .active')).to have_selector('a', text: 'Boards')
+ end
+end
diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
index 82975027e5b..dcc92dda950 100644
--- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
@@ -93,6 +93,19 @@ RSpec.shared_examples 'chat slash commands service' do
subject.trigger(params)
end
+
+ context 'when user is blocked' do
+ before do
+ chat_name.user.block
+ end
+
+ it 'blocks command execution' do
+ expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
+
+ result = subject.trigger(params)
+ expect(result).to include(text: /^Whoops! This action is not allowed/)
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/policies/clusterable_shared_examples.rb b/spec/support/shared_examples/policies/clusterable_shared_examples.rb
index 4f9873d53e4..0b427c23256 100644
--- a/spec/support/shared_examples/policies/clusterable_shared_examples.rb
+++ b/spec/support/shared_examples/policies/clusterable_shared_examples.rb
@@ -13,7 +13,11 @@ shared_examples 'clusterable policies' do
clusterable.add_developer(current_user)
end
+ it { expect_disallowed(:read_cluster) }
it { expect_disallowed(:add_cluster) }
+ it { expect_disallowed(:create_cluster) }
+ it { expect_disallowed(:update_cluster) }
+ it { expect_disallowed(:admin_cluster) }
end
context 'with a maintainer' do
@@ -22,7 +26,11 @@ shared_examples 'clusterable policies' do
end
context 'with no clusters' do
+ it { expect_allowed(:read_cluster) }
it { expect_allowed(:add_cluster) }
+ it { expect_allowed(:create_cluster) }
+ it { expect_allowed(:update_cluster) }
+ it { expect_allowed(:admin_cluster) }
end
end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
index 85682b4919d..a37b2392d52 100644
--- a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -40,7 +40,7 @@ shared_examples 'move quick action' do
wait_for_requests
- expect(page).to have_content "Move this issue failed because target project doesn't exists"
+ expect(page).to have_content "Failed to move this issue because target project doesn't exist."
expect(issue.reload).to be_open
end
end
diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb
new file mode 100644
index 00000000000..dd338ea47c7
--- /dev/null
+++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# Note that we actually update the attribute on the target_project/group, rather than
+# using `allow`. This is because there are some specs where, based on how the notification
+# is done, using an `allow` doesn't change the correct object.
+shared_examples 'project emails are disabled' do
+ let(:target_project) { notification_target.is_a?(Project) ? notification_target : notification_target.project }
+
+ before do
+ reset_delivered_emails!
+ target_project.clear_memoization(:emails_disabled)
+ end
+
+ it 'sends no emails with project emails disabled' do
+ target_project.update_attribute(:emails_disabled, true)
+
+ notification_trigger
+
+ should_not_email_anyone
+ end
+
+ it 'sends emails to someone' do
+ target_project.update_attribute(:emails_disabled, false)
+
+ notification_trigger
+
+ should_email_anyone
+ end
+end
+
+shared_examples 'group emails are disabled' do
+ let(:target_group) { notification_target.is_a?(Group) ? notification_target : notification_target.project.group }
+
+ before do
+ reset_delivered_emails!
+ target_group.clear_memoization(:emails_disabled)
+ end
+
+ it 'sends no emails with group emails disabled' do
+ target_group.update_attribute(:emails_disabled, true)
+
+ notification_trigger
+
+ should_not_email_anyone
+ end
+
+ it 'sends emails to someone' do
+ target_group.update_attribute(:emails_disabled, false)
+
+ notification_trigger
+
+ should_email_anyone
+ end
+end
diff --git a/spec/tasks/gitlab/update_templates_rake_spec.rb b/spec/tasks/gitlab/update_templates_rake_spec.rb
index 7b17549b8c7..14b4ad5e3d8 100644
--- a/spec/tasks/gitlab/update_templates_rake_spec.rb
+++ b/spec/tasks/gitlab/update_templates_rake_spec.rb
@@ -8,9 +8,18 @@ describe 'gitlab:update_project_templates rake task' do
before do
Rake.application.rake_require 'tasks/gitlab/update_templates'
create(:admin)
+
allow(Gitlab::ProjectTemplate)
.to receive(:archive_directory)
.and_return(Pathname.new(tmpdir))
+
+ # Gitlab::HTTP resolves the domain to an IP prior to WebMock taking effect, hence the wildcard
+ stub_request(:get, %r{^https://.*/api/v4/projects/gitlab-org%2Fproject-templates%2Frails/repository/commits\?page=1&per_page=1})
+ .to_return(
+ status: 200,
+ body: [{ id: '67812735b83cb42710f22dc98d73d42c8bf4d907' }].to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
end
after do
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index cbb4199954a..70cdc08b4b6 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -70,6 +70,23 @@ describe 'layouts/_head' do
expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />')
end
+ context 'when an asset_host is set and snowplow url is set' do
+ let(:asset_host) { 'http://test.host' }
+
+ before do
+ allow(ActionController::Base).to receive(:asset_host).and_return(asset_host)
+ allow(Gitlab::CurrentSettings).to receive(:snowplow_enabled?).and_return(true)
+ allow(Gitlab::CurrentSettings).to receive(:snowplow_collector_hostname).and_return('www.snow.plow')
+ end
+
+ it 'add a snowplow script tag with asset host' do
+ render
+ expect(rendered).to match('http://test.host/assets/snowplow/')
+ expect(rendered).to match('window.snowplow')
+ expect(rendered).to match('www.snow.plow')
+ end
+ end
+
def stub_helper_with_safe_string(method)
allow_any_instance_of(PageLayoutHelper).to receive(method)
.and_return(%q{foo" http-equiv="refresh}.html_safe)
diff --git a/spec/workers/build_process_worker_spec.rb b/spec/workers/build_process_worker_spec.rb
deleted file mode 100644
index d9a02ece142..00000000000
--- a/spec/workers/build_process_worker_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe BuildProcessWorker do
- describe '#perform' do
- context 'when build exists' do
- let(:pipeline) { create(:ci_pipeline) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it 'processes build' do
- expect_any_instance_of(Ci::Pipeline).to receive(:process!)
- .with([build.id])
-
- described_class.new.perform(build.id)
- end
- end
-
- context 'when build 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/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 39f1beb4efa..3b69b81f12e 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -14,6 +14,10 @@ describe PostReceive do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
end
+ def perform(changes: base64_changes)
+ described_class.new.perform(gl_repository, key_id, changes)
+ end
+
context "as a sidekiq worker" do
it "responds to #perform" do
expect(described_class.new).to respond_to(:perform)
@@ -28,7 +32,7 @@ describe PostReceive do
it "returns false and logs an error" do
expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}")
- expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be(false)
+ expect(perform).to be(false)
end
end
@@ -39,7 +43,7 @@ describe PostReceive do
expect(Git::TagPushService).not_to receive(:new)
expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
- described_class.new.perform(gl_repository, key_id, "")
+ perform(changes: "")
end
end
@@ -50,40 +54,80 @@ describe PostReceive do
expect(Git::BranchPushService).not_to receive(:new)
expect(Git::TagPushService).not_to receive(:new)
- expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false
+ expect(perform).to be false
end
end
context 'with changes' do
before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
+ allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])
end
context "branches" do
- let(:changes) { "123456 789012 refs/heads/tést" }
+ let(:changes) do
+ <<~EOF
+ '123456 789012 refs/heads/tést1'
+ '123456 789012 refs/heads/tést2'
+ EOF
+ end
- it "calls Git::BranchPushService" do
- expect_next_instance_of(Git::BranchPushService) do |service|
+ it 'expires the branches cache' do
+ expect(project.repository).to receive(:expire_branches_cache).once
+
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
+
+ it 'calls Git::BranchPushService' do
+ expect_any_instance_of(Git::BranchPushService) do |service|
expect(service).to receive(:execute).and_return(true)
end
expect(Git::TagPushService).not_to receive(:new)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ perform
end
end
context "tags" do
- let(:changes) { "123456 789012 refs/tags/tag" }
+ let(:changes) do
+ <<~EOF
+ 654321 210987 refs/tags/tag1
+ 654322 210986 refs/tags/tag2
+ 654323 210985 refs/tags/tag3
+ 654324 210984 refs/tags/tag4
+ 654325 210983 refs/tags/tag5
+ EOF
+ end
+
+ before do
+ expect(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])
+ end
+
+ it 'does not expire branches cache' do
+ expect(project.repository).not_to receive(:expire_branches_cache)
+
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
+
+ it "only invalidates tags once" do
+ expect(project.repository).to receive(:repository_event).exactly(5).times.with(:push_tag).and_call_original
+ expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original
+ expect(project.repository).to receive(:expire_tags_cache).once.and_call_original
+
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
it "calls Git::TagPushService" do
- expect(Git::BranchPushService).not_to receive(:execute)
+ expect(Git::BranchPushService).not_to receive(:new)
- expect_next_instance_of(Git::TagPushService) do |service|
+ expect_any_instance_of(Git::TagPushService) do |service|
expect(service).to receive(:execute).and_return(true)
end
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ expect(Git::BranchPushService).not_to receive(:new)
+
+ perform
end
end
@@ -94,7 +138,7 @@ describe PostReceive do
expect(Git::BranchPushService).not_to receive(:new)
expect(Git::TagPushService).not_to receive(:new)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ perform
end
end
@@ -111,7 +155,7 @@ describe PostReceive do
let(:changes_count) { changes.lines.count }
- subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
+ subject { perform }
context "with valid .gitlab-ci.yml" do
before do
@@ -180,7 +224,13 @@ describe PostReceive do
it 'calls SystemHooksService' do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ perform
+ end
+
+ it 'increments the usage data counter of pushes event' do
+ counter = Gitlab::UsageDataCounters::SourceCodeCounter
+
+ expect { perform }.to change { counter.read(:pushes) }.by(1)
end
end
end
@@ -197,7 +247,7 @@ describe PostReceive do
# a second to ensure we see changes.
Timecop.freeze(1.second.from_now) do
expect do
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ perform
project.reload
end.to change(project, :last_activity_at)
.and change(project, :last_repository_updated_at)
@@ -208,7 +258,8 @@ describe PostReceive do
context "webhook" do
it "fetches the correct project" do
expect(Project).to receive(:find_by).with(id: project.id.to_s)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+
+ perform
end
it "does not run if the author is not in the project" do
@@ -218,16 +269,18 @@ describe PostReceive do
expect(project).not_to receive(:execute_hooks)
- expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be_falsey
+ expect(perform).to be_falsey
end
it "asks the project to trigger all hooks" do
+ create(:project_hook, push_events: true, tag_push_events: true, project: project)
+ create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project)
allow(Project).to receive(:find_by).and_return(project)
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ perform
end
it "enqueues a UpdateMergeRequestsWorker job" do
@@ -235,7 +288,7 @@ describe PostReceive do
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
- described_class.new.perform(gl_repository, key_id, base64_changes)
+ perform
end
end
end
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
index 4de51ecb3e9..66d517332ba 100644
--- a/spec/workers/repository_update_remote_mirror_worker_spec.rb
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -2,99 +2,70 @@
require 'rails_helper'
-describe RepositoryUpdateRemoteMirrorWorker do
+describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do
subject { described_class.new }
- let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+ let(:remote_mirror) { create(:remote_mirror) }
let(:scheduled_time) { Time.now - 5.minutes }
around do |example|
Timecop.freeze(Time.now) { example.run }
end
- describe '#perform' do
- context 'with status none' do
- before do
- remote_mirror.update(update_status: 'none')
- end
-
- it 'sets status as finished when update remote mirror service executes successfully' do
- expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
-
- expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished')
- end
-
- it 'resets the notification flag upon success' do
- expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
- remote_mirror.update_column(:error_notification_sent, true)
-
- expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.error_notification_sent }.to(false)
- end
-
- it 'sets status as failed when update remote mirror service executes with errors' do
- error_message = 'fail!'
-
- expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service|
- expect(service).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message)
- end
+ def expect_mirror_service_to_return(mirror, result, tries = 0)
+ expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service|
+ expect(service).to receive(:execute).with(mirror, tries).and_return(result)
+ end
+ end
- # Mock the finder so that it returns an object we can set expectations on
- expect_next_instance_of(RemoteMirrorFinder) do |finder|
- expect(finder).to receive(:execute).and_return(remote_mirror)
- end
- expect(remote_mirror).to receive(:mark_as_failed).with(error_message)
+ describe '#perform' do
+ it 'calls out to the service to perform the update' do
+ expect_mirror_service_to_return(remote_mirror, status: :success)
- expect do
- subject.perform(remote_mirror.id, Time.now)
- end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message)
- end
+ subject.perform(remote_mirror.id, scheduled_time)
+ end
- it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
- remote_mirror.update(last_update_started_at: Time.now)
+ it 'does not do anything if the mirror was already updated' do
+ remote_mirror.update(last_update_started_at: Time.now, update_status: :finished)
- expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true)
- expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror)
+ expect(Projects::UpdateRemoteMirrorService).not_to receive(:new)
- expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil
- end
+ subject.perform(remote_mirror.id, scheduled_time)
end
- context 'with unexpected error' do
- it 'marks mirror as failed' do
- allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError)
+ it 'schedules a retry when the mirror is marked for retrying' do
+ remote_mirror = create(:remote_mirror, update_status: :to_retry)
+ expect_mirror_service_to_return(remote_mirror, status: :error, message: 'Retry!')
- expect do
- subject.perform(remote_mirror.id, Time.now)
- end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError)
- expect(remote_mirror.reload.update_status).to eq('failed')
- end
- end
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 1)
- context 'with another worker already running' do
- before do
- remote_mirror.update(update_status: 'started')
- end
-
- it 'raises RemoteMirrorUpdateAlreadyInProgressError' do
- expect do
- subject.perform(remote_mirror.id, Time.now)
- end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError)
- end
+ subject.perform(remote_mirror.id, scheduled_time)
end
- context 'with status failed' do
- before do
- remote_mirror.update(update_status: 'failed')
+ it 'clears the lease if there was an unexpected exception' do
+ expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service|
+ expect(service).to receive(:execute).with(remote_mirror, 1).and_raise('Unexpected!')
end
+ expect { subject.perform(remote_mirror.id, Time.now, 1) }.to raise_error('Unexpected!')
- it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do
- remote_mirror.update(last_update_started_at: Time.now)
+ lease = Gitlab::ExclusiveLease.new("#{described_class.name}:#{remote_mirror.id}", timeout: 1.second)
- expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false)
- expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
+ expect(lease.try_obtain).not_to be_nil
+ end
- expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished')
- end
+ it 'retries 3 times for the worker to finish before rescheduling' do
+ expect(subject).to receive(:in_lock)
+ .with("#{described_class.name}:#{remote_mirror.id}",
+ retries: 3,
+ ttl: remote_mirror.max_runtime,
+ sleep_sec: described_class::LOCK_WAIT_TIME)
+ .and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ expect(described_class).to receive(:perform_in)
+ .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 0)
+
+ subject.perform(remote_mirror.id, scheduled_time)
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 2a406b92c5f..a295039ec54 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -998,15 +998,15 @@
dependencies:
vue-eslint-parser "^6.0.4"
-"@gitlab/svgs@^1.67.0":
- version "1.67.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.67.0.tgz#c7b94eca13b99fd3aaa737fb6dcc0abc41d3c579"
- integrity sha512-hJOmWEs6RkjzyKkb1vc9wwKGZIBIP0coHkxu/KgOoxhBVudpGk4CH7xJ6UuB2TKpb0SEh5CC1CzRZfBYaFhsaA==
-
-"@gitlab/ui@5.14.0":
- version "5.14.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.14.0.tgz#850214cfc6bb57f7ce672dc1cc60ea3d39ad41f3"
- integrity sha512-JXUmk+hT4Rj2GBh0xAF43dYeloBEDX22rgeaDU6/RzD3JEA353yEm2+HOsBjPkQFDAh6Zp7OZSBuhDFrQe8sbg==
+"@gitlab/svgs@^1.68.0":
+ version "1.68.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.68.0.tgz#d631bd84ea7907592240d8417e82ba66d6a54c0c"
+ integrity sha512-3JmIq0bHg4InjLooM+kQFPfg3d7B1Pye67pN9+12kZXIa0nRGuwKEq3WSbcS+ACdg5jcVPNPYqStItEO4teHdw==
+
+"@gitlab/ui@5.15.0":
+ version "5.15.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.15.0.tgz#1ce92cfed77dcd63a90d751043b42b19e64431c9"
+ integrity sha512-YrYgERcmTxC+oP4GaY7onqvYgvTsyGCiiegQbZbXdNRLGGAmvfxWPzQRz8Ci9yIYkLvi0X2AV7BT8RTVOPQgXg==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"