summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore2
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml14
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml22
-rw-r--r--.markdownlint.json1
-rw-r--r--.rubocop.yml5
-rw-r--r--CHANGELOG.md7
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile9
-rw-r--r--Gemfile.lock18
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js11
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/blob/viewer/index.js3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue97
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue5
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js8
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js32
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js12
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue8
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js23
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js32
-rw-r--r--app/assets/javascripts/transfer_edit.js (renamed from app/assets/javascripts/project_edit.js)6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/plugins/global_toast.js8
-rw-r--r--app/assets/stylesheets/framework/typography.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/utilities.scss6
-rw-r--r--app/controllers/admin/applications_controller.rb4
-rw-r--r--app/controllers/concerns/issuable_collections.rb28
-rw-r--r--app/controllers/concerns/paginated_collection.rb19
-rw-r--r--app/controllers/concerns/sessionless_authentication.rb4
-rw-r--r--app/controllers/concerns/static_object_external_storage.rb24
-rw-r--r--app/controllers/dashboard/snippets_controller.rb17
-rw-r--r--app/controllers/dashboard/todos_controller.rb32
-rw-r--r--app/controllers/explore/snippets_controller.rb13
-rw-r--r--app/controllers/profiles_controller.rb9
-rw-r--r--app/controllers/projects/forks_controller.rb15
-rw-r--r--app/controllers/projects/repositories_controller.rb4
-rw-r--r--app/controllers/projects/snippets_controller.rb19
-rw-r--r--app/controllers/snippets_controller.rb10
-rw-r--r--app/controllers/users_controller.rb12
-rw-r--r--app/helpers/application_helper.rb19
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/releases_helper.rb38
-rw-r--r--app/models/application_setting.rb17
-rw-r--r--app/models/application_setting_implementation.rb4
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/merge_request.rb7
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/pages/lookup_path.rb38
-rw-r--r--app/models/pages/virtual_domain.rb28
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--app/models/project.rb14
-rw-r--r--app/models/project_feature.rb4
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/user.rb17
-rw-r--r--app/presenters/event_presenter.rb20
-rw-r--r--app/services/ci/compare_reports_base_service.rb2
-rw-r--r--app/services/ci/register_job_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb21
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/views/admin/application_settings/_repository_static_objects.html.haml18
-rw-r--r--app/views/admin/application_settings/repository.html.haml11
-rw-r--r--app/views/admin/applications/index.html.haml3
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml2
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/_event_scope.html.haml5
-rw-r--r--app/views/events/event/_common.html.haml4
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/graphiql/rails/editors/show.html.erb18
-rw-r--r--app/views/groups/settings/_advanced.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml20
-rw-r--r--app/views/projects/buttons/_download_links.html.haml3
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/releases/index.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_list.html.haml7
-rw-r--r--app/views/shared/snippets/_snippet.html.haml6
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--changelogs/unreleased/34338-details-element.yml5
-rw-r--r--changelogs/unreleased/61385-replace-vue-resource.yml5
-rw-r--r--changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml5
-rw-r--r--changelogs/unreleased/64799-disable-transfer-group.yml5
-rw-r--r--changelogs/unreleased/65940-run-pipeline.yml5
-rw-r--r--changelogs/unreleased/65988-optimize-snippet-listings.yml5
-rw-r--r--changelogs/unreleased/66637-use-chronic-duration-in-thread-safe-way.yml5
-rw-r--r--changelogs/unreleased/67248-snippet-title-whitespace.yml5
-rw-r--r--changelogs/unreleased/add-source-and-merge_request-to-pipeline-webhook.yml5
-rw-r--r--changelogs/unreleased/ce-indicator-for-pipeline-for-merge-train.yml5
-rw-r--r--changelogs/unreleased/fj-62807-not-prefill-target-branch.yml5
-rw-r--r--changelogs/unreleased/issue-64738.yml5
-rw-r--r--changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml5
-rw-r--r--changelogs/unreleased/security-12-3-bump-pages.yml5
-rw-r--r--changelogs/unreleased/sh-fix-oauth-application-page.yml5
-rw-r--r--changelogs/unreleased/static-objects-external-storage.yml5
-rw-r--r--config/dependency_decisions.yml7
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/chronic_duration.rb2
-rw-r--r--config/initializers/countries.rb61
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/repository.rb2
-rw-r--r--config/webpack.config.js25
-rw-r--r--danger/commit_messages/Dangerfile29
-rw-r--r--db/migrate/20190722104947_add_static_object_token_to_users.rb18
-rw-r--r--db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb14
-rw-r--r--db/migrate/20190725183432_add_index_to_index_on_static_object_token.rb21
-rw-r--r--db/migrate/20190826100605_add_group_column_to_events.rb14
-rw-r--r--db/post_migrate/20190715193142_migrate_discussion_id_on_promoted_epics.rb62
-rw-r--r--db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb17
-rw-r--r--db/schema.rb9
-rw-r--r--doc/administration/auth/smartcard.md69
-rw-r--r--doc/administration/geo/replication/docker_registry.md112
-rw-r--r--doc/administration/geo/replication/index.md2
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/smime_signing_email.md51
-rw-r--r--doc/administration/static_objects_external_storage.md50
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md6
-rw-r--r--doc/administration/troubleshooting/kubernetes_cheat_sheet.md4
-rw-r--r--doc/administration/troubleshooting/linux_cheat_sheet.md2
-rw-r--r--doc/administration/troubleshooting/sidekiq.md2
-rw-r--r--doc/administration/troubleshooting/test_environments.md4
-rw-r--r--doc/api/merge_requests.md60
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/examples/code_quality.md117
-rw-r--r--doc/ci/variables/predefined_variables.md1
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/code_review.md2
-rw-r--r--doc/development/distributed_tracing.md12
-rw-r--r--doc/development/documentation/feature-change-workflow.md9
-rw-r--r--doc/development/documentation/styleguide.md41
-rw-r--r--doc/development/instrumentation.md4
-rw-r--r--doc/development/new_fe_guide/style/prettier.md35
-rw-r--r--doc/development/performance.md26
-rw-r--r--doc/development/rake_tasks.md10
-rw-r--r--doc/development/testing_guide/frontend_testing.md15
-rw-r--r--doc/downgrade_ee_to_ce/README.md16
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/raketasks/import.md10
-rw-r--r--doc/security/asset_proxy.md72
-rw-r--r--doc/subscriptions/index.md2
-rw-r--r--doc/topics/autodevops/index.md10
-rw-r--r--doc/user/analytics/cycle_analytics.md8
-rw-r--r--doc/user/analytics/index.md13
-rw-r--r--doc/user/application_security/dependency_list/img/dependency_list_v12_2.pngbin207114 -> 0 bytes
-rw-r--r--doc/user/application_security/dependency_list/img/dependency_list_v12_3.pngbin0 -> 622199 bytes
-rw-r--r--doc/user/application_security/dependency_list/index.md10
-rw-r--r--doc/user/application_security/dependency_scanning/index.md68
-rw-r--r--doc/user/application_security/index.md25
-rw-r--r--doc/user/discussions/index.md3
-rw-r--r--doc/user/markdown.md64
-rw-r--r--doc/user/permissions.md11
-rw-r--r--doc/user/profile/account/create_accounts.md36
-rw-r--r--doc/user/profile/account/img/admin_user_button.pngbin0 -> 84870 bytes
-rw-r--r--doc/user/profile/account/img/admin_user_form.pngbin0 -> 196691 bytes
-rw-r--r--doc/user/profile/account/img/register_tab.pngbin0 -> 205561 bytes
-rw-r--r--doc/user/profile/index.md4
-rw-r--r--doc/user/project/integrations/github.md9
-rw-r--r--doc/user/project/integrations/prometheus.md17
-rw-r--r--doc/user/project/integrations/webhooks.md16
-rw-r--r--doc/user/project/members/index.md20
-rw-r--r--doc/user/project/merge_requests/code_quality.md113
-rw-r--r--doc/user/project/merge_requests/index.md5
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md4
-rw-r--r--doc/user/project/operations/tracing.md2
-rw-r--r--doc/user/project/pages/getting_started_part_four.md6
-rw-r--r--doc/user/project/protected_branches.md5
-rw-r--r--doc/user/project/settings/index.md2
-rw-r--r--doc/user/snippets.md5
-rw-r--r--doc/workflow/gitlab_flow.md3
-rw-r--r--doc/workflow/lfs/lfs_administration.md6
-rw-r--r--lib/api/entities/internal.rb19
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/helpers/projects_helpers.rb3
-rw-r--r--lib/api/helpers/runner.rb2
-rw-r--r--lib/api/internal/pages.rb7
-rw-r--r--lib/api/merge_requests.rb20
-rw-r--r--lib/api/pages_domains.rb6
-rw-r--r--lib/api/project_import.rb3
-rw-r--r--lib/api/projects.rb3
-rw-r--r--lib/api/users.rb3
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/banzai/filter/color_filter.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb4
-rw-r--r--lib/banzai/filter/footnote_filter.rb4
-rw-r--r--lib/banzai/filter/math_filter.rb8
-rw-r--r--lib/banzai/filter/suggestion_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb4
-rw-r--r--lib/banzai/issuable_extractor.rb4
-rw-r--r--lib/bitbucket/connection.rb4
-rw-r--r--lib/gitlab.rb4
-rw-r--r--lib/gitlab/auth/request_authenticator.rb4
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb22
-rw-r--r--lib/gitlab/background_migration/fix_promoted_epics_discussion_ids.rb22
-rw-r--r--lib/gitlab/data_builder/pipeline.rb17
-rw-r--r--lib/gitlab/database/obsolete_ignored_columns.rb38
-rw-r--r--lib/gitlab/i18n/po_linter.rb8
-rw-r--r--lib/gitlab/issuable_metadata.rb4
-rw-r--r--lib/gitlab/noteable_metadata.rb33
-rw-r--r--lib/gitlab/patch/chronic_duration.rb35
-rw-r--r--lib/gitlab/repository_cache_adapter.rb65
-rw-r--r--lib/gitlab/repository_set_cache.rb67
-rw-r--r--lib/gitlab/sidekiq_daemon/monitor.rb184
-rw-r--r--lib/gitlab/sidekiq_middleware/monitor.rb4
-rw-r--r--lib/gitlab/sidekiq_monitor.rb182
-rw-r--r--lib/gitlab/time_tracking_formatter.rb47
-rw-r--r--lib/gitlab/utils/strong_memoize.rb6
-rw-r--r--lib/google_api/cloud_platform/client.rb4
-rw-r--r--lib/quality/test_level.rb2
-rw-r--r--lib/system_check/app/init_script_up_to_date_check.rb2
-rw-r--r--lib/system_check/app/redis_version_check.rb2
-rw-r--r--lib/tasks/db_obsolete_ignored_columns.rake21
-rw-r--r--lib/tasks/gitlab/graphql.rake4
-rw-r--r--lib/tasks/gitlab/uploads/legacy.rake2
-rw-r--r--locale/gitlab.pot49
-rw-r--r--qa/qa.rb3
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/main/login.rb6
-rw-r--r--qa/qa/page/merge_request/show.rb6
-rw-r--r--qa/qa/page/project/sub_menus/repository.rb2
-rw-r--r--qa/qa/resource/api_fabricator.rb16
-rw-r--r--qa/qa/resource/base.rb12
-rw-r--r--qa/qa/resource/merge_request.rb4
-rw-r--r--qa/qa/resource/project_member.rb35
-rw-r--r--qa/qa/resource/protected_branch.rb (renamed from qa/qa/resource/branch.rb)53
-rw-r--r--qa/qa/resource/repository/project_push.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb2
-rw-r--r--qa/qa/support/api.rb1
-rw-r--r--rubocop/cop/scalability/file_uploads.rb61
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--scripts/review_apps/base-config.yaml151
-rwxr-xr-xscripts/review_apps/review-apps.sh117
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb10
-rw-r--r--spec/controllers/concerns/static_object_external_storage_spec.rb96
-rw-r--r--spec/controllers/dashboard/snippets_controller_spec.rb21
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb24
-rw-r--r--spec/controllers/explore/snippets_controller_spec.rb15
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb61
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb54
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb54
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb32
-rw-r--r--spec/controllers/snippets_controller_spec.rb9
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb46
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb32
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb5
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb8
-rw-r--r--spec/features/projects/fork_spec.rb1
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb5
-rw-r--r--spec/fixtures/api/schemas/internal/pages/lookup_path.json25
-rw-r--r--spec/fixtures/api/schemas/internal/pages/virtual_domain.json16
-rw-r--r--spec/fixtures/valid.po5
-rw-r--r--spec/frontend/transfer_edit_spec.js53
-rw-r--r--spec/frontend/vue_shared/plugins/global_toast_spec.js24
-rw-r--r--spec/helpers/application_helper_spec.rb37
-rw-r--r--spec/helpers/events_helper_spec.rb2
-rw-r--r--spec/helpers/releases_helper_spec.rb46
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js167
-rw-r--r--spec/javascripts/reports/components/modal_open_name_spec.js2
-rw-r--r--spec/javascripts/sidebar/mock_data.js10
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js4
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js33
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js86
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js4
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js37
-rw-r--r--spec/lib/gitlab/auth/user_auth_finders_spec.rb54
-rw-r--r--spec/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb18
-rw-r--r--spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb43
-rw-r--r--spec/lib/gitlab/noteable_metadata_spec.rb29
-rw-r--r--spec/lib/gitlab/patch/chronic_duration_spec.rb27
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb7
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb75
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb (renamed from spec/lib/gitlab/sidekiq_monitor_spec.rb)4
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb6
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb16
-rw-r--r--spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb81
-rw-r--r--spec/models/application_setting_spec.rb14
-rw-r--r--spec/models/ci/pipeline_spec.rb16
-rw-r--r--spec/models/oauth_access_token_spec.rb28
-rw-r--r--spec/models/pages/lookup_path_spec.rb64
-rw-r--r--spec/models/pages/virtual_domain_spec.rb43
-rw-r--r--spec/models/pages_domain_spec.rb12
-rw-r--r--spec/models/project_feature_spec.rb54
-rw-r--r--spec/models/project_spec.rb11
-rw-r--r--spec/models/user_spec.rb10
-rw-r--r--spec/presenters/event_presenter_spec.rb41
-rw-r--r--spec/requests/api/applications_spec.rb2
-rw-r--r--spec/requests/api/events_spec.rb2
-rw-r--r--spec/requests/api/import_github_spec.rb2
-rw-r--r--spec/requests/api/internal/pages_spec.rb28
-rw-r--r--spec/requests/api/merge_requests_spec.rb64
-rw-r--r--spec/requests/api/project_events_spec.rb2
-rw-r--r--spec/routing/project_routing_spec.rb70
-rw-r--r--spec/rubocop/cop/scalability/file_uploads_spec.rb54
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb108
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb4
-rw-r--r--spec/services/search/global_service_spec.rb8
-rw-r--r--spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb59
328 files changed, 4807 insertions, 1423 deletions
diff --git a/.dockerignore b/.dockerignore
index 4f5b33de167..d5568619169 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -10,6 +10,7 @@
# - ./INSTALLATION_TYPE
# - ./VERSION
+/.git/
/app/
/bin/
/builds/
@@ -69,6 +70,7 @@
/locale/
/log/
/modules/
+/node_modules/
/plugins/
/public/
/rubocop/
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index a20215694c0..b2058506ea1 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -232,3 +232,17 @@ qa-frontend-node:latest:
extends: .qa-frontend-node
image: node:latest
allow_failure: true
+
+webpack-dev-server:
+ extends:
+ - .default-tags
+ - .default-retry
+ - .default-cache
+ - .except-docs-qa
+ dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
+ variables:
+ SETUP_DB: "false"
+ WEBPACK_MEMORY_TEST: "true"
+ script:
+ - node --version
+ - node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 6f1505b5c0d..5db194e06f7 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -74,12 +74,10 @@ schedule:review-build-cng:
- .review-schedules-only
needs: ["gitlab:assets:compile"]
-review-deploy:
+.review-deploy-base:
extends: .review-base
allow_failure: true
- retry: 1
stage: review
- needs: ["review-build-cng"]
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
@@ -109,10 +107,16 @@ review-deploy:
expire_in: 2 days
when: always
+review-deploy:
+ extends:
+ - .review-deploy-base
+ needs: ["review-build-cng"]
+
schedule:review-deploy:
extends:
- - review-deploy
+ - .review-deploy-base
- .review-schedules-only
+ needs: ["schedule:review-build-cng"]
review-stop:
extends: review-deploy
@@ -134,11 +138,19 @@ review-stop:
artifacts:
paths: []
+review-cleanup-failed-deployment:
+ extends: review-stop
+ stage: prepare
+ when: on_success
+ needs: []
+ allow_failure: false
+ script:
+ - delete_failed_release
+
.review-qa-base:
extends:
- .review-docker
- .review-only
- retry: 2
stage: qa
variables:
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
diff --git a/.markdownlint.json b/.markdownlint.json
index 2c40c0859f0..f06c0766c38 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -8,7 +8,6 @@
"style": "dash"
},
"line-length": false,
- "commands-show-output": false,
"no-duplicate-header": {
"allow_different_nesting": true
},
diff --git a/.rubocop.yml b/.rubocop.yml
index f24cbb6ce92..73743ebf9a2 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -275,3 +275,8 @@ RSpec/BeSuccessMatcher:
- 'ee/spec/support/shared_examples/controllers/**/*'
- 'spec/support/controllers/**/*'
- 'ee/spec/support/controllers/**/*'
+Scalability/FileUploads:
+ Enabled: true
+ Include:
+ - 'lib/api/**/*.rb'
+ - 'ee/lib/api/**/*.rb'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a432b091c7e..e379c23ee3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.2.5
+
+### Security (1 change)
+
+- Upgrade pages to 1.7.2.
+
+
## 12.2.4
### Fixed (7 changes)
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 27f9cd322bb..a8fdfda1c78 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.8.0
+1.8.1
diff --git a/Gemfile b/Gemfile
index ac848cce5e8..d79e97aabdd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -84,7 +84,9 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.9.11'
-gem 'graphiql-rails', '~> 1.4.10'
+# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 will be released
+# https://gitlab.com/gitlab-org/gitlab-ce/issues/67263
+gem 'graphiql-rails', '~> 1.7.0'
gem 'apollo_upload_server', '~> 2.0.0.beta3'
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
@@ -263,7 +265,7 @@ gem 'fast_blank'
# Parse time & duration
gem 'chronic', '~> 0.10.2'
-gem 'chronic_duration', '~> 0.10.6'
+gem 'gitlab_chronic_duration', '~> 0.10.6.1'
gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0'
@@ -445,3 +447,6 @@ gem 'unleash', '~> 0.1.5'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
+
+# Countries list
+gem 'countries', '~> 3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 48053e5740e..025542422d3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -136,8 +136,6 @@ GEM
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
- chronic_duration (0.10.6)
- numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
claide (1.0.3)
@@ -159,6 +157,10 @@ GEM
contracts (0.11.0)
cork (0.3.0)
colored2 (~> 3.1)
+ countries (3.0.0)
+ i18n_data (~> 0.8.0)
+ sixarm_ruby_unaccent (~> 1.1)
+ unicode_utils (~> 1.4)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
@@ -352,6 +354,8 @@ GEM
rubocop-gitlab-security (~> 0.1.0)
rubocop-performance (~> 1.1.0)
rubocop-rspec (~> 1.19)
+ gitlab_chronic_duration (0.10.6.1)
+ numerizer (~> 0.1.1)
gitlab_omniauth-ldap (2.1.1)
net-ldap (~> 0.16)
omniauth (~> 1.3)
@@ -398,7 +402,7 @@ GEM
rake (~> 12)
grape_logging (1.7.0)
grape
- graphiql-rails (1.4.10)
+ graphiql-rails (1.7.0)
railties
sprockets-rails
graphql (1.9.11)
@@ -458,6 +462,7 @@ GEM
httpclient (2.8.3)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
+ i18n_data (0.8.0)
icalendar (2.4.1)
ice_nine (0.11.2)
influxdb (0.2.3)
@@ -924,6 +929,7 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
+ sixarm_ruby_unaccent (1.2.0)
slack-notifier (1.5.1)
snowplow-tracker (0.6.1)
contracts (~> 0.7, <= 0.11)
@@ -988,6 +994,7 @@ GEM
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.6.0)
+ unicode_utils (1.4.0)
unicorn (5.4.1)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -1082,10 +1089,10 @@ DEPENDENCIES
carrierwave (~> 1.3)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
- chronic_duration (~> 0.10.6)
commonmarker (~> 0.17)
concurrent-ruby (~> 1.1)
connection_pool (~> 2.0)
+ countries (~> 3.0)
creole (~> 0.5.0)
danger (~> 6.0)
database_cleaner (~> 1.7.0)
@@ -1133,6 +1140,7 @@ DEPENDENCIES
gitlab-peek (~> 0.0.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 2.7)
+ gitlab_chronic_duration (~> 0.10.6.1)
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
google-api-client (~> 0.23)
@@ -1142,7 +1150,7 @@ DEPENDENCIES
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.1)
grape_logging (~> 1.7)
- graphiql-rails (~> 1.4.10)
+ graphiql-rails (~> 1.7.0)
graphql (~> 1.9.11)
graphql-docs (~> 1.6.0)
grpc (~> 1.19.0)
diff --git a/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js b/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js
new file mode 100644
index 00000000000..6a40f1cbc5e
--- /dev/null
+++ b/app/assets/javascripts/analytics/cycle_analytics/mixins/add_stage_mixin.js
@@ -0,0 +1,11 @@
+export default {
+ data() {
+ return {
+ isCustomStageForm: false,
+ };
+ },
+ methods: {
+ showAddStageForm: () => {},
+ hideAddStageForm: () => {},
+ },
+};
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1d97ad5ec11..992c5e5e330 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
+ mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: 'api/:version/application/statistics',
group(groupId, callback) {
@@ -371,6 +372,14 @@ const Api = {
});
},
+ postMergeRequestPipeline(id, { mergeRequestId }) {
+ const url = Api.buildUrl(this.mergeRequestsPipeline)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':merge_request_iid', mergeRequestId);
+
+ return axios.post(url);
+ },
+
releases(id) {
const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id));
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index d246a1f6064..9ea455069f3 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
@@ -105,7 +106,6 @@ export default class BlobViewer {
toggleCopyButtonState() {
if (!this.copySourceBtn) return;
-
if (this.simpleViewer.getAttribute('data-loaded')) {
this.copySourceBtn.setAttribute('title', __('Copy source to clipboard'));
this.copySourceBtn.classList.remove('disabled');
@@ -152,7 +152,6 @@ export default class BlobViewer {
this.activeViewer = newViewer;
this.toggleCopyButtonState();
-
BlobViewer.loadViewer(newViewer)
.then(viewer => {
$(viewer).renderGFM();
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 4890f99e9d1..e5b030d4900 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,14 +1,19 @@
<script>
-import PipelinesService from '../../pipelines/services/pipelines_service';
-import PipelineStore from '../../pipelines/stores/pipelines_store';
-import pipelinesMixin from '../../pipelines/mixins/pipelines';
-import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
-import { getParameterByName } from '../../lib/utils/common_utils';
-import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import PipelinesService from '~/pipelines/services/pipelines_service';
+import PipelineStore from '~/pipelines/stores/pipelines_store';
+import pipelinesMixin from '~/pipelines/mixins/pipelines';
+import eventHub from '~/pipelines/event_hub';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
+import bp from '~/breakpoints';
export default {
components: {
TablePagination,
+ GlButton,
+ GlLoadingIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
@@ -33,6 +38,21 @@ export default {
required: false,
default: 'child',
},
+ canRunPipeline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ mergeRequestId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
@@ -53,6 +73,41 @@ export default {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
+ /**
+ * The Run Pipeline button can only be rendered when:
+ * - In MR view - we use `canRunPipeline` for that purpose
+ * - If the latest pipeline has the `detached_merge_request_pipeline` flag
+ *
+ * @returns {Boolean}
+ */
+ canRenderPipelineButton() {
+ return this.canRunPipeline && this.latestPipelineDetachedFlag;
+ },
+ /**
+ * Checks if either `detached_merge_request_pipeline` or
+ * `merge_request_pipeline` are tru in the first
+ * object in the pipelines array.
+ *
+ * @returns {Boolean}
+ */
+ latestPipelineDetachedFlag() {
+ const latest = this.state.pipelines[0];
+ return (
+ latest &&
+ latest.flags &&
+ (latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline)
+ );
+ },
+ /**
+ * When we are on Desktop and the button is visible
+ * we need to add a negative margin to the table
+ * to make it inline with the button
+ *
+ * @returns {Boolean}
+ */
+ shouldAddNegativeMargin() {
+ return this.canRenderPipelineButton && bp.isDesktop();
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -77,6 +132,22 @@ export default {
this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
}
},
+ /**
+ * When the user clicks on the Run Pipeline button
+ * we need to make a post request and
+ * to update the table content once the request is finished.
+ *
+ * We are emitting an event through the eventHub using the old pattern
+ * to make use of the code in mixins/pipelines.js that handles all the
+ * table events
+ *
+ */
+ onClickRunPipeline() {
+ eventHub.$emit('runMergeRequestPipeline', {
+ projectId: this.projectId,
+ mergeRequestId: this.mergeRequestId,
+ });
+ },
},
};
</script>
@@ -99,11 +170,25 @@ export default {
/>
<div v-else-if="shouldRenderTable" class="table-holder">
+ <div v-if="canRenderPipelineButton" class="nav justify-content-end">
+ <gl-button
+ v-if="canRenderPipelineButton"
+ variant="success"
+ class="js-run-mr-pipeline prepend-top-10 btn-wide-on-xs"
+ :disabled="state.isRunningMergeRequestPipeline"
+ @click="onClickRunPipeline"
+ >
+ <gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline />
+ {{ s__('Pipelines|Run Pipeline') }}
+ </gl-button>
+ </div>
+
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
+ :class="{ 'negative-margin-top': shouldAddNegativeMargin }"
/>
</div>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
index d946594a069..63549596fac 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
@@ -23,7 +23,10 @@ export default {
</script>
<template>
- <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded">
+ <div
+ :class="{ active: isActive }"
+ class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
+ >
<slot></slot>
<div v-if="canEdit" class="dropdown">
<gl-button
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index b3ae47af750..c9a6b10b2f3 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlEmptyState } from '@gitlab/ui';
import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
+import addStageMixin from 'ee_else_ce/analytics/cycle_analytics/mixins/add_stage_mixin';
import Flash from '../flash';
import { __ } from '~/locale';
import Translate from '../vue_shared/translate';
@@ -43,8 +44,12 @@ export default () => {
DateRangeDropdown: () =>
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
'stage-nav-item': stageNavItem,
+ CustomStageForm: () =>
+ import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'),
+ AddStageButton: () =>
+ import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'),
},
- mixins: [filterMixins],
+ mixins: [filterMixins, addStageMixin],
data() {
return {
store: CycleAnalyticsStore,
@@ -124,6 +129,7 @@ export default () => {
return;
}
+ this.hideAddStageForm();
this.isLoadingStage = true;
this.store.setStageEvents([], stage);
this.store.setActiveStage(stage);
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index ce0c9256148..cec824a529c 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -14,7 +14,7 @@ export default class TransferDropdown {
}
buildDropdown() {
- const extraOptions = [{ id: '', text: __('No parent group') }, 'divider'];
+ const extraOptions = [{ id: '-1', text: __('No parent group') }, 'divider'];
this.groupDropdown.glDropdown({
selectable: true,
@@ -33,5 +33,6 @@ export default class TransferDropdown {
assignSelected(selected) {
this.parentInput.val(selected.id);
+ this.parentInput.change();
}
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index b6868e63716..52674107df2 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -333,7 +333,8 @@ export default class MergeRequestTabs {
mountPipelinesView() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- const { CommitPipelinesTable } = gl;
+ const { CommitPipelinesTable, mrWidgetData } = gl;
+
this.commitPipelinesTable = new CommitPipelinesTable({
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
@@ -341,6 +342,9 @@ export default class MergeRequestTabs {
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
+ canRunPipeline: true,
+ projectId: pipelineTableViewEl.dataset.projectId,
+ mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
},
}).$mount();
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 88454c3fb4c..358f49deb35 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions } from 'vuex';
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index d036ff07d89..f32392c9e29 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -2,6 +2,7 @@ import initAvatarPicker from '~/avatar_picker';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
+import setupTransferEdit from '~/transfer_edit';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants';
@@ -17,6 +18,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
);
mountBadgeSettings(GROUP_BADGE);
+ setupTransferEdit('.js-group-transfer-form', '#new_parent_group_id');
// Initialize Subgroups selector
groupsSelect();
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 92ed6a652d7..c9dbe576c4b 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,6 +1,6 @@
import { PROJECT_BADGE } from '~/badges/constants';
import initSettingsPanels from '~/settings_panels';
-import setupProjectEdit from '~/project_edit';
+import setupTransferEdit from '~/transfer_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
@@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
initProjectLoadingSpinner();
initProjectPermissionsSettings();
- setupProjectEdit();
+ setupTransferEdit('.js-project-transfer-form', 'select.select2');
dirtySubmitFactory(
document.querySelectorAll(
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 126a9a47a2b..876b30299fb 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '../../locale';
-import Flash from '../../flash';
+import createFlash from '../../flash';
import Poll from '../../lib/utils/poll';
import EmptyState from '../components/empty_state.vue';
import SvgBlankState from '../components/blank_state.vue';
@@ -62,6 +62,7 @@ export default {
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
+ eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
@@ -69,6 +70,7 @@ export default {
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
+ eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
destroyed() {
this.poll.stop();
@@ -110,7 +112,7 @@ export default {
// Stop polling
this.poll.stop();
// Restarting the poll also makes an initial request
- this.poll.restart();
+ return this.poll.restart();
},
fetchPipelines() {
if (!this.isMakingRequest) {
@@ -156,7 +158,31 @@ export default {
this.service
.postAction(endpoint)
.then(() => this.updateTable())
- .catch(() => Flash(__('An error occurred while making the request.')));
+ .catch(() => createFlash(__('An error occurred while making the request.')));
+ },
+
+ /**
+ * When the user clicks on the run pipeline button
+ * we toggle the state of the button to be disabled
+ *
+ * Once the post request has finished, we fetch the
+ * pipelines again to show the most recent data
+ *
+ * Once the pipeline has been updated, we toggle back the
+ * loading state and re-enable the run pipeline button
+ */
+ runMergeRequestPipeline(options) {
+ this.store.toggleIsRunningPipeline(true);
+
+ this.service
+ .runMRPipeline(options)
+ .then(() => this.updateTable())
+ .catch(() => {
+ createFlash(
+ __('An error occurred while trying to run a new pipeline for this Merge Request.'),
+ );
+ })
+ .finally(() => this.store.toggleIsRunningPipeline(false));
},
},
};
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 8317d3f4510..3c755db23dc 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,4 +1,5 @@
import axios from '../../lib/utils/axios_utils';
+import Api from '~/api';
export default class PipelinesService {
/**
@@ -39,4 +40,9 @@ export default class PipelinesService {
postAction(endpoint) {
return axios.post(`${endpoint}.json`);
}
+
+ // eslint-disable-next-line class-methods-use-this
+ runMRPipeline({ projectId, mergeRequestId }) {
+ return Api.postMergeRequestPipeline(projectId, { mergeRequestId });
+ }
}
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js
index 651251d2623..a4bbada89c8 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js
@@ -7,6 +7,9 @@ export default class PipelinesStore {
this.state.pipelines = [];
this.state.count = {};
this.state.pageInfo = {};
+
+ // Used in MR Pipelines tab
+ this.state.isRunningMergeRequestPipeline = false;
}
storePipelines(pipelines = []) {
@@ -29,4 +32,13 @@ export default class PipelinesStore {
this.state.pageInfo = paginationInfo;
}
+
+ /**
+ * Toggles the isRunningPipeline flag
+ *
+ * @param {Boolean} value
+ */
+ toggleIsRunningPipeline(value = false) {
+ this.state.isRunningMergeRequestPipeline = value;
+ }
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index c6cc04a139f..ce592720531 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -67,18 +67,14 @@ export default {
saveAssignees() {
this.loading = true;
- function setLoadingFalse() {
- this.loading = false;
- }
-
this.mediator
.saveAssignees(this.field)
- .then(setLoadingFalse.bind(this))
.then(() => {
+ this.loading = false;
refreshUserMergeRequestCounts();
})
.catch(() => {
- setLoadingFalse();
+ this.loading = false;
return new Flash(__('Error occurred when saving assignees'));
});
},
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index cbe20f761ff..feb08e3acaf 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,7 +1,4 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class SidebarService {
constructor(endpointMap) {
@@ -18,23 +15,15 @@ export default class SidebarService {
}
get() {
- return Vue.http.get(this.endpoint);
+ return axios.get(this.endpoint);
}
update(key, data) {
- return Vue.http.put(
- this.endpoint,
- {
- [key]: data,
- },
- {
- emulateJSON: true,
- },
- );
+ return axios.put(this.endpoint, { [key]: data });
}
getProjectsAutocomplete(searchTerm) {
- return Vue.http.get(this.projectsAutocompleteEndpoint, {
+ return axios.get(this.projectsAutocompleteEndpoint, {
params: {
search: searchTerm,
},
@@ -42,11 +31,11 @@ export default class SidebarService {
}
toggleSubscription() {
- return Vue.http.post(this.toggleSubscriptionEndpoint);
+ return axios.post(this.toggleSubscriptionEndpoint);
}
moveIssue(moveToProjectId) {
- return Vue.http.post(this.moveIssueEndpoint, {
+ return axios.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
});
}
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 643fe6c00b6..4a7000cbbda 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -32,7 +32,10 @@ export default class SidebarMediator {
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
- return this.service.update(field, selected.length === 0 ? [0] : selected);
+ const assignees = selected.length === 0 ? [0] : selected;
+ const data = { assignee_ids: assignees };
+
+ return this.service.update(field, data);
}
setMoveToProjectId(projectId) {
@@ -42,8 +45,7 @@ export default class SidebarMediator {
fetch() {
return this.service
.get()
- .then(response => response.json())
- .then(data => {
+ .then(({ data }) => {
this.processFetchedData(data);
})
.catch(() => new Flash(__('Error occurred when fetching sidebar data')));
@@ -71,23 +73,17 @@ export default class SidebarMediator {
}
fetchAutocompleteProjects(searchTerm) {
- return this.service
- .getProjectsAutocomplete(searchTerm)
- .then(response => response.json())
- .then(data => {
- this.store.setAutocompleteProjects(data);
- return this.store.autocompleteProjects;
- });
+ return this.service.getProjectsAutocomplete(searchTerm).then(({ data }) => {
+ this.store.setAutocompleteProjects(data);
+ return this.store.autocompleteProjects;
+ });
}
moveIssue() {
- return this.service
- .moveIssue(this.store.moveToProjectId)
- .then(response => response.json())
- .then(data => {
- if (window.location.pathname !== data.web_url) {
- visitUrl(data.web_url);
- }
- });
+ return this.service.moveIssue(this.store.moveToProjectId).then(({ data }) => {
+ if (window.location.pathname !== data.web_url) {
+ visitUrl(data.web_url);
+ }
+ });
}
}
diff --git a/app/assets/javascripts/project_edit.js b/app/assets/javascripts/transfer_edit.js
index 47bf2226781..bb15e11fd4c 100644
--- a/app/assets/javascripts/project_edit.js
+++ b/app/assets/javascripts/transfer_edit.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
-export default function setupProjectEdit() {
- const $transferForm = $('.js-project-transfer-form');
- const $selectNamespace = $transferForm.find('select.select2');
+export default function setupTransferEdit(formSelector, targetSelector) {
+ const $transferForm = $(formSelector);
+ const $selectNamespace = $transferForm.find(targetSelector);
$selectNamespace.on('change', () => {
$transferForm.find(':submit').prop('disabled', !$selectNamespace.val());
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 40c095aa954..4b5201bbca7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/require-default-prop */
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
+import { sprintf, s__ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
@@ -73,8 +73,8 @@ export default {
},
errorText() {
return sprintf(
- __(
- 'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
+ s__(
+ 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
),
{
linkStart: `<a href="${this.troubleshootingDocsPath}">`,
@@ -89,6 +89,9 @@ export default {
isMergeRequestPipeline() {
return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline);
},
+ showSourceBranch() {
+ return Boolean(this.pipeline.ref.branch);
+ },
},
};
</script>
@@ -109,7 +112,7 @@ export default {
<div class="ci-widget-content">
<div class="media-body">
<div class="font-weight-bold js-pipeline-info-container">
- {{ s__('Pipeline|Pipeline') }}
+ {{ pipeline.details.name }}
<gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
>#{{ pipeline.id }}</gl-link
>
@@ -121,48 +124,13 @@ export default {
class="commit-sha js-commit-link font-weight-normal"
>{{ pipeline.commit.short_id }}</gl-link
>
+ </template>
+ <template v-if="showSourceBranch">
{{ s__('Pipeline|on') }}
- <template v-if="isTriggeredByMergeRequest">
- <gl-link
- v-gl-tooltip
- :href="pipeline.merge_request.path"
- :title="pipeline.merge_request.title"
- class="font-weight-normal"
- >!{{ pipeline.merge_request.iid }}</gl-link
- >
- {{ s__('Pipeline|with') }}
- <tooltip-on-truncate
- :title="pipeline.merge_request.source_branch"
- truncate-target="child"
- class="label-branch label-truncate"
- >
- <gl-link
- :href="pipeline.merge_request.source_branch_path"
- class="font-weight-normal"
- >{{ pipeline.merge_request.source_branch }}</gl-link
- >
- </tooltip-on-truncate>
-
- <template v-if="isMergeRequestPipeline">
- {{ s__('Pipeline|into') }}
- <tooltip-on-truncate
- :title="pipeline.merge_request.target_branch"
- truncate-target="child"
- class="label-branch label-truncate"
- >
- <gl-link
- :href="pipeline.merge_request.target_branch_path"
- class="font-weight-normal"
- >{{ pipeline.merge_request.target_branch }}</gl-link
- >
- </tooltip-on-truncate>
- </template>
- </template>
<tooltip-on-truncate
- v-else
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate"
+ class="label-branch label-truncate font-weight-normal"
v-html="sourceBranchLink"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b520d302407..326440f5013 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
@@ -9,6 +10,7 @@ import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
import icon from '../icon.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import axios from '~/lib/utils/axios_utils';
export default {
components: {
@@ -167,10 +169,9 @@ export default {
if (text) {
this.markdownPreviewLoading = true;
this.markdownPreview = __('Loading…');
- this.$http
+ axios
.post(this.markdownPreviewPath, { text })
- .then(resp => resp.json())
- .then(data => this.renderMarkdown(data))
+ .then(response => this.renderMarkdown(response.data))
.catch(() => new Flash(__('Error loading markdown preview')));
} else {
this.renderMarkdown();
diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js
new file mode 100644
index 00000000000..c0de1cdc615
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js
@@ -0,0 +1,8 @@
+import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
+
+Vue.use(GlToast);
+
+export default function showGlobalToast(...args) {
+ return Vue.toasted.show(...args);
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 33caac4d725..ba123ff9a67 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -67,6 +67,18 @@
max-height: calc(100vh - 100px);
}
+ details {
+ margin-bottom: $gl-padding;
+
+ summary {
+ margin-bottom: $gl-padding;
+ }
+
+ *:first-child:not(summary) {
+ margin-top: $gl-padding;
+ }
+ }
+
// Single code lines should wrap
code {
font-family: $monospace-font;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 15a779dde1d..faa0a9909d5 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -726,6 +726,7 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
+$pipelines-table-header-height: 40px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index d80155a416d..e20711a193d 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -41,7 +41,6 @@
width: 20%;
}
-
.fa {
color: $cycle-analytics-light-gray;
@@ -146,7 +145,6 @@
.stage-nav-item {
line-height: 65px;
- border: 1px solid $border-color;
&.active {
background: $blue-50;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index d4bd5b1b7dc..cda6c9ce0cc 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -26,6 +26,10 @@
}
.pipelines {
+ .negative-margin-top {
+ margin-top: -$pipelines-table-header-height;
+ }
+
.stage {
max-width: 90px;
width: 90px;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 3648ec5e239..d2906ce0780 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -15,3 +15,9 @@
font-size: $size;
}
}
+
+.border-width-1px { border-width: 1px; }
+.border-style-dashed { border-style: dashed; }
+.border-style-solid { border-style: solid; }
+.border-color-blue-300 { border-color: $blue-300; }
+.border-color-default { border-color: $border-color; }
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 3648c8be426..22e629ccf59 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -7,7 +7,9 @@ class Admin::ApplicationsController < Admin::ApplicationController
before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
- @applications = ApplicationsFinder.new.execute
+ applications = ApplicationsFinder.new.execute
+ @applications = Kaminari.paginate_array(applications).page(params[:page])
+ @application_counts = OauthAccessToken.distinct_resource_owner_counts(@applications)
end
def show
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 8ea77b994de..88044cf7557 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,6 +2,7 @@
module IssuableCollections
extend ActiveSupport::Concern
+ include PaginatedCollection
include SortingHelper
include SortingPreference
include Gitlab::IssuableMetadata
@@ -17,8 +18,11 @@ module IssuableCollections
def set_issuables_index
@issuables = issuables_collection
- set_pagination
- return if redirect_out_of_range(@total_pages)
+ unless pagination_disabled?
+ set_pagination
+
+ return if redirect_out_of_range(@issuables, @total_pages)
+ end
if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -38,12 +42,10 @@ module IssuableCollections
end
def set_pagination
- return if pagination_disabled?
-
@issuables = @issuables.page(params[:page])
@issuables = per_page_for_relative_position if params[:sort] == 'relative_position'
@issuable_meta_data = issuable_meta_data(@issuables, collection_type, current_user)
- @total_pages = issuable_page_count
+ @total_pages = issuable_page_count(@issuables)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -57,20 +59,8 @@ module IssuableCollections
end
# rubocop: enable CodeReuse/ActiveRecord
- def redirect_out_of_range(total_pages)
- return false if total_pages.nil? || total_pages.zero?
-
- out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
- if out_of_range
- redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
- end
-
- out_of_range
- end
-
- def issuable_page_count
- page_count_for_relation(@issuables, finder.row_count) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def issuable_page_count(relation)
+ page_count_for_relation(relation, finder.row_count)
end
def page_count_for_relation(relation, row_count)
diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb
new file mode 100644
index 00000000000..be84215a9e2
--- /dev/null
+++ b/app/controllers/concerns/paginated_collection.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module PaginatedCollection
+ extend ActiveSupport::Concern
+
+ private
+
+ def redirect_out_of_range(collection, total_pages = collection.total_pages)
+ return false if total_pages.zero?
+
+ out_of_range = collection.current_page > total_pages
+
+ if out_of_range
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
+ end
+
+ out_of_range
+ end
+end
diff --git a/app/controllers/concerns/sessionless_authentication.rb b/app/controllers/concerns/sessionless_authentication.rb
index 4304b8565ce..ba06384a37a 100644
--- a/app/controllers/concerns/sessionless_authentication.rb
+++ b/app/controllers/concerns/sessionless_authentication.rb
@@ -2,10 +2,10 @@
# == SessionlessAuthentication
#
-# Controller concern to handle PAT and RSS token authentication methods
+# Controller concern to handle PAT, RSS, and static objects token authentication methods
#
module SessionlessAuthentication
- # This filter handles personal access tokens, and atom requests with rss tokens
+ # This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
diff --git a/app/controllers/concerns/static_object_external_storage.rb b/app/controllers/concerns/static_object_external_storage.rb
new file mode 100644
index 00000000000..dbfe0ed3adf
--- /dev/null
+++ b/app/controllers/concerns/static_object_external_storage.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module StaticObjectExternalStorage
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationHelper
+ end
+
+ def redirect_to_external_storage
+ return if external_storage_request?
+
+ redirect_to external_storage_url_or_path(request.fullpath, project)
+ end
+
+ def external_storage_request?
+ header_token = request.headers['X-Gitlab-External-Storage-Token']
+ return false unless header_token.present?
+
+ external_storage_token = Gitlab::CurrentSettings.static_objects_external_storage_auth_token
+ ActiveSupport::SecurityUtils.secure_compare(header_token, external_storage_token) ||
+ raise(Gitlab::Access::AccessDeniedError)
+ end
+end
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index 161c22046f9..6feade3df03 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,14 +1,19 @@
# frozen_string_literal: true
class Dashboard::SnippetsController < Dashboard::ApplicationController
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
+
skip_cross_project_access_check :index
def index
- @snippets = SnippetsFinder.new(
- current_user,
- author: current_user,
- scope: params[:scope]
- ).execute
- @snippets = @snippets.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: current_user, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 8f6fcb362d2..940d1482611 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,6 +2,7 @@
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
+ include PaginatedCollection
before_action :authorize_read_project!, only: :index
before_action :authorize_read_group!, only: :index
@@ -12,7 +13,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@todos = @todos.page(params[:page])
@todos = @todos.with_entity_associations
- return if redirect_out_of_range(@todos)
+ return if redirect_out_of_range(@todos, todos_page_count(@todos))
end
def destroy
@@ -82,28 +83,15 @@ class Dashboard::TodosController < Dashboard::ApplicationController
}
end
- def todo_params
- params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def redirect_out_of_range(todos)
- total_pages =
- if todo_params.except(:sort, :page).empty?
- (current_user.todos_pending_count.to_f / todos.limit_value).ceil
- else
- todos.total_pages
- end
-
- return false if total_pages.zero?
-
- out_of_range = todos.current_page > total_pages
-
- if out_of_range
- redirect_to url_for(safe_params.merge(page: total_pages, only_path: true))
+ def todos_page_count(todos)
+ if todo_params.except(:sort, :page).empty? # rubocop: disable CodeReuse/ActiveRecord
+ (current_user.todos_pending_count.to_f / todos.limit_value).ceil
+ else
+ todos.total_pages
end
+ end
- out_of_range
+ def todo_params
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 76ed142c939..d4c6aae2ca8 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -1,8 +1,17 @@
# frozen_string_literal: true
class Explore::SnippetsController < Explore::ApplicationController
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
+
def index
- @snippets = SnippetsFinder.new(current_user).execute
- @snippets = @snippets.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user)
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 1d16ddb1608..958a24b6c0e 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -46,6 +46,15 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_personal_access_tokens_path
end
+ def reset_static_object_token
+ Users::UpdateService.new(current_user, user: @user).execute! do |user|
+ user.reset_static_object_token!
+ end
+
+ redirect_to profile_personal_access_tokens_path,
+ notice: s_('Profiles|Static object token was successfully reset')
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def audit_log
@events = AuditEvent.where(entity_type: "User", entity_id: current_user.id)
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ac1c4bc7fd3..1bb21857dcf 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -2,6 +2,7 @@
class Projects::ForksController < Projects::ApplicationController
include ContinueParams
+ include RendersMemberAccess
# Authorize
before_action :whitelist_query_limiting, only: [:create]
@@ -11,14 +12,16 @@ class Projects::ForksController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- base_query = project.forks.includes(:creator)
+ @total_forks_count = project.forks.size
+ @public_forks_count = project.forks.public_only.size
+ @private_forks_count = @total_forks_count - project.forks.public_and_internal_only.size
+ @internal_forks_count = @total_forks_count - @public_forks_count - @private_forks_count
- forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
- @total_forks_count = base_query.size
- @private_forks_count = @total_forks_count - forks.size
- @public_forks_count = @total_forks_count - @private_forks_count
+ @forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
+ @forks = @forks.includes(:route, :creator, :group, namespace: [:route, :owner])
+ .page(params[:page])
- @forks = forks.page(params[:page])
+ prepare_projects_for_rendering(@forks)
respond_to do |format|
format.html
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index a51759641e4..d69f9e65874 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -2,6 +2,9 @@
class Projects::RepositoriesController < Projects::ApplicationController
include ExtractsPath
+ include StaticObjectExternalStorage
+
+ prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }
# Authorize
before_action :require_non_empty_project, except: :create
@@ -9,6 +12,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
before_action :assign_append_sha, only: :archive
before_action :authorize_download_code!
before_action :authorize_admin_project!, only: :create
+ before_action :redirect_to_external_storage, only: :archive, if: :static_objects_external_storage_enabled?
def create
@project.create_repository
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 59f948959d6..dbd11c8ddc8 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -6,6 +6,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SpammableActions
include SnippetsActions
include RendersBlob
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
@@ -28,15 +30,14 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
- @snippets = SnippetsFinder.new(
- current_user,
- project: @project,
- scope: params[:scope]
- ).execute
- @snippets = @snippets.page(params[:page])
- if @snippets.out_of_range? && @snippets.total_pages != 0
- redirect_to project_snippets_path(@project, page: @snippets.total_pages)
- end
+ @snippets = SnippetsFinder.new(current_user, project: @project, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
def new
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 869655e9550..5805d068e21 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -7,6 +7,8 @@ class SnippetsController < ApplicationController
include SnippetsActions
include RendersBlob
include PreviewMarkdown
+ include PaginatedCollection
+ include Gitlab::NoteableMetadata
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
@@ -32,7 +34,13 @@ class SnippetsController < ApplicationController
@user = UserFinder.new(params[:username]).find_by_username!
@snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope])
- .execute.page(params[:page])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ return if redirect_out_of_range(@snippets)
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
render 'index'
else
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 91e0efcf45f..e38d4073de3 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -4,6 +4,7 @@ class UsersController < ApplicationController
include RoutableActions
include RendersMemberAccess
include ControllerWithCrossProjectAccessCheck
+ include Gitlab::NoteableMetadata
requires_cross_project_access show: false,
groups: false,
@@ -165,11 +166,12 @@ class UsersController < ApplicationController
end
def load_snippets
- @snippets = SnippetsFinder.new(
- current_user,
- author: user,
- scope: params[:scope]
- ).execute.page(params[:page])
+ @snippets = SnippetsFinder.new(current_user, author: user, scope: params[:scope])
+ .execute
+ .page(params[:page])
+ .inc_author
+
+ @noteable_meta_data = noteable_meta_data(@snippets, 'Snippet')
end
def build_canonical_path(user)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ffa5719fefb..1671aa5bd04 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -169,6 +169,25 @@ module ApplicationHelper
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
+ def static_objects_external_storage_enabled?
+ Gitlab::CurrentSettings.static_objects_external_storage_enabled?
+ end
+
+ def external_storage_url_or_path(path, project = @project)
+ return path unless static_objects_external_storage_enabled?
+
+ uri = URI(Gitlab::CurrentSettings.static_objects_external_storage_url)
+ path = URI(path) # `path` could have query parameters, so we need to split query and path apart
+
+ query = Rack::Utils.parse_nested_query(path.query)
+ query['token'] = current_user.static_object_token unless project.public?
+
+ uri.path = path.path
+ uri.query = query.to_query unless query.empty?
+
+ uri.to_s
+ end
+
def page_filter_path(options = {})
without = options.delete(:without)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b1a6e988a1d..93e282e44be 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -168,6 +168,8 @@ module ApplicationSettingsHelper
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
+ :static_objects_external_storage_auth_token,
+ :static_objects_external_storage_url,
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index e990e425cb6..09866ca75ff 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -103,7 +103,7 @@ module EventsHelper
words << "at"
end
- words << event.project_name
+ words << event.resource_parent_name
words.join(" ")
end
@@ -223,3 +223,5 @@ module EventsHelper
end
end
end
+
+EventsHelper.prepend_if_ee('EE::EventsHelper')
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
new file mode 100644
index 00000000000..4d9fe345edf
--- /dev/null
+++ b/app/helpers/releases_helper.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ReleasesHelper
+ IMAGE_PATH = 'illustrations/releases.svg'
+ DOCUMENTATION_PATH = 'user/project/releases/index'
+
+ def illustration
+ image_path(IMAGE_PATH)
+ end
+
+ def help_page
+ help_page_path(DOCUMENTATION_PATH)
+ end
+
+ def url_for_merge_requests
+ project_merge_requests_url(@project, params_for_issue_and_mr_paths)
+ end
+
+ def url_for_issues
+ project_issues_url(@project, params_for_issue_and_mr_paths)
+ end
+
+ def data_for_releases_page
+ {
+ project_id: @project.id,
+ illustration_path: illustration,
+ documentation_path: help_page,
+ merge_requests_url: url_for_merge_requests,
+ issues_url: url_for_issues
+ }
+ end
+
+ private
+
+ def params_for_issue_and_mr_paths
+ { scope: 'all', state: 'opened' }
+ end
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e39d655325f..c9cd0140ed8 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -8,6 +8,7 @@ class ApplicationSetting < ApplicationRecord
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
+ add_authentication_token_field :static_objects_external_storage_auth_token
belongs_to :instance_administration_project, class_name: "Project"
@@ -31,15 +32,6 @@ class ApplicationSetting < ApplicationRecord
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
- self.ignored_columns += %i[
- clientside_sentry_dsn
- clientside_sentry_enabled
- koding_enabled
- koding_url
- sentry_dsn
- sentry_enabled
- ]
-
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
@@ -211,6 +203,13 @@ class ApplicationSetting < ApplicationRecord
allow_blank: false,
if: :asset_proxy_enabled?
+ validates :static_objects_external_storage_url,
+ addressable_url: true, allow_blank: true
+
+ validates :static_objects_external_storage_auth_token,
+ presence: true,
+ if: :static_objects_external_storage_url?
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index f402c0e2775..8d9597aa5a4 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -306,6 +306,10 @@ module ApplicationSettingImplementation
archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
end
+ def static_objects_external_storage_enabled?
+ static_objects_external_storage_url.present?
+ end
+
private
def array_to_string(arr)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d2271c1335c..4aaabed6b7b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -835,12 +835,12 @@ module Ci
return unless merge_request_event?
strong_memoize(:merge_request_event_type) do
- if detached_merge_request_pipeline?
- :detached
+ if merge_train_pipeline?
+ :merge_train
elsif merge_request_pipeline?
:merged_result
- elsif merge_train_pipeline?
- :merge_train
+ elsif detached_merge_request_pipeline?
+ :detached
end
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 6a44bc7c401..b3e4df730b4 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -3,6 +3,10 @@
module Noteable
extend ActiveSupport::Concern
+ # This object is used to gather noteable meta data for list displays
+ # avoiding n+1 queries and improving performance.
+ NoteableMeta = Struct.new(:user_notes_count)
+
class_methods do
# `Noteable` class names that support replying to individual notes.
def replyable_types
diff --git a/app/models/event.rb b/app/models/event.rb
index 52d54be39a9..580bb770599 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -3,6 +3,8 @@
class Event < ApplicationRecord
include Sortable
include FromUnion
+ include Presentable
+
default_scope { reorder(nil) }
CREATED = 1
@@ -135,6 +137,10 @@ class Event < ApplicationRecord
end
end
+ def present
+ super(presenter_class: ::EventPresenter)
+ end
+
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def visible_to_user?(user = nil)
@@ -161,12 +167,8 @@ class Event < ApplicationRecord
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
- def project_name
- if project
- project.full_name
- else
- "(deleted project)"
- end
+ def resource_parent
+ project || group
end
def target_title
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 901ebcf249f..74f8067db0a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1239,7 +1239,7 @@ class MergeRequest < ApplicationRecord
end
def compare_reports(service_class, current_user = nil)
- with_reactive_cache(service_class.name) do |data|
+ with_reactive_cache(service_class.name, current_user&.id) do |data|
unless service_class.new(project, current_user)
.latest?(base_pipeline, actual_head_pipeline, data)
raise InvalidateReactiveCache
@@ -1249,12 +1249,13 @@ class MergeRequest < ApplicationRecord
end || { status: :parsing }
end
- def calculate_reactive_cache(identifier, *args)
+ def calculate_reactive_cache(identifier, current_user_id = nil, *args)
service_class = identifier.constantize
raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
- service_class.new(project).execute(base_pipeline, actual_head_pipeline)
+ current_user = User.find_by(id: current_user_id)
+ service_class.new(project, current_user).execute(base_pipeline, actual_head_pipeline)
end
def all_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index 5bd3a7f969a..62b3f47fadd 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -33,8 +33,6 @@ class Note < ApplicationRecord
end
end
- self.ignored_columns += %i[original_discussion_id]
-
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
redact_field :note
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 637c017a342..bf2aec74ec8 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class NotificationSetting < ApplicationRecord
- self.ignored_columns += %i[events]
-
enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 0aa920fa828..9789d8ed62b 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -6,6 +6,8 @@ class OauthAccessToken < Doorkeeper::AccessToken
alias_attribute :user, :resource_owner
+ scope :distinct_resource_owner_counts, ->(applications) { where(application: applications).distinct.group(:application_id).count(:resource_owner_id) }
+
def scopes=(value)
if value.is_a?(Array)
super(Doorkeeper::OAuth::Scopes.from_array(value).to_s)
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
new file mode 100644
index 00000000000..1b3183a2a43
--- /dev/null
+++ b/app/models/pages/lookup_path.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Pages
+ class LookupPath
+ def initialize(project, domain: nil)
+ @project = project
+ @domain = domain
+ end
+
+ def project_id
+ project.id
+ end
+
+ def access_control
+ project.private_pages?
+ end
+
+ def https_only
+ domain_https = domain ? domain.https? : true
+ project.pages_https_only? && domain_https
+ end
+
+ def source
+ {
+ type: 'file',
+ path: File.join(project.full_path, 'public/')
+ }
+ end
+
+ def prefix
+ '/'
+ end
+
+ private
+
+ attr_reader :project, :domain
+ end
+end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
new file mode 100644
index 00000000000..3a876dc06a2
--- /dev/null
+++ b/app/models/pages/virtual_domain.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Pages
+ class VirtualDomain
+ def initialize(projects, domain: nil)
+ @projects = projects
+ @domain = domain
+ end
+
+ def certificate
+ domain&.certificate
+ end
+
+ def key
+ domain&.key
+ end
+
+ def lookup_paths
+ projects.map do |project|
+ project.pages_lookup_path(domain: domain)
+ end.sort_by(&:prefix).reverse
+ end
+
+ private
+
+ attr_reader :projects, :domain
+ end
+end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index a2a471074a9..22a6bae7cf7 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -185,6 +185,10 @@ class PagesDomain < ApplicationRecord
self.certificate_source = 'gitlab_provided' if key_changed?
end
+ def pages_virtual_domain
+ Pages::VirtualDomain.new([project], domain: self)
+ end
+
private
def set_verification_code
diff --git a/app/models/project.rb b/app/models/project.rb
index d948410e397..12f5da05efa 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -61,11 +61,11 @@ class Project < ApplicationRecord
cache_markdown_field :description, pipeline: :description
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
- :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
- :merge_requests_access_level, :issues_access_level, :wiki_access_level,
- :snippets_access_level, :builds_access_level, :repository_access_level,
- to: :project_feature, allow_nil: true
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?,
+ :issues_enabled?, :pages_enabled?, :public_pages?, :private_pages?,
+ :merge_requests_access_level, :issues_access_level, :wiki_access_level,
+ :snippets_access_level, :builds_access_level, :repository_access_level,
+ to: :project_feature, allow_nil: true
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
@@ -2201,6 +2201,10 @@ class Project < ApplicationRecord
members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
+ def pages_lookup_path(domain: nil)
+ Pages::LookupPath.new(self, domain: domain)
+ end
+
private
def merge_requests_allowing_collaboration(source_branch = nil)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 78e82955342..efa3fbcf015 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -129,6 +129,10 @@ class ProjectFeature < ApplicationRecord
pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public?
end
+ def private_pages?
+ !public_pages?
+ end
+
private
# Validates builds and merge requests access level
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5cb4b56a114..e5a83366776 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1134,6 +1134,10 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self)
end
+ def redis_set_cache
+ @redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
+ end
+
def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 00931457344..b2fca65b9e0 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -55,6 +55,7 @@ class Snippet < ApplicationRecord
scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) }
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
scope :fresh, -> { order("created_at DESC") }
+ scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> { includes(author: :status) }
participant :author
diff --git a/app/models/user.rb b/app/models/user.rb
index 5f109feb96a..48acdfeb2ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,14 +23,9 @@ class User < ApplicationRecord
DEFAULT_NOTIFICATION_LEVEL = :participating
- self.ignored_columns += %i[
- authentication_token
- email_provider
- external_email
- ]
-
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
+ add_authentication_token_field :static_object_token
default_value_for :admin, false
default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
@@ -61,6 +56,9 @@ class User < ApplicationRecord
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
"administrator if you think this is an error."
+ # Removed in GitLab 12.3. Keep until after 2019-09-22.
+ self.ignored_columns += %i[support_bot]
+
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass
@@ -1437,6 +1435,13 @@ class User < ApplicationRecord
ensure_feed_token!
end
+ # Each existing user needs to have a `static_object_token`.
+ # We do this on read since migrating all existing users is not a feasible
+ # solution.
+ def static_object_token
+ ensure_static_object_token!
+ end
+
def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
new file mode 100644
index 00000000000..f31d362d5fa
--- /dev/null
+++ b/app/presenters/event_presenter.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class EventPresenter < Gitlab::View::Presenter::Delegated
+ presents :event
+
+ def resource_parent_name
+ resource_parent&.full_name || ''
+ end
+
+ def target_link_options
+ case resource_parent
+ when Group
+ [event.group, event.target]
+ when Project
+ [event.project.namespace.becomes(Namespace), event.project, event.target]
+ else
+ ''
+ end
+ end
+end
diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb
index 6c2d80d8f45..5b76e1824e4 100644
--- a/app/services/ci/compare_reports_base_service.rb
+++ b/app/services/ci/compare_reports_base_service.rb
@@ -41,7 +41,7 @@ module Ci
end
def serializer_params
- { project: project }
+ { project: project, current_user: current_user }
end
def get_report(pipeline)
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 9d4cf5df713..21055ad6617 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -6,7 +6,7 @@ module Ci
class RegisterJobService
attr_reader :runner
- JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300, 900].freeze
+ JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300, 900, 1800, 3600].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 308a3a10d1a..88ed0c3ef4c 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -33,7 +33,8 @@ module MergeRequests
merge_request.assign_attributes(params.to_h.compact)
merge_request.compare_commits = []
- merge_request.target_branch = find_target_branch
+ set_merge_request_target_branch
+
merge_request.can_be_created = projects_and_branches_valid?
# compare branches only if branches are valid, otherwise
@@ -93,8 +94,12 @@ module MergeRequests
project_from_params
end
- def find_target_branch
- target_branch || target_project.default_branch
+ def set_merge_request_target_branch
+ if source_branch_default? && !target_branch_specified?
+ merge_request.target_branch = nil
+ else
+ merge_request.target_branch ||= target_project.default_branch
+ end
end
def source_branch_specified?
@@ -149,7 +154,15 @@ module MergeRequests
end
def same_source_and_target?
- source_project == target_project && target_branch == source_branch
+ same_source_and_target_project? && target_branch == source_branch
+ end
+
+ def source_branch_default?
+ same_source_and_target_project? && source_branch == target_project.default_branch
+ end
+
+ def same_source_and_target_project?
+ source_project == target_project
end
def source_branch_exists?
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index f711839e389..18a90c952fa 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -18,7 +18,7 @@ module Search
end
def projects
- @projects ||= ProjectsFinder.new(current_user: current_user).execute
+ @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
end
def allowed_scopes
diff --git a/app/views/admin/application_settings/_repository_static_objects.html.haml b/app/views/admin/application_settings/_repository_static_objects.html.haml
new file mode 100644
index 00000000000..03aa48b2282
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_static_objects.html.haml
@@ -0,0 +1,18 @@
+= form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-static-objects-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :static_objects_external_storage_url, class: 'label-bold' do
+ = _('External storage URL')
+ = f.text_field :static_objects_external_storage_url, class: 'form-control'
+ %span.form-text.text-muted#static_objects_external_storage_url_help_block
+ = _('URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...).')
+ .form-group
+ = f.label :static_objects_external_storage_auth_token, class: 'label-bold' do
+ = _('External storage authentication token')
+ = f.text_field :static_objects_external_storage_auth_token, class: 'form-control'
+ %span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
+ = _('A secure token that identifies an external storage request.')
+
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index b50a0dd5a18..25f8b6541b5 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -34,3 +34,14 @@
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
= render 'repository_check'
+
+%section.settings.as-repository-static-objects.no-animate#js-repository-static-objects-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Repository static objects')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN).')
+ .settings-content
+ = render 'repository_static_objects'
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index 2cdf98075d1..758d722cc63 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -19,7 +19,8 @@
%tr{ :id => "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri
- %td= application.access_tokens.map(&:resource_owner_id).uniq.count
+ %td= @application_counts[application.id].to_i
%td= application.trusted? ? 'Y': 'N'
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
%td= render 'delete_form', application: application
+= paginate @applications, theme: 'gitlab'
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 5507f12b73b..a6acf948ed4 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,5 +1,5 @@
- more_info_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank'
+ anchor: 'add-existing-kubernetes-cluster'), target: '_blank'
- rbac_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
diff --git a/app/views/clusters/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
index 749177fa6c1..3b9ceaa2b8a 100644
--- a/app/views/clusters/clusters/user/_header.html.haml
+++ b/app/views/clusters/clusters/user/_header.html.haml
@@ -1,5 +1,5 @@
%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'adding-an-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'add-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index d56234e6c1a..406e8a93194 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -1,5 +1,7 @@
return unless event.visible_to_user?(current_user)
+event = event.present
+
xml.entry do
xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}"
xml.link href: event_feed_url(event)
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 222175c818a..647f0597adb 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,3 +1,5 @@
+- event = event.present
+
- if event.visible_to_user?(current_user)
.event-item
.event-item-timestamp
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 98941722434..67e4c538b4a 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -2,6 +2,5 @@
= event_preposition(event)
- if event.project
= link_to_project(event.project)
- - else
- = event.project_name
-
+ - elsif event.group
+ = link_to event.resource_parent_name, group_path(event.group)
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index b02fdb4b638..50c5885c648 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -8,7 +8,7 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
%span.event-target-type.append-right-4= event.target_type.titleize.downcase
- = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
+ = link_to event.target_link_options, class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
%span.event-target-title.append-right-4{ dir: "auto" }
@@ -17,4 +17,4 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event)
- = render "events/event_scope", event: event
+ = render "events/event_scope", event: event if event.resource_parent.present?
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 2f156603414..606b0febb57 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -10,4 +10,4 @@
- if event.project
= link_to_project(event.project)
- else
- = event.project_name
+ = event.resource_parent_name
diff --git a/app/views/graphiql/rails/editors/show.html.erb b/app/views/graphiql/rails/editors/show.html.erb
new file mode 100644
index 00000000000..abb1ed0e772
--- /dev/null
+++ b/app/views/graphiql/rails/editors/show.html.erb
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= GraphiQL::Rails.config.title || 'GraphiQL' %></title>
+
+ <%= stylesheet_link_tag("graphiql/rails/application") %>
+ <%= javascript_include_tag("graphiql/rails/application", nonce: true) %>
+ </head>
+ <body>
+ <%= content_tag :div, 'Loading...', id: 'graphiql-container', data: {
+ graphql_endpoint_path: graphql_endpoint_path,
+ initial_query: GraphiQL::Rails.config.initial_query,
+ logo: GraphiQL::Rails.config.logo,
+ headers: GraphiQL::Rails.config.resolve_headers(self),
+ query_params: GraphiQL::Rails.config.query_params
+ } %>
+ </body>
+</html>
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index d1eb6478997..64fec260f3b 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -25,7 +25,7 @@
.sub-section
%h4.warning-title Transfer group
- = form_for @group, url: transfer_group_path(@group), method: :put do |f|
+ = form_for @group, url: transfer_group_path(@group), method: :put, html: { class: 'js-group-transfer-form' } do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: 'Search groups', data: { data: parent_group_options(@group) } })
= hidden_field_tag 'new_parent_group_id'
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 08a39fc4f58..d9e94908b80 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -54,3 +54,23 @@
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
= reset_message.html_safe
+
+- if static_objects_external_storage_enabled?
+ %hr
+ .row.prepend-top-default
+ .col-lg-4
+ %h4.prepend-top-0
+ = s_('AccessTokens|Static object token')
+ %p
+ = s_('AccessTokens|Your static object token is used to authenticate you when repository static objects (e.g. archives, blobs, ...) are being served from an external storage.')
+ %p
+ = s_('AccessTokens|It cannot be used to access any other data.')
+ .col-lg-8
+ = label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
+ = text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ %p.form-text.text-muted
+ - reset_link = url_for [:reset, :static_object_token, :profile]
+ - reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
+ - reset_link_end = '</a>'.html_safe
+ - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can access repository static objects as if they were you. You should %{reset_link_start}reset it%{reset_link_end} if that ever happens.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
+ = reset_message.html_safe
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
index d344167a6c5..b256d94065b 100644
--- a/app/views/projects/buttons/_download_links.html.haml
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -2,4 +2,5 @@
.btn-group.ml-0.w-100
- formats.each do |(fmt, extra_class)|
- = link_to fmt, project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
+ - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
+ = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 68b35072f26..81c354f1c8f 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -5,4 +5,5 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
+ "project-id": @project.id,
} }
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 0397a7034c7..8384561891a 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -1,6 +1,6 @@
.top-area
.nav-text
- - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
+ - full_count_title = "#{@public_forks_count} public, #{@internal_forks_count} internal, and #{@private_forks_count} private"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index be01905dd35..c6615b26bc0 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -51,7 +51,7 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
+ = dropdown_toggle f.object.target_branch || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select target branch"))
= dropdown_filter(_("Search branches"))
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
index 326b83c856e..4d5b8cc80f7 100644
--- a/app/views/projects/releases/index.html.haml
+++ b/app/views/projects/releases/index.html.haml
@@ -1,3 +1,3 @@
- page_title _('Releases')
-#js-releases-page{ data: { project_id: @project.id, illustration_path: image_path('illustrations/releases.svg'), documentation_path: help_page_path('user/project/releases/index') } }
+#js-releases-page{ data: data_for_releases_page }
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 1dc538826dc..dfb0e7ed297 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,7 +1,7 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
-#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
+#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
= icon('spinner spin')
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index bb05658c719..d70a1631010 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -42,12 +42,6 @@
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests,
issues: issues, pipeline_status: pipeline_status, compact_mode: compact_mode
-
- - if @private_forks_count && @private_forks_count > 0
- %li.project-row.private-forks-notice
- = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
- %strong= pluralize(@private_forks_count, 'private fork')
- %span &nbsp;you have no access to.
= paginate_collection(projects, remote: remote) unless skip_pagination
- else
- if @contributed_projects
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index ebb634fe75f..1a9ae68f53d 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -17,7 +17,7 @@
= render "snippets/actions"
.snippet-header.limited-header-width
- %h2.snippet-title.prepend-top-0.append-bottom-0.qa-snippet-title
+ %h2.snippet-title.prepend-top-0.mb-3.qa-snippet-title
= markdown_field(@snippet, :title)
- if @snippet.description.present?
diff --git a/app/views/shared/snippets/_list.html.haml b/app/views/shared/snippets/_list.html.haml
index 5d2152eb411..766f48fff3d 100644
--- a/app/views/shared/snippets/_list.html.haml
+++ b/app/views/shared/snippets/_list.html.haml
@@ -1,12 +1,11 @@
- remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
-- if @snippets.exists?
+- if @snippets.to_a.empty?
+ .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
+- else
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
= paginate @snippets, theme: 'gitlab', remote: remote
-
-- else
- .nothing-here-block= s_("SnippetsEmptyState|No snippets found")
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 42af97bc6af..0ef626868a2 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,4 +1,5 @@
- link_project = local_assigns.fetch(:link_project, false)
+- notes_count = @noteable_meta_data[snippet.id].user_notes_count
%li.snippet-row
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
@@ -12,10 +13,9 @@
%ul.controls
%li
- - note_count = snippet.notes.user.count
- = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
+ = link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
= icon('comments')
- = note_count
+ = notes_count
%li
%span.sr-only
= visibility_level_label(snippet.visibility_level)
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 3191eaa1e2c..7516dfe1602 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -27,7 +27,7 @@
- if event.project
= link_to_project(event.project)
- else
- = event.project_name
+ = event.resource_parent_name
- else
made a private contribution
- else
diff --git a/changelogs/unreleased/34338-details-element.yml b/changelogs/unreleased/34338-details-element.yml
new file mode 100644
index 00000000000..95e50e1d4a9
--- /dev/null
+++ b/changelogs/unreleased/34338-details-element.yml
@@ -0,0 +1,5 @@
+---
+title: Add some padding to details markdown element
+merge_request: 32716
+author:
+type: fixed
diff --git a/changelogs/unreleased/61385-replace-vue-resource.yml b/changelogs/unreleased/61385-replace-vue-resource.yml
new file mode 100644
index 00000000000..787581b3f81
--- /dev/null
+++ b/changelogs/unreleased/61385-replace-vue-resource.yml
@@ -0,0 +1,5 @@
+---
+title: Replaced vue resource to axios in the Markdown field preview component
+merge_request: 32742
+author: Prakash Chokalingam @prakash_Chokalingam
+type: fixed
diff --git a/changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml b/changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml
new file mode 100644
index 00000000000..a1f99833ce0
--- /dev/null
+++ b/changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Mention in docs how to disable project snippets
+merge_request: 32391
+author: Jacopo Beschi @jacopo-beschi
+type: other
diff --git a/changelogs/unreleased/64799-disable-transfer-group.yml b/changelogs/unreleased/64799-disable-transfer-group.yml
new file mode 100644
index 00000000000..68d4c5bfc06
--- /dev/null
+++ b/changelogs/unreleased/64799-disable-transfer-group.yml
@@ -0,0 +1,5 @@
+---
+title: Disable "Transfer group" button when no group is selected
+merge_request: 31387
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/65940-run-pipeline.yml b/changelogs/unreleased/65940-run-pipeline.yml
new file mode 100644
index 00000000000..c0e89a19373
--- /dev/null
+++ b/changelogs/unreleased/65940-run-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Run Pipeline button & API for MR Pipelines
+merge_request: 31722
+author:
+type: added
diff --git a/changelogs/unreleased/65988-optimize-snippet-listings.yml b/changelogs/unreleased/65988-optimize-snippet-listings.yml
new file mode 100644
index 00000000000..186a83cf9f3
--- /dev/null
+++ b/changelogs/unreleased/65988-optimize-snippet-listings.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize queries for snippet listings
+merge_request: 32576
+author:
+type: performance
diff --git a/changelogs/unreleased/66637-use-chronic-duration-in-thread-safe-way.yml b/changelogs/unreleased/66637-use-chronic-duration-in-thread-safe-way.yml
new file mode 100644
index 00000000000..549f523076e
--- /dev/null
+++ b/changelogs/unreleased/66637-use-chronic-duration-in-thread-safe-way.yml
@@ -0,0 +1,5 @@
+---
+title: Use `ChronicDuration` in a thread-safe way
+merge_request: 32817
+author:
+type: fixed
diff --git a/changelogs/unreleased/67248-snippet-title-whitespace.yml b/changelogs/unreleased/67248-snippet-title-whitespace.yml
new file mode 100644
index 00000000000..b6460faac4d
--- /dev/null
+++ b/changelogs/unreleased/67248-snippet-title-whitespace.yml
@@ -0,0 +1,5 @@
+---
+title: Add bottom margin to snippet title
+merge_request: 32877
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-source-and-merge_request-to-pipeline-webhook.yml b/changelogs/unreleased/add-source-and-merge_request-to-pipeline-webhook.yml
new file mode 100644
index 00000000000..e7300335427
--- /dev/null
+++ b/changelogs/unreleased/add-source-and-merge_request-to-pipeline-webhook.yml
@@ -0,0 +1,5 @@
+---
+title: Add source and merge_request fields to pipeline event webhook
+merge_request: 32373
+author: Bian Jiaping
+type: added
diff --git a/changelogs/unreleased/ce-indicator-for-pipeline-for-merge-train.yml b/changelogs/unreleased/ce-indicator-for-pipeline-for-merge-train.yml
new file mode 100644
index 00000000000..06d2f4b56f5
--- /dev/null
+++ b/changelogs/unreleased/ce-indicator-for-pipeline-for-merge-train.yml
@@ -0,0 +1,5 @@
+---
+title: Make MR pipeline widget text more descriptive
+merge_request: 32025
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-62807-not-prefill-target-branch.yml b/changelogs/unreleased/fj-62807-not-prefill-target-branch.yml
new file mode 100644
index 00000000000..f19634d80b2
--- /dev/null
+++ b/changelogs/unreleased/fj-62807-not-prefill-target-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid prefilling target branch when source branch is the default one
+merge_request: 32701
+author:
+type: other
diff --git a/changelogs/unreleased/issue-64738.yml b/changelogs/unreleased/issue-64738.yml
new file mode 100644
index 00000000000..0a65ebc750b
--- /dev/null
+++ b/changelogs/unreleased/issue-64738.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent archived projects from showing up in global search
+merge_request: 31498
+author: David Palubin
+type: fixed
diff --git a/changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml b/changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml
new file mode 100644
index 00000000000..f86e0a4259f
--- /dev/null
+++ b/changelogs/unreleased/remove-vue-resource-from-sidebar-service.yml
@@ -0,0 +1,5 @@
+---
+title: Remove vue resource from sidebar service
+merge_request: 32400
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/security-12-3-bump-pages.yml b/changelogs/unreleased/security-12-3-bump-pages.yml
new file mode 100644
index 00000000000..cf602e3cf48
--- /dev/null
+++ b/changelogs/unreleased/security-12-3-bump-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade pages to 1.8.1
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-fix-oauth-application-page.yml b/changelogs/unreleased/sh-fix-oauth-application-page.yml
new file mode 100644
index 00000000000..9ac78db2c79
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-oauth-application-page.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize /admin/applications so that it does not timeout
+merge_request: 32852
+author:
+type: performance
diff --git a/changelogs/unreleased/static-objects-external-storage.yml b/changelogs/unreleased/static-objects-external-storage.yml
new file mode 100644
index 00000000000..fd687b2262c
--- /dev/null
+++ b/changelogs/unreleased/static-objects-external-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Enable serving static objects from an external storage
+merge_request: 31025
+author:
+type: added
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 33c90989548..5346bf45473 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -613,3 +613,10 @@
:why: https://bitbucket.org/atlassian/atlassian-jwt-ruby/src/master/LICENSE.txt
:versions: []
:when: 2019-08-30 05:45:35.317663000 Z
+- - :license
+ - unicode_utils
+ - MIT
+ - :who: Aishwarya Subramanain
+ :why: https://github.com/hexorx/countries/blob/master/LICENSE
+ :versions: []
+ :when: 2019-09-11 13:08:28.431132000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 87159b695f9..92674aafa90 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -718,6 +718,10 @@ production: &base
# Browser session with smartcard sign-in is required for Git access
# required_for_git_access: false
+ # Use X.509 SAN extensions certificates to identify GitLab users
+ # Add a subjectAltName to your certificates like: email:user
+ # san_extensions: true
+
## Kerberos settings
kerberos:
# Allow the HTTP Negotiate authentication method for Git clients
diff --git a/config/initializers/chronic_duration.rb b/config/initializers/chronic_duration.rb
index aa43eef434c..5528df6c660 100644
--- a/config/initializers/chronic_duration.rb
+++ b/config/initializers/chronic_duration.rb
@@ -1,5 +1,3 @@
# frozen_string_literal: true
ChronicDuration.raise_exceptions = true
-
-ChronicDuration.prepend Gitlab::Patch::ChronicDuration
diff --git a/config/initializers/countries.rb b/config/initializers/countries.rb
new file mode 100644
index 00000000000..d65ae19852f
--- /dev/null
+++ b/config/initializers/countries.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+ISO3166.configure do |config|
+ config.locales = [:en]
+end
+
+# GitLab permits users to sign up in Ukraine except the Crimean Region: https://about.gitlab.com/handbook/people-operations/code-of-conduct/#trade-compliance-exportimport-control
+# This overrides the display name for Ukraine to Ukraine (except Crimean Region)
+# To be removed after https://gitlab.com/gitlab-org/gitlab-ee/issues/14784 is implemented
+# Data fetched is based on https://github.com/hexorx/countries/blob/master/lib/countries/data/countries/UA.yaml
+ISO3166::Data.register(
+ continent: "Europe",
+ address_format: "|-
+ {{recipient}}
+ {{street}}
+ {{city}} {{region_short}}
+ {{postalcode}}
+ {{country}}",
+ alpha2: "UA",
+ alpha3: "UKR",
+ country_code: '380',
+ international_prefix: '810',
+ ioc: "UKR",
+ gec: "UP",
+ name: "Ukraine (except Crimean Region)",
+ national_destination_code_lengths: [2],
+ national_number_lengths: [8, 9],
+ national_prefix: '8',
+ number: '804',
+ region: "Europe",
+ subregion: "Eastern Europe",
+ world_region: "EMEA",
+ un_locode: "UA",
+ nationality: "Ukrainian",
+ vat_rates: {
+ standard: 20
+ },
+ reduced: [7],
+ super_reduced: {
+ parking: { postal_code: true }
+ },
+ unofficial_names: %w(Ukraine Ucrania ウクライナ Oekraïne Украина Україна Украіна),
+ languages_official: ["uk"],
+ languages_spoken: ["uk"],
+ geo: {
+ latitude: 48.379433,
+ latitude_dec: '48.92656326293945',
+ longitude: 31.16558,
+ longitude_dec: '31.47578239440918',
+ max_latitude: 52.37958099999999,
+ max_longitude: 40.2285809,
+ min_latitude: 44.2924,
+ min_longitude: 22.137159,
+ bounds: {
+ northeast: { lat: 52.37958099999999, lng: 40.2285809 },
+ southwest: { lat: 44.2924, lng: 22.137159 }
+ }
+ },
+ currency_code: "UAH",
+ start_of_week: "monday"
+)
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 9f3e104bc2b..20f31ff6810 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -60,7 +60,7 @@ Sidekiq.configure_server do |config|
# Sidekiq (e.g. in an initializer).
ActiveRecord::Base.clear_all_connections!
- Gitlab::SidekiqMonitor.instance.start if enable_sidekiq_monitor
+ Gitlab::SidekiqDaemon::Monitor.instance.start if enable_sidekiq_monitor
end
if enable_reliable_fetch?
diff --git a/config/routes.rb b/config/routes.rb
index a622ce268da..7d091576e8f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -112,6 +112,7 @@ Rails.application.routes.draw do
draw :jira_connect
draw :username
draw :trial_registration
+ draw :country
end
Gitlab.ee do
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 83a2b33514b..403f430850e 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -8,6 +8,7 @@ resource :profile, only: [:show, :update] do
put :reset_incoming_email_token
put :reset_feed_token
+ put :reset_static_object_token
put :update_username
end
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index b89e1c7f9af..093b86f3259 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -65,7 +65,7 @@ scope format: false do
resources :protected_tags, only: [:index, :show, :create, :update, :destroy]
end
- scope constraints: { id: /.+/ } do
+ scope constraints: { id: /[^\0]+/ } do
scope controller: :blob do
get '/new/*id', action: :new, as: :new_blob
post '/create/*id', action: :create, as: :create_blob
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 969a84e85dd..f3f0a5f8934 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -17,6 +17,7 @@ const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
+const WEBPACK_MEMORY_TEST = process.env.WEBPACK_MEMORY_TEST;
const NO_COMPRESSION = process.env.NO_COMPRESSION;
const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
@@ -337,6 +338,30 @@ module.exports = {
},
},
+ // output the in-memory heap size upon compilation and exit
+ WEBPACK_MEMORY_TEST && {
+ apply(compiler) {
+ compiler.hooks.emit.tapAsync('ReportMemoryConsumptionPlugin', (compilation, callback) => {
+ console.log('Assets compiled...');
+ if (global.gc) {
+ console.log('Running garbage collection...');
+ global.gc();
+ } else {
+ console.error(
+ "WARNING: you must use the --expose-gc node option to accurately measure webpack's heap size",
+ );
+ }
+ const memoryUsage = process.memoryUsage().heapUsed;
+ const toMB = bytes => Math.floor(bytes / 1024 / 1024);
+
+ console.log(`Webpack heap size: ${toMB(memoryUsage)} MB`);
+
+ // exit in case we're running webpack-dev-server
+ IS_DEV_SERVER && process.exit();
+ });
+ },
+ },
+
// enable HMR only in webpack-dev-server
DEV_SERVER_LIVERELOAD && new webpack.HotModuleReplacementPlugin(),
diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile
index 0c675cc4c9c..d371ade4887 100644
--- a/danger/commit_messages/Dangerfile
+++ b/danger/commit_messages/Dangerfile
@@ -88,9 +88,36 @@ def lint_commit(commit) # rubocop:disable Metrics/AbcSize
# We ignore revert commits as they are well structured by Git already
return false if commit.message.start_with?('Revert "')
+ is_squash = gitlab.mr_json['squash']
+ is_wip = gitlab.mr_json['work_in_progress']
+ is_fixup = commit.message.start_with?('fixup!', 'squash!')
+
+ if is_fixup
+ # The MR is set to squash - Danger adds an informative notice
+ # The MR is not set to squash - Danger fails. if also WIP warn only, not error
+ if is_squash
+ return false
+ end
+
+ if is_wip
+ warn_commit(
+ commit,
+ 'Squash or Fixup commits must be squashed before merge, or enable squash merge option'
+ )
+ else
+ fail_commit(
+ commit,
+ 'Squash or Fixup commits must be squashed before merge, or enable squash merge option'
+ )
+ end
+
+ # Makes no sense to process other rules for fixup commits, they trigger just more noise
+ return false
+ end
+
# Fail if a suggestion commit is used and squash is not enabled
if commit.message.start_with?('Apply suggestion to')
- if gitlab.mr_json['squash']
+ if is_squash
return false
else
fail_commit(
diff --git a/db/migrate/20190722104947_add_static_object_token_to_users.rb b/db/migrate/20190722104947_add_static_object_token_to_users.rb
new file mode 100644
index 00000000000..6ef85d9acaa
--- /dev/null
+++ b/db/migrate/20190722104947_add_static_object_token_to_users.rb
@@ -0,0 +1,18 @@
+# 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 AddStaticObjectTokenToUsers < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :users, :static_object_token, :string, limit: 255
+ end
+
+ def down
+ remove_column :users, :static_object_token
+ end
+end
diff --git a/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb b/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb
new file mode 100644
index 00000000000..a23e6ed66cd
--- /dev/null
+++ b/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb
@@ -0,0 +1,14 @@
+# 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 AddStaticObjectsExternalStorageColumnsToApplicationSettings < ActiveRecord::Migration[5.2]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :static_objects_external_storage_url, :string, limit: 255
+ add_column :application_settings, :static_objects_external_storage_auth_token, :string, limit: 255
+ end
+end
diff --git a/db/migrate/20190725183432_add_index_to_index_on_static_object_token.rb b/db/migrate/20190725183432_add_index_to_index_on_static_object_token.rb
new file mode 100644
index 00000000000..423c45b9543
--- /dev/null
+++ b/db/migrate/20190725183432_add_index_to_index_on_static_object_token.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 AddIndexToIndexOnStaticObjectToken < 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_concurrent_index :users, :static_object_token, unique: true
+ end
+
+ def down
+ remove_concurrent_index :users, :static_object_token
+ end
+end
diff --git a/db/migrate/20190826100605_add_group_column_to_events.rb b/db/migrate/20190826100605_add_group_column_to_events.rb
index cd7b2b1d96a..dfc9d8cbdf1 100644
--- a/db/migrate/20190826100605_add_group_column_to_events.rb
+++ b/db/migrate/20190826100605_add_group_column_to_events.rb
@@ -1,9 +1,19 @@
# frozen_string_literal: true
class AddGroupColumnToEvents < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
DOWNTIME = false
- def change
- add_reference :events, :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }
+ disable_ddl_transaction!
+
+ def up
+ add_column(:events, :group_id, :bigint) unless column_exists?(:events, :group_id)
+ add_concurrent_index(:events, :group_id)
+ add_concurrent_foreign_key(:events, :namespaces, column: :group_id, on_delete: :cascade)
+ end
+
+ def down
+ remove_column(:events, :group_id) if column_exists?(:events, :group_id)
end
end
diff --git a/db/post_migrate/20190715193142_migrate_discussion_id_on_promoted_epics.rb b/db/post_migrate/20190715193142_migrate_discussion_id_on_promoted_epics.rb
new file mode 100644
index 00000000000..13f8477f3ea
--- /dev/null
+++ b/db/post_migrate/20190715193142_migrate_discussion_id_on_promoted_epics.rb
@@ -0,0 +1,62 @@
+# 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 MigrateDiscussionIdOnPromotedEpics < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # We have ~5000 unique discussion_ids -> this migration will take about 102 minutes
+ # (5000/100 * 2 minutes + 2 minutes initial delay) on gitlab.com.
+ DOWNTIME = false
+ BATCH_SIZE = 100
+ DELAY_INTERVAL = 2.minutes
+ MIGRATION = 'FixPromotedEpicsDiscussionIds'
+
+ disable_ddl_transaction!
+
+ class SystemNoteMetadata < ActiveRecord::Base
+ self.table_name = 'system_note_metadata'
+ self.inheritance_column = :_type_disabled
+ end
+
+ class Note < ActiveRecord::Base
+ include EachBatch
+
+ has_one :system_note_metadata, class_name: 'MigrateDiscussionIdOnPromotedEpics::SystemNoteMetadata'
+
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+
+ def self.fetch_discussion_ids_query
+ promoted_epics_query = Note
+ .joins(:system_note_metadata)
+ .where(system: true)
+ .where(noteable_type: 'Epic')
+ .where(system_note_metadata: { action: 'moved' })
+ .select("DISTINCT noteable_id")
+
+ Note.where(noteable_type: 'Epic')
+ .where(noteable_id: promoted_epics_query)
+ .distinct.pluck(:discussion_id)
+ end
+ end
+
+ def up
+ add_concurrent_index(:system_note_metadata, :note_id, where: "action='moved'", name: 'temp_index_system_note_metadata_on_moved_note_id')
+ add_concurrent_index(:notes, [:id, :noteable_id], where: "noteable_type='Epic' AND system", name: 'temp_index_notes_on_id_and_noteable_id' )
+
+ all_discussion_ids = Note.fetch_discussion_ids_query
+ all_discussion_ids.in_groups_of(BATCH_SIZE, false).each_with_index do |ids, index|
+ delay = DELAY_INTERVAL * (index + 1)
+ BackgroundMigrationWorker.perform_in(delay, MIGRATION, [ids])
+ end
+
+ remove_concurrent_index(:system_note_metadata, :note_id, where: "action='moved'", name: 'temp_index_system_note_metadata_on_moved_note_id')
+ remove_concurrent_index(:notes, [:id, :noteable_id], where: "noteable_type='Epic' AND system", name: 'temp_index_notes_on_id_and_noteable_id')
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb b/db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb
new file mode 100644
index 00000000000..78f7c0ecf0f
--- /dev/null
+++ b/db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnApplicationIdOnOauthAccessTokens < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :oauth_access_tokens, :application_id
+ end
+
+ def down
+ remove_concurrent_index :oauth_access_tokens, :application_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6ddfb8bcb39..3906976d296 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_09_05_223900) do
+ActiveRecord::Schema.define(version: 2019_09_10_000130) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -284,6 +284,8 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.text "asset_proxy_whitelist"
t.text "encrypted_asset_proxy_secret_key"
t.string "encrypted_asset_proxy_secret_key_iv"
+ t.string "static_objects_external_storage_url", limit: 255
+ t.string "static_objects_external_storage_auth_token", limit: 255
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
@@ -2390,6 +2392,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.datetime "revoked_at"
t.datetime "created_at", null: false
t.string "scopes"
+ t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id"
t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
@@ -3565,6 +3568,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.integer "bot_type", limit: 2
t.string "first_name", limit: 255
t.string "last_name", limit: 255
+ t.string "static_object_token", limit: 255
t.index ["accepted_term_id"], name: "index_users_on_accepted_term_id"
t.index ["admin"], name: "index_users_on_admin"
t.index ["bot_type"], name: "index_users_on_bot_type"
@@ -3584,6 +3588,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.index ["state"], name: "index_users_on_state"
t.index ["state"], name: "index_users_on_state_and_internal", where: "(ghost IS NOT TRUE)"
t.index ["state"], name: "index_users_on_state_and_internal_ee", where: "((ghost IS NOT TRUE) AND (bot_type IS NULL))"
+ t.index ["static_object_token"], name: "index_users_on_static_object_token", unique: true
t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)"
t.index ["username"], name: "index_users_on_username"
t.index ["username"], name: "index_users_on_username_trigram", opclass: :gin_trgm_ops, using: :gin
@@ -3864,7 +3869,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
add_foreign_key "epics", "users", column: "assignee_id", name: "fk_dccd3f98fc", on_delete: :nullify
add_foreign_key "epics", "users", column: "author_id", name: "fk_3654b61b03", on_delete: :cascade
add_foreign_key "epics", "users", column: "closed_by_id", name: "fk_aa5798e761", on_delete: :nullify
- add_foreign_key "events", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "events", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "external_pull_requests", "projects", on_delete: :cascade
diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md
index 4f236d1afb8..920a2f0b399 100644
--- a/doc/administration/auth/smartcard.md
+++ b/doc/administration/auth/smartcard.md
@@ -39,6 +39,45 @@ Certificate:
Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com
```
+### Authentication against a local database with X.509 certificates and SAN extensions **(PREMIUM ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8605) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+
+Smartcards with X.509 certificates using SAN extensions can be used to authenticate
+with GitLab.
+
+NOTE: **Note:**
+This is an experimental feature. Smartcard authentication against local databases may
+change or be removed completely in future releases.
+
+To use a smartcard with an X.509 certificate to authenticate against a local
+database with GitLab, at least one of the `subjectAltName` (SAN) extensions
+need to define the user identity (`email`) within the GitLab instance (`URI`).
+
+`URI`: needs to match `Gitlab.config.host.gitlab`.
+
+For example:
+
+```text
+Certificate:
+ Data:
+ Version: 1 (0x0)
+ Serial Number: 12856475246677808609 (0xb26b601ecdd555e1)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: O=Random Corp Ltd, CN=Random Corp
+ Validity
+ Not Before: Oct 30 12:00:00 2018 GMT
+ Not After : Oct 30 12:00:00 2019 GMT
+ ...
+ X509v3 extensions:
+ X509v3 Key Usage:
+ Key Encipherment, Data Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication
+ X509v3 Subject Alternative Name:
+ email:gitlab-user@example.com, URI:http://gitlab.example.com/
+```
+
### Authentication against an LDAP server
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7693) in
@@ -152,6 +191,36 @@ attribute. As a prerequisite, you must use an LDAP server that:
1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect.
+### Additional steps when using SAN extensions
+
+**For Omnibus installations**
+
+1. Add to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['smartcard_san_extensions'] = true
+ ```
+
+1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure)
+ GitLab for the changes to take effect.
+
+**For installations from source**
+
+1. Add the `san_extensions` line to config/gitlab.yml` within the smartcard section:
+
+ ```yaml
+ smartcard:
+ enabled: true
+ ca_file: '/etc/ssl/certs/CA.pem'
+ client_certificate_required_port: 3444
+
+ # Enable the use of SAN extensions to match users with certificates
+ san_extensions: true
+ ```
+
+1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
+ GitLab for the changes to take effect.
+
### Additional steps when authenticating against an LDAP server
**For Omnibus installations**
diff --git a/doc/administration/geo/replication/docker_registry.md b/doc/administration/geo/replication/docker_registry.md
index d5c2d2362b1..b326717205e 100644
--- a/doc/administration/geo/replication/docker_registry.md
+++ b/doc/administration/geo/replication/docker_registry.md
@@ -1,23 +1,113 @@
# Docker Registry for a secondary node **(PREMIUM ONLY)**
-You can set up a [Docker Registry] on your
+You can set up a [Docker Registry](https://docs.docker.com/registry/) on your
**secondary** Geo node that mirrors the one on the **primary** Geo node.
## Storage support
-CAUTION: **Warning:**
-If you use [local storage][registry-storage]
-for the Container Registry you **cannot** replicate it to a **secondary** node.
-
Docker Registry currently supports a few types of storages. If you choose a
distributed storage (`azure`, `gcs`, `s3`, `swift`, or `oss`) for your Docker
Registry on the **primary** node, you can use the same storage for a **secondary**
Docker Registry as well. For more information, read the
-[Load balancing considerations][registry-load-balancing]
+[Load balancing considerations](https://docs.docker.com/registry/deploying/#load-balancing-considerations)
when deploying the Registry, and how to set up the storage driver for GitLab's
-integrated [Container Registry][registry-storage].
+integrated [Container Registry](../../container_registry.md#container-registry-storage-driver).
+
+## Replicating Docker Registry
+
+You can enable a storage-agnostic replication so it
+can be used for cloud or local storages. Whenever a new image is pushed to the
+primary node, each **secondary** node will pull it to its own container
+repository.
+
+To configure Docker Registry replication:
+
+1. Configure the [**primary** node](#configure-primary-node).
+1. Configure the [**secondary** node](#configure-secondary-node).
+1. Verify Docker Registry [replication](#verify-replication).
+
+### Configure **primary** node
+
+Make sure that you have Container Registry set up and working on
+the **primary** node before following the next steps.
+
+We need to make Docker Registry send notification events to the
+**primary** node.
+
+1. SSH into your GitLab **primary** server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['notifications'] = [
+ {
+ 'name' => 'geo_event',
+ 'url' => 'https://example.com/api/v4/container_registry_event/events',
+ 'timeout' => '500ms',
+ 'threshold' => 5,
+ 'backoff' => '1s',
+ 'headers' => {
+ 'Authorization' => ['<replace_with_a_secret_token>']
+ }
+ }
+ ]
+ ```
+
+ NOTE: **Note:**
+ If you use an external Registry (not the one integrated with GitLab), you must add
+ these settings to its configuration. In this case, you will also have to specify
+ notification secret in `registry.notification_secret` section of
+ `/etc/gitlab/gitlab.rb` file.
+
+1. Reconfigure the **primary** node for the change to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+### Configure **secondary** node
+
+Make sure you have Container Registry set up and working on
+the **secondary** node before following the next steps.
+
+The following steps should be done on each **secondary** node you're
+expecting to see the Docker images replicated.
+
+Because we need to allow the **secondary** node to communicate securely with
+the **primary** node Container Registry, we need to have a single key
+pair for all the nodes. The **secondary** node will use this key to
+generate a short-lived JWT that is pull-only-capable to access the
+**primary** node Container Registry.
+
+1. SSH into the **secondary** node and login as the `root` user:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Copy `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` from the **primary** to the **secondary** node.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['registry_replication'] = {
+ enabled: true,
+ primary_api_url: 'http://primary.example.com:5000/' # internal address to the primary registry, will be used by GitLab to directly communicate with primary registry API
+ }
+ ```
+
+1. Reconfigure the **secondary** node for the change to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+### Verify replication
-[ee]: https://about.gitlab.com/pricing/
-[Docker Registry]: https://docs.docker.com/registry/
-[registry-storage]: ../../container_registry.md#container-registry-storage-driver
-[registry-load-balancing]: https://docs.docker.com/registry/deploying/#load-balancing-considerations
+To verify Container Registry replication is working, go to **Admin Area > Geo** (`/admin/geo/nodes`) on the **secondary** node.
+The initial replication, or "backfill", will probably still be in progress.
+You can monitor the synchronization process on each Geo node from the **primary** node's **Geo Nodes** dashboard in your browser.
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index dbd466b906d..1d50c924f6e 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -274,7 +274,7 @@ You can keep track of the progress to include the missing items in:
| [Server-side Git Hooks](../../custom_hooks.md) | No | No |
| [Elasticsearch integration](../../../integration/elasticsearch.md) | No | No |
| [GitLab Pages](../../pages/index.md) | No | No |
-| [Container Registry](../../container_registry.md) ([track progress](https://gitlab.com/gitlab-org/gitlab-ee/issues/2870)) | No | No |
+| [Container Registry](../../container_registry.md) | Yes | No |
| [NPM Registry](../../npm_registry.md) | No | No |
| [Maven Packages](../../maven_packages.md) | No | No |
| [External merge request diffs](../../merge_request_diffs.md) | No, if they are on-disk | No |
diff --git a/doc/administration/index.md b/doc/administration/index.md
index d557068e6c8..df3501ae950 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -104,6 +104,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
## User settings and permissions
+- [Creating users](../user/profile/account/create_accounts.md): Create users manually or through authentication integrations.
- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
@@ -142,6 +143,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Repository storage types](repository_storage_types.md): Information about the different repository storage types.
- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage.
- [Limit repository size](../user/admin_area/settings/account_and_limit_settings.md): Set a hard limit for your repositories' size. **(STARTER ONLY)**
+- [Static objects external storage](static_objects_external_storage.md): Set external storage for static objects in a repository.
## Continuous Integration settings
diff --git a/doc/administration/smime_signing_email.md b/doc/administration/smime_signing_email.md
index b2e3bf8487b..530553ec1c4 100644
--- a/doc/administration/smime_signing_email.md
+++ b/doc/administration/smime_signing_email.md
@@ -11,29 +11,56 @@ S/MIME signs and/or encrypts the message itself
## Enable S/MIME signing
This setting must be explicitly enabled and a single pair of key and certificate
-files must be provided in `gitlab.rb` or `gitlab.yml` if you are using Omnibus
-GitLab or installed GitLab from source respectively:
-
-```yaml
-email_smime:
- enabled: true
- key_file: /etc/pki/smime/private/gitlab.key
- cert_file: /etc/pki/smime/certs/gitlab.crt
-```
+files must be provided:
-- Both files must be provided PEM-encoded.
-- The key file must be unencrypted so that Gitlab can read it without user
+- Both files must be PEM-encoded.
+- The key file must be unencrypted so that GitLab can read it without user
intervention.
+- Only RSA keys are supported.
NOTE: **Note:** Be mindful of the access levels for your private keys and visibility to
third parties.
+**For Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and adapt the file paths:
+
+ ```ruby
+ gitlab_rails['gitlab_email_smime_enabled'] = true
+ gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
+ gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
+ ```
+
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+NOTE: **Note:** The key needs to be readable by the GitLab system user (`git` by default).
+
+**For installations from source:**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ email_smime:
+ # Uncomment and set to true if you need to enable email S/MIME signing (default: false)
+ enabled: true
+ # S/MIME private key file in PEM format, unencrypted
+ # Default is '.gitlab_smime_key' relative to Rails.root (i.e. root of the GitLab app).
+ key_file: /etc/pki/smime/private/gitlab.key
+ # S/MIME public certificate key in PEM format, will be attached to signed messages
+ # Default is '.gitlab_smime_cert' relative to Rails.root (i.e. root of the GitLab app).
+ cert_file: /etc/pki/smime/certs/gitlab.crt
+ ```
+
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
+
+NOTE: **Note:** The key needs to be readable by the GitLab system user (`git` by default).
+
### How to convert S/MIME PKCS#12 / PFX format to PEM encoding
Typically S/MIME certificates are handled in binary PKCS#12 format (`.pfx` or `.p12`
extensions), which contain the following in a single encrypted file:
-- Server certificate
+- Public certificate
- Intermediate certificates (if any)
- Private key
diff --git a/doc/administration/static_objects_external_storage.md b/doc/administration/static_objects_external_storage.md
new file mode 100644
index 00000000000..e4d60c77199
--- /dev/null
+++ b/doc/administration/static_objects_external_storage.md
@@ -0,0 +1,50 @@
+# Static objects external storage
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31025) in GitLab 12.3.
+
+GitLab can be configured to serve repository static objects (for example, archives) from an external
+storage, such as a CDN.
+
+## Configuring
+
+To configure external storage for static objects:
+
+1. Navigate to **Admin Area > Settings > Repository**.
+1. Expand the **Repository static objects** section.
+1. Enter the base URL and an arbitrary token.
+
+The token is required to distinguish requests coming from the external storage, so users don't
+circumvent the external storage and go for the application directly. The token is expected to be
+set in the `X-Gitlab-External-Storage-Token` header in requests originating from the external
+storage.
+
+## Serving private static objects
+
+GitLab will append a user-specific token for static object URLs that belong to private projects,
+so an external storage can be authenticated on behalf of the user. When processing requests originating
+from the external storage, GitLab will look for the token in the `token` query parameter or in
+the `X-Gitlab-Static-Object-Token` header to check the user's ability to access the requested object.
+
+## Requests flow example
+
+The following example shows a sequence of requests and responses between the user,
+GitLab, and the CDN:
+
+```mermaid
+sequenceDiagram
+ User->>GitLab: GET /project/-/archive/master.zip
+ GitLab->>User: 302 Found
+ Note over User,GitLab: Location: https://cdn.com/project/-/archive/master.zip?token=secure-user-token
+ User->>CDN: GET /project/-/archive/master.zip?token=secure-user-token
+ alt object not in cache
+ CDN->>GitLab: GET /project/-/archive/master.zip
+ Note over CDN,GitLab: X-Gitlab-External-Storage-Token: secure-cdn-token<br/>X-Gitlab-Static-Object-Token: secure-user-token
+ GitLab->>CDN: 200 OK
+ CDN->>User: master.zip
+ else object in cache
+ CDN->>GitLab: GET /project/-/archive/master.zip
+ Note over CDN,GitLab: X-Gitlab-External-Storage-Token: secure-cdn-token<br/>X-Gitlab-Static-Object-Token: secure-user-token<br/>If-None-Match: etag-value
+ GitLab->>CDN: 304 Not Modified
+ CDN->>User: master.zip
+ end
+```
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 0c5611aa6cd..19c564c7616 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -64,7 +64,7 @@ Array.methods.grep(/sing/)
## Find method source
-Works for [non-instrumented methods](https://docs.gitlab.com/ce/development/instrumentation.html#checking-instrumented-methods):
+Works for [non-instrumented methods](../../development/instrumentation.md#checking-instrumented-methods):
```ruby
instance_of_object.method(:foo).source_location
@@ -474,7 +474,7 @@ User.active.count
```
```bash
-# Using curl and jq (up to a max 100, see [pagination](https://docs.gitlab.com/ee/api/#pagination)
+# Using curl and jq (up to a max 100, see pagination docs https://docs.gitlab.com/ee/api/#pagination
curl --silent --header "Private-Token: ********************" "https://gitlab.example.com/api/v4/users?per_page=100&active" | jq --compact-output '.[] | [.id,.name,.username]'
```
@@ -744,7 +744,7 @@ build.dependencies.each do |d| { puts "status: #{d.status}, finished at: #{d.fin
### Disable strict artifact checking (Introduced in GitLab 10.3.0)
-See <https://docs.gitlab.com/ee/administration/job_artifacts.html#validation-for-dependencies>.
+See [job artifacts documentation](../job_artifacts.md#validation-for-dependencies).
```ruby
Feature.enable('ci_disable_validates_dependencies')
diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
index 6bcd4c48e5a..1247060058b 100644
--- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
+++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
@@ -177,7 +177,7 @@ and they will assist you with any issues you are having.
```
After <https://gitlab.com/gitlab-org/charts/gitlab/issues/780> is fixed, it should
- be possible to use [Updating GitLab using the Helm Chart](https://docs.gitlab.com/ee/install/kubernetes/gitlab_chart.html#updating-gitlab-using-the-helm-chart)
+ be possible to use [Updating GitLab using the Helm Chart](https://docs.gitlab.com/charts/index.html#updating-gitlab-using-the-helm-chart)
for upgrades.
- How to apply changes to GitLab config:
@@ -244,7 +244,7 @@ to those documents for details.
on your workstation.
- When all the pods show either a `Running` or `Completed` status, get the GitLab password as
- described in [Initial login](https://docs.gitlab.com/ee/install/kubernetes/gitlab_chart.html#initial-login),
+ described in [Initial login](https://docs.gitlab.com/charts/installation/deployment.html#initial-login),
and log in to GitLab via the UI. It will be accessible via `https://gitlab.domain`
where `domain` is the value provided in the yaml file.
diff --git a/doc/administration/troubleshooting/linux_cheat_sheet.md b/doc/administration/troubleshooting/linux_cheat_sheet.md
index 2bbb498f020..853f553571c 100644
--- a/doc/administration/troubleshooting/linux_cheat_sheet.md
+++ b/doc/administration/troubleshooting/linux_cheat_sheet.md
@@ -259,7 +259,7 @@ then compare summaries of both results and dive into the differences.
Rough numbers for calls to `open` and `openat` (used to access files) on various configurations.
Slow storage can cause the dreaded `DeadlineExceeded` error in Gitaly.
-Also [see this entry](https://docs.gitlab.com/ee/administration/operations/filesystem_benchmarking.html)
+Also [see this entry](../operations/filesystem_benchmarking.md)
in the handbook for quick tests customers can perform to check their filesystem performance.
Keep in mind that timing information from `strace` is often somewhat inaccurate, so
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index c41edb5dbfc..fdafac8420e 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -270,7 +270,7 @@ is interrupted mid-execution and it is not guaranteed
that proper rollback of transactions is implemented.
```ruby
-Gitlab::SidekiqMonitor.cancel_job('job-id')
+Gitlab::SidekiqDaemon::Monitor.cancel_job('job-id')
```
> This requires the Sidekiq to be run with `SIDEKIQ_MONITOR_WORKER=1`
diff --git a/doc/administration/troubleshooting/test_environments.md b/doc/administration/troubleshooting/test_environments.md
index 075effc5dc3..f1cdaf580a3 100644
--- a/doc/administration/troubleshooting/test_environments.md
+++ b/doc/administration/troubleshooting/test_environments.md
@@ -23,7 +23,7 @@ but contributions are welcome.
### GitLab
-Please see [our Docker test environment docs](https://docs.gitlab.com/ee/install/digitaloceandocker.html#create-new-gitlab-container)
+Please see [our Docker test environment docs](../../install/digitaloceandocker.md#create-new-gitlab-container)
for how to run GitLab on Docker. When spinning this up with `docker-machine`, ensure
you change a few things:
@@ -59,7 +59,7 @@ docker run --name gitlab_saml -p 8080:8080 -p 8443:8443 \
-d jamedjo/test-saml-idp
```
-The following will also need to go in your `/etc/gitlab/gitlab.rb`. See [our SAML docs](https://docs.gitlab.com/ee/integration/saml.html)
+The following will also need to go in your `/etc/gitlab/gitlab.rb`. See [our SAML docs](../../integration/saml.md)
for more, as well as the list of [default usernames, passwords, and emails](https://hub.docker.com/r/jamedjo/test-saml-idp/#usage).
```ruby
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 0d030ef30c8..c7637ad23de 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -821,6 +821,66 @@ Parameters:
]
```
+## Create MR Pipeline
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31722) in Gitlab 12.3.
+
+Create a new [pipeline for a merge request](../ci/merge_request_pipelines/index.md). A pipeline created via this endpoint will not run a regular branch/tag pipeline, it requires `.gitlab-ci.yml` to be configured with `only: [merge_requests]` to create jobs.
+
+The new pipeline can be:
+
+- A detached merge request pipeline.
+- A [pipeline for merged results](../ci/merge_request_pipelines/pipelines_for_merged_results/index.md)
+ if the [project setting is enabled](../ci/merge_request_pipelines/pipelines_for_merged_results/index.md#enabling-pipelines-for-merged-results).
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/pipelines
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
+- `merge_request_iid` (required) - The internal ID of the merge request
+
+```json
+{
+ "id": 2,
+ "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "ref": "refs/merge-requests/1/head",
+ "status": "pending",
+ "web_url": "http://localhost/user1/project1/pipelines/2",
+ "before_sha": "0000000000000000000000000000000000000000",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "id": 1,
+ "name": "John Doe1",
+ "username": "user1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon",
+ "web_url": "http://example.com"
+ },
+ "created_at": "2019-09-04T19:20:18.267Z",
+ "updated_at": "2019-09-04T19:20:18.459Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null,
+ "coverage": null,
+ "detailed_status": {
+ "icon": "status_pending",
+ "text": "pending",
+ "label": "pending",
+ "group": "pending",
+ "tooltip": "pending",
+ "has_details": false,
+ "details_path": "/user1/project1/pipelines/2",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png"
+ }
+}
+```
+
## Create MR
Creates a new merge request.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index cf28ea84704..3ea1434d5ca 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -929,7 +929,7 @@ POST /projects
| `ci_config_path` | string | no | The path to CI config file |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
-| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
| `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds |
@@ -986,7 +986,7 @@ POST /projects/user/:user_id
| `ci_config_path` | string | no | The path to CI config file |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
-| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
@@ -1043,7 +1043,7 @@ PUT /projects/:id
| `ci_default_git_depth` | integer | no | Default number of revisions for [shallow cloning](../user/project/pipelines/settings.md#git-shallow-clone) |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project |
| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) |
-| `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins |
+| `repository_storage` | string | no | **(STARTER ONLY)** Which storage shard the repository is on. Available only to admins |
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge request by default |
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index fc0125fcc18..4da527154ad 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -580,7 +580,7 @@ For private and internal projects:
If you want to use your own Docker images for docker-in-docker there are a few things you need to do in addition to the steps in the [docker-in-docker](#use-docker-in-docker-workflow-with-docker-executor) section:
1. Update the `image` and `service` to point to your registry.
-1. Add a service [alias](https://docs.gitlab.com/ee/ci/yaml/#servicesalias).
+1. Add a service [alias](../yaml/README.md#servicesalias).
Below is an example of what your `.gitlab-ci.yml` should look like,
assuming you have it configured with [TLS enabled](#tls-enabled):
diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md
index 9c65de115b4..88bcead7beb 100644
--- a/doc/ci/examples/code_quality.md
+++ b/doc/ci/examples/code_quality.md
@@ -1,118 +1,5 @@
---
-disqus_identifier: 'https://docs.gitlab.com/ee/ci/examples/code_climate.html'
-type: reference, howto
+redirect_to: '../../user/project/merge_requests/code_quality.md#example-configuration'
---
-# Analyze your project's Code Quality
-
-CAUTION: **Caution:**
-The job definition shown below is supported on GitLab 11.11 and later versions.
-It also requires the GitLab Runner 11.5 or later.
-For earlier versions, use the [previous job definitions](#previous-job-definitions).
-
-This example shows how to run Code Quality on your code by using GitLab CI/CD
-and Docker.
-
-First, you need GitLab Runner with
-[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
-
-Once you set up the Runner, include the CodeQuality template in your CI config:
-
-```yaml
-include:
- - template: Code-Quality.gitlab-ci.yml
-```
-
-The above example will create a `code_quality` job in your CI/CD pipeline which
-will scan your source code for code quality issues. The report will be saved as a
-[Code Quality report artifact](../yaml/README.md#artifactsreportscodequality-starter)
-that you can later download and analyze.
-Due to implementation limitations we always take the latest Code Quality artifact available.
-
-TIP: **Tip:**
-For [GitLab Starter][ee] users, this information will be automatically
-extracted and shown right in the merge request widget.
-[Learn more on Code Quality in merge requests](../../user/project/merge_requests/code_quality.md).
-
-CAUTION: **Caution:**
-On self-managed instances, if a malicious actor compromises the Code Quality job
-definition they will be able to execute privileged docker commands on the Runner
-host. Having proper access control policies mitigates this attack vector by
-allowing access only to trusted actors.
-
-## Previous job definitions
-
-CAUTION: **Caution:**
-Before GitLab 11.5, Code Quality job and artifact had to be named specifically
-to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
-and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
-
-For GitLab 11.5 and earlier, the job should look like:
-
-```yaml
-code_quality:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- artifacts:
- reports:
- codequality: gl-code-quality-report.json
-```
-
-For GitLab 11.4 and earlier, the job should look like:
-
-```yaml
-code_quality:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- artifacts:
- paths: [gl-code-quality-report.json]
-```
-
-Alternatively the job name could be `codeclimate` or `codequality`
-and the artifact name could be `codeclimate.json`.
-These names have been deprecated with GitLab 11.0
-and may be removed in next major release, GitLab 12.0.
-
-For GitLab 10.3 and earlier, the job should look like:
-
-```yaml
-codequality:
- image: docker:latest
- variables:
- DOCKER_DRIVER: overlay
- services:
- - docker:dind
- script:
- - docker pull codeclimate/codeclimate:0.69.0
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
- - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
- artifacts:
- paths: [codeclimate.json]
-```
-
-[cli]: https://github.com/codeclimate/codeclimate
-[ee]: https://about.gitlab.com/pricing/
+This document was moved to [another location](../../user/project/merge_requests/code_quality.md#example-configuration).
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 409f7d62538..a3c253cc517 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -100,6 +100,7 @@ future GitLab releases.**
| `CI_RUNNER_REVISION` | all | 10.6 | GitLab Runner revision that is executing the current job |
| `CI_RUNNER_TAGS` | 8.10 | 0.5 | The defined runner tags |
| `CI_RUNNER_VERSION` | all | 10.6 | GitLab Runner version that is executing the current job |
+| `CI_RUNNER_SHORT_TOKEN` | all | 12.3 | First eight characters of GitLab Runner's token used to authenticate new job requests. Used as Runner's unique ID |
| `CI_SERVER` | all | all | Mark that job is executed in CI environment |
| `CI_SERVER_HOST` | 12.1 | all | Host component of the GitLab instance URL, without protocol and port (like gitlab.example.com) |
| `CI_SERVER_NAME` | all | all | The name of CI server that is used to coordinate jobs |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 8aae0e85c89..61012d8653a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2089,7 +2089,7 @@ staging:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23464) in GitLab 12.3.
`interruptible` is used to indicate that a job should be canceled if made redundant by a newer run of the same job. Defaults to `true`.
-This value will only be used if the [automatic cancellation of redundant pipelines feature](https://docs.gitlab.com/ee/user/project/pipelines/settings.html#auto-cancel-pending-pipelines)
+This value will only be used if the [automatic cancellation of redundant pipelines feature](../../user/project/pipelines/settings.md#auto-cancel-pending-pipelines)
is enabled.
When enabled, a pipeline on the same branch will be canceled when:
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 814624c7586..5667f58b0c3 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -99,7 +99,7 @@ automatically.
Its simplest usage is to provide the value for `title`:
```text
-$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
+bin/changelog 'Hey DZ, I added a feature to GitLab!'
```
At this point the script would ask you to select the category of the change (mapped to the `type` field in the entry):
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index bcfc0734c06..0afa44a5491 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -72,7 +72,7 @@ from teams other than your own.
1. If your merge request includes adding a new UI/UX paradigm [^1], it must be
**approved by a [UX lead][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must be
- **approved by a [Distribution team member][team]**. See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/dev-backend/distribution/) for more details.
+ **approved by a [Distribution team member][team]**. See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/#how-to-work-with-distribution) for more details.
#### Security requirements
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index 4776c8348d4..d2810fe89f0 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -27,7 +27,7 @@ no overhead at all.
To enable `GITLAB_TRACING`, a valid _"configuration-string"_ value should be set, with a URL-like
form:
-```console
+```sh
GITLAB_TRACING=opentracing://<driver>?<param_name>=<param_value>&<param_name_2>=<param_value_2>
```
@@ -90,7 +90,7 @@ documentation](https://www.jaegertracing.io/docs/1.9/getting-started/).
If you have Docker available, the easier approach to running the Jaeger all-in-one is through
Docker, using the following command:
-```console
+```sh
$ docker run \
--rm \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
@@ -121,8 +121,8 @@ appropriate configuration string.
**TL;DR:** If you are running everything on the same host, use the following value:
-```console
-$ export GITLAB_TRACING="opentracing://jaeger?http_endpoint=http%3A%2F%2Flocalhost%3A14268%2Fapi%2Ftraces&sampler=const&sampler_param=1"
+```sh
+export GITLAB_TRACING="opentracing://jaeger?http_endpoint=http%3A%2F%2Flocalhost%3A14268%2Fapi%2Ftraces&sampler=const&sampler_param=1"
```
This configuration string uses the Jaeger driver `opentracing://jaeger` with the following options:
@@ -152,7 +152,7 @@ application.
When `GITLAB_TRACING` is configured properly, the application will log this on startup:
-```console
+```sh
13:41:53 gitlab-workhorse.1 | 2019/02/12 13:41:53 Tracing enabled
...
13:41:54 gitaly.1 | 2019/02/12 13:41:54 Tracing enabled
@@ -161,7 +161,7 @@ When `GITLAB_TRACING` is configured properly, the application will log this on s
If `GITLAB_TRACING` is not configured correctly, this will also be logged:
-```console
+```sh
13:43:45 gitaly.1 | 2019/02/12 13:43:45 skipping tracing configuration step: tracer: unable to load driver mytracer
```
diff --git a/doc/development/documentation/feature-change-workflow.md b/doc/development/documentation/feature-change-workflow.md
index 00c76fe0f1b..38ccae41df4 100644
--- a/doc/development/documentation/feature-change-workflow.md
+++ b/doc/development/documentation/feature-change-workflow.md
@@ -92,11 +92,10 @@ do the following:
#### Authoring
-As a developer, you must ship the documentation with the code of the feature that
-you are creating or updating. The documentation is an essential part of the product.
+As a developer, if a ~feature issue also contains the ~Documentation label, you must ship the new or updated documentation with the code of the feature. The documentation is an essential part of the product.
Technical writers are happy to help, as requested and planned on an issue-by-issue basis.
-Follow the process below unless otherwise agreed with the product manager and technical writer for a given issue:
+For feature issues requiring documentation, follow the process below unless otherwise agreed with the product manager and technical writer for a given issue:
- Include any new and edited docs in the MR introducing the code.
- Use the Documentation requirements confirmed by the Product Manager in the
@@ -111,8 +110,8 @@ Follow the process below unless otherwise agreed with the product manager and te
idea or outline, or request any other help, ping the Technical Writer for the relevant
[DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages)
in your issue or MR, or write within `#docs` on the GitLab Slack.
-- The docs must be merged with the code **by the feature freeze date**, otherwise
- the feature cannot be included with the release. A policy for documenting feature-flagged
+- If you are working on documentation in a separate MR, ensure that if the code is merged by the 17th, the docs are as well, per the [Engineering Workflow](https://about.gitlab.com/handbook/engineering/workflow/). If the docs are not ready, the PM can approve merging the code if the engineer and tech writer commit to get documentation merged by the 21st. Otherwise the feature is not considered complete, and should not be merged.
+- A policy for documenting feature-flagged
issues is forthcoming and you are welcome to join the [discussion](https://gitlab.com/gitlab-org/gitlab-ce/issues/56813).
#### Reviews and merging
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 4a50e90a26c..39b5e191a7b 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -262,7 +262,7 @@ table_display_block: true
## Punctuation
Check the general punctuation rules for the GitLab documentation on the table below.
-Check specific punctuation rules for [list items](#list-items) below.
+Check specific punctuation rules for [lists](#lists) below.
| Rule | Example |
| ---- | ------- |
@@ -274,37 +274,44 @@ Check specific punctuation rules for [list items](#list-items) below.
| Always add a space before and after dashes when using it in a sentence (for replacing a comma, for example). | _You should try this - or not._ |
| Always use lowercase after a colon. | _Related Issues: a way to create a relationship between issues._ |
-## List items
+## Lists
- 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 [nested subitem](#nesting-inside-a-list-item).
-- Only use ordered lists when their items describe a sequence of steps to follow:
- Do:
+### Ordered vs. unordered lists
- These are the steps to do something:
+Only use ordered lists when their items describe a sequence of steps to follow.
- 1. First, do step 1
- 1. Then, do step 2
- 1. Finally, do step 3
+Do:
+
+```md
+These are the steps to do something:
+
+1. First, do the first step.
+1. Then, do the next step.
+1. Finally, do the last step.
+```
- Don't:
+Don't:
- This is a list of different features:
+```md
+This is a list of available features:
- 1. Feature 1
- 1. Feature 2
- 1. Feature 3
+1. Feature 1
+1. Feature 2
+1. Feature 3
+```
-**Markup:**
+### Markup
- Use dashes (`-`) for unordered lists instead of asterisks (`*`).
-- Prefix `1.` to each item in an ordered list.
+- Prefix `1.` to every item in an ordered list.
When rendered, the list items will appear with sequential numbering automatically.
-**Punctuation:**
+### Punctuation
- 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.
@@ -343,7 +350,7 @@ Do:
- Let's say this is also a complete sentence.
- Not a complete sentence.
-Don't (third item should have a `.` to match the first and second items):
+Don't (vary use of periods; majority rules):
- Let's say this is a complete sentence.
- Let's say this is also a complete sentence.
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 777d372ec60..b22a63621dc 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -81,9 +81,7 @@ If you're using Pry you can use the `$` command to display the source code of a
method (along with its source location), this is easier than running the above
Ruby code. In case of the above snippet you'd run the following:
-```
-$ Banzai::Renderer.render
-```
+- `$ Banzai::Renderer.render`
This will print out something along the lines of:
diff --git a/doc/development/new_fe_guide/style/prettier.md b/doc/development/new_fe_guide/style/prettier.md
index 5f44c640d76..17b209d419e 100644
--- a/doc/development/new_fe_guide/style/prettier.md
+++ b/doc/development/new_fe_guide/style/prettier.md
@@ -61,35 +61,38 @@ This will go over all files in a specific folder and save it.
## VSCode Settings
-### Format on Save
+### Select Prettier as default formatter
-To automatically format your files with Prettier, add the following properties to your User or Workspace Settings:
+To select Prettier as a formatter, add the following properties to your User or Workspace Settings:
```javascript
{
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
"[javascript]": {
- "editor.formatOnSave": true
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
- "editor.formatOnSave": true
- },
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ }
}
```
-### Conflicts with Vetur Extension
+### Format on Save
-There are some [runtime issues](https://github.com/vuejs/vetur/issues/950) with [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [the Vetur extension](https://marketplace.visualstudio.com/items?itemName=octref.vetur) for VSCode. To fix this, try adding the following properties to your User or Workspace Settings:
+To automatically format your files with Prettier, add the following properties to your User or Workspace Settings:
```javascript
{
- "prettier.disableLanguages": [],
- "vetur.format.defaultFormatter.html": "none",
- "vetur.format.defaultFormatter.js": "none",
- "vetur.format.defaultFormatter.css": "none",
- "vetur.format.defaultFormatter.less": "none",
- "vetur.format.defaultFormatter.postcss": "none",
- "vetur.format.defaultFormatter.scss": "none",
- "vetur.format.defaultFormatter.stylus": "none",
- "vetur.format.defaultFormatter.ts": "none",
+ "[html]": {
+ "editor.formatOnSave": true
+ },
+ "[javascript]": {
+ "editor.formatOnSave": true
+ },
+ "[vue]": {
+ "editor.formatOnSave": true
+ },
}
```
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 14b3f8204d2..6e6c80b7a7c 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -123,7 +123,7 @@ Keeping that in mind, to create a profile, identify (or create) a spec that
exercises the troublesome code path, then run it using the `bin/rspec-stackprof`
helper, e.g.:
-```
+```sh
$ LIMIT=10 bin/rspec-stackprof spec/policies/project_policy_spec.rb
8/8 |====== 100 ======>| Time: 00:00:18
@@ -157,22 +157,22 @@ it calls, were being executed.
To create a graphical view of the call stack:
-```shell
-$ stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot
-$ dot -Tsvg project_policy_spec.dot > project_policy_spec.svg
+```sh
+stackprof tmp/project_policy_spec.rb.dump --graphviz > project_policy_spec.dot
+dot -Tsvg project_policy_spec.dot > project_policy_spec.svg
```
To load the profile in [kcachegrind](https://kcachegrind.github.io/):
-```
-$ stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind
-$ kcachegrind project_policy_spec.callgrind # Linux
-$ qcachegrind project_policy_spec.callgrind # Mac
+```sh
+stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind
+kcachegrind project_policy_spec.callgrind # Linux
+qcachegrind project_policy_spec.callgrind # Mac
```
It may be useful to zoom in on a specific method, e.g.:
-```
+```sh
$ stackprof tmp/project_policy_spec.rb.dump --method warm_asset_cache
TestEnv#warm_asset_cache (/Users/lupine/dev/gitlab.com/gitlab-org/gitlab-development-kit/gitlab/spec/support/test_env.rb:164)
samples: 0 self (0.0%) / 6288 total (36.9%)
@@ -225,9 +225,9 @@ may have changed over time.
To activate profiling in your local environment, run the following:
-```
-$ export RSPEC_PROFILING=yes
-$ rake rspec_profiling:install
+```sh
+export RSPEC_PROFILING=yes
+rake rspec_profiling:install
```
This creates an SQLite3 database in `tmp/rspec_profiling`, into which statistics
@@ -237,7 +237,7 @@ variable set.
Ad-hoc investigation of the collected results can be performed in an interactive
shell:
-```
+```sh
$ rake rspec_profiling:console
irb(main):001:0> results.count
=> 231
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index e9d6cfe00b2..a144bf70a86 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -216,3 +216,13 @@ bundle exec rake routes
Since these take some time to create, it's often helpful to save the output to
a file for quick reference.
+
+## Show obsolete `ignored_columns`
+
+To see a list of all obsolete `ignored_columns` run:
+
+```
+bundle exec rake db:obsolete_ignored_columns
+```
+
+Feel free to remove their definitions from their `ignored_columns` definitions.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 7dc89a3fcdb..91004bf177d 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -20,9 +20,18 @@ We have started to migrate frontend tests to the [Jest](https://jestjs.io) testi
Jest tests can be found in `/spec/frontend` and `/ee/spec/frontend` in EE.
-It is not yet a requirement to use Jest. You can view the
-[epic](https://gitlab.com/groups/gitlab-org/-/epics/873) of issues
-we need to solve before being able to use Jest for all our needs.
+### When should I use Jest over Karma?
+
+If you need to update an existing Karma test file (found in `spec/javascripts`), you do not
+need to migrate the whole spec to Jest. Simply updating the Karma spec to test your change
+is fine. It is probably more appropriate to migrate to Jest in a separate merge request.
+
+If you need to create a new test file, we strongly recommend creating one in Jest. This will
+help support our migration and we think you'll love using Jest.
+
+As always, please use discretion. Jest solves a lot of issues we experienced in Karma and
+provides a better developer experience, however there are potentially unexpected issues
+which could arise (especially with testing against browser specific features).
### Differences to Karma
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index a3f6f2b327c..0dc029a4cd1 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -47,14 +47,14 @@ to avoid getting this error, you need to remove all instances of the
**Omnibus Installation**
-```
-$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
+```sh
+sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
```
**Source Installation**
-```
-$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
+```sh
+bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
### Variables environment scopes
@@ -89,10 +89,10 @@ To downgrade a source installation, you need to replace the current remote of
your GitLab installation with the Community Edition's remote, fetch the latest
changes, and checkout the latest stable branch:
-```
-$ git remote set-url origin git@gitlab.com:gitlab-org/gitlab-ce.git
-$ git fetch --all
-$ git checkout 8-x-stable
+```sh
+git remote set-url origin git@gitlab.com:gitlab-org/gitlab-ce.git
+git fetch --all
+git checkout 8-x-stable
```
Remember to follow the correct [update guides](../update/README.md) to make
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index fbbe2a34952..cfd0fd48c70 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -225,7 +225,7 @@ First, we will create a new project to host our application. You can do this
either by running the CLI client:
```bash
-$ oc new-project gitlab
+oc new-project gitlab
```
or by using the web interface:
diff --git a/doc/integration/github.md b/doc/integration/github.md
index f19b3109d15..23dd67f6891 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -158,7 +158,7 @@ For installation from source:
You will also need to disable Git SSL verification on the server hosting GitLab.
```
-$ git config --global http.sslVerify false
+git config --global http.sslVerify false
```
For the changes to take effect, [reconfigure GitLab] if you installed
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index d93e7241fda..326f7e4b982 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -42,17 +42,17 @@ If you are using an installation from source, replace `/var/opt/gitlab/` with `/
#### Omnibus Installation
-```
-$ sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
+```sh
+sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
```
#### Installation from source
Before running this command you need to change the directory to where your GitLab installation is located:
-```
-$ cd /home/git/gitlab
-$ sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
+```sh
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
```
#### Example output
diff --git a/doc/security/asset_proxy.md b/doc/security/asset_proxy.md
index 6a2341c28c8..b480905339b 100644
--- a/doc/security/asset_proxy.md
+++ b/doc/security/asset_proxy.md
@@ -1,28 +1,68 @@
+# Proxying assets
+
A possible security concern when managing a public facing GitLab instance is
the ability to steal a users IP address by referencing images in issues, comments, etc.
For example, adding `![Example image](http://example.com/example.png)` to
an issue description will cause the image to be loaded from the external
-server in order to be displayed. However this also allows the external server
+server in order to be displayed. However, this also allows the external server
to log the IP address of the user.
One way to mitigate this is by proxying any external images to a server you
-control. GitLab handles this by allowing you to run the "Camo" server
-[cactus/go-camo](https://github.com/cactus/go-camo#how-it-works).
-The image request is sent to the Camo server, which then makes the request for
-the original image. This way an attacker only ever seems the IP address
-of your Camo server.
+control.
+
+GitLab can be configured to use an asset proxy server when requesting external images/videos in
+issues, comments, etc. This helps ensure that malicious images do not expose the user's IP address
+when they are fetched.
+
+We currently recommend using [cactus/go-camo](https://github.com/cactus/go-camo#how-it-works)
+as it supports proxying video and is more configurable.
+
+## Installing Camo server
+
+A Camo server is used to act as the proxy.
+
+To install a Camo server as an asset proxy:
+
+1. Deploy a `go-camo` server. Helpful instructions can be found in
+ [building catus/go-camo](https://github.com/cactus/go-camo#building).
+
+1. Make sure your instance of GitLab is running, and that you have created a private API token.
+ Using the API, configure the asset proxy settings on your GitLab instance. For example:
+
+ ```sh
+ curl --request "PUT" "https://gitlab.example.com/api/v4/application/settings?\
+ asset_proxy_enabled=true&\
+ asset_proxy_url=https://proxy.gitlab.example.com&\
+ asset_proxy_secret_key=<somekey>" \
+ --header 'PRIVATE-TOKEN: <my_private_token>'
+ ```
+
+ The following settings are supported:
+
+ | Attribute | Description |
+ |:-------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
+ | `asset_proxy_enabled` | Enable proxying of assets. If enabled, requires: `asset_proxy_url`). |
+ | `asset_proxy_secret_key` | Shared secret with the asset proxy server. |
+ | `asset_proxy_url` | URL of the asset proxy server. |
+ | `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. |
+
+1. Restart the server for the changes to take effect. Each time you change any values for the asset
+ proxy, you need to restart the server.
+
+## Using the Camo server
+
+Once the Camo server is running and you've enabled the GitLab settings, any image or video that
+references an external source will get proxied to the Camo server.
-Once you have your Camo server up and running, you can configure GitLab to
-proxy image requests to it. The following settings are supported:
+For example, the following is a link to an image in Markdown:
-| Attribute | Description |
-| ------------------------- | ----------- |
-| `asset_proxy_enabled` | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. |
-| `asset_proxy_secret_key` | Shared secret with the asset proxy server. |
-| `asset_proxy_url` | URL of the asset proxy server. |
-| `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. |
+```markdown
+![logo](https://about.gitlab.com/images/press/logo/jpg/gitlab-icon-rgb.jpg)
+```
-These can be set via the [Application setting API](../api/settings.md)
+The following is an example of a source link that could result:
-Note that a GitLab restart is required to apply any changes.
+```text
+http://proxy.gitlab.example.com/f9dd2b40157757eb82afeedbf1290ffb67a3aeeb/68747470733a2f2f61626f75742e6769746c61622e636f6d2f696d616765732f70726573732f6c6f676f2f6a70672f6769746c61622d69636f6e2d7267622e6a7067
+```
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index 13c406727ab..576aad17c0a 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -115,7 +115,7 @@ To subscribe to GitLab through a self-managed installation:
1. [Install](https://about.gitlab.com/install/) GitLab.
1. Complete the installation with
- [administration tasks](https://docs.gitlab.com/ee/administration/).
+ [administration tasks](../administration/index.md).
1. Select the **Starter**, **Premium**, or **Ultimate** self-managed plan
through the [GitLab Subscription Manager](https://customers.gitlab.com/).
1. Apply your license file. After purchase, a license file is sent to the email
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 15fdb52ac00..0cbd85fc568 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -780,15 +780,19 @@ It is also possible to copy and paste the contents of the [Auto DevOps
template] into your project and edit this as needed. You may prefer to do it
that way if you want to specifically remove any part of it.
-### Using components of Auto-DevOps
+### Using components of Auto DevOps
-If you only require a subset of the features offered by Auto-DevOps, you can include
-individual Auto-DevOps jobs into your own `.gitlab-ci.yml`.
+If you only require a subset of the features offered by Auto DevOps, you can include
+individual Auto DevOps jobs into your own `.gitlab-ci.yml`. Each component job relies
+on a stage that should be defined in the `.gitlab-ci.yml` that includes the template.
For example, to make use of [Auto Build](#auto-build), you can add the following to
your `.gitlab-ci.yml`:
```yaml
+stages:
+ - build
+
include:
- template: Jobs/Build.gitlab-ci.yml
```
diff --git a/doc/user/analytics/cycle_analytics.md b/doc/user/analytics/cycle_analytics.md
index b7389c8689d..377af433342 100644
--- a/doc/user/analytics/cycle_analytics.md
+++ b/doc/user/analytics/cycle_analytics.md
@@ -1,7 +1,10 @@
# Cycle Analytics
> - Introduced prior to GitLab 12.2 at the project level.
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2 at the group level (enabled by feature flag `analytics`).
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2 at the group level.
+
+NOTE: **Note:**
+As of GitLab 12.3 this feature is enabled by the `cycle_analytics` feature flag.
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
@@ -24,9 +27,6 @@ Cycle Analytics is available:
In the future, multiple groups will be selectable which will effectively make this an
instance-level feature.
- NOTE: **Note:**
- Requires the [analytics workspace](index.md) to be enabled.
-
- At the project level via **Project > Cycle Analytics**.
There are seven stages that are tracked as part of the Cycle Analytics calculations.
diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md
index ec719c0b4a1..ba2735dc324 100644
--- a/doc/user/analytics/index.md
+++ b/doc/user/analytics/index.md
@@ -1,22 +1,21 @@
# Analytics workspace
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) in GitLab 12.2 (enabled using `analytics` feature flag).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) in GitLab 12.2.
The Analytics workspace will make it possible to aggregate analytics across
GitLab, so that users can view information across multiple projects and groups
in one place.
-To access the centralized analytics workspace:
-
-1. Ensure it's enabled. Requires a GitLab administrator to enable it with the `analytics` feature
- flag.
-1. Once enabled, click on **Analytics** from the top navigation bar.
+To access the centralized analytics workspace, click on **Analytics** from the top navigation bar.
## Available analytics
From the centralized analytics workspace, the following analytics are available:
-- [Cycle Analytics](cycle_analytics.md).
+- [Cycle Analytics](cycle_analytics.md):
+
+1. Requires a GitLab administrator to enable it with the `cycle_analytics` feature flag.
+1. Once enabled, click on **Analytics** and then **Cycle Analytics** from the top navigation bar.
NOTE: **Note:**
Project-level Cycle Analytics are still available at a project's **Project > Cycle Analytics**.
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
deleted file mode 100644
index af9cee08d71..00000000000
--- a/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dependency_list/img/dependency_list_v12_3.png b/doc/user/application_security/dependency_list/img/dependency_list_v12_3.png
new file mode 100644
index 00000000000..1ae44687ed5
--- /dev/null
+++ b/doc/user/application_security/dependency_list/img/dependency_list_v12_3.png
Binary files differ
diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md
index 38c38bbd8a9..4f51ff4a50b 100644
--- a/doc/user/application_security/dependency_list/index.md
+++ b/doc/user/application_security/dependency_list/index.md
@@ -17,7 +17,7 @@ sidebar.
## Viewing dependencies
-![Dependency List](img/dependency_list_v12_2.png)
+![Dependency List](img/dependency_list_v12_3.png)
Dependencies are displayed with the following information:
@@ -28,6 +28,7 @@ Dependencies are displayed with the following information:
| 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 |
+| License | Links to dependency's software licenses |
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
@@ -43,6 +44,13 @@ 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.
+## Licenses
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10536) in GitLab Ultimate 12.3.
+
+If the [License Compliance](../license_compliance/index.md) CI job is configured,
+the [discovered licenses](../license_compliance/index.md#supported-languages-and-package-managers) will be displayed on this page.
+
## Downloading the Dependency List
Your project's full list of dependencies and their details can be downloaded in
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index d7b2572c717..166a71b6fbe 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -4,8 +4,11 @@ type: reference, howto
# Dependency Scanning **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5105)
-in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5105) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.7.
+
+Dependency Scanning helps to automatically find security vulnerabilities in your dependencies
+while you are developing and testing your applications, for example when your
+application is using an external (open source) library which is known to be vulnerable.
## Overview
@@ -18,7 +21,7 @@ in your existing `.gitlab-ci.yml` file or by implicitly using
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
GitLab checks the Dependency Scanning report, compares the found vulnerabilities
-between the source and target branches, and shows the information right on the
+between the source and target branches, and shows the information on the
merge request.
![Dependency Scanning Widget](img/dependency_scanning.png)
@@ -32,12 +35,6 @@ The results are sorted by the severity of the vulnerability:
1. Unknown
1. Everything else
-## Use cases
-
-It helps to automatically find security vulnerabilities in your dependencies
-while you are developing and testing your applications. For example when your
-application is using an external (open source) library which is known to be vulnerable.
-
## Requirements
To run a Dependency Scanning job, you need GitLab Runner with the
@@ -162,10 +159,39 @@ using environment variables.
| `PIP_INDEX_URL` | Base URL of Python Package Index (default `https://pypi.org/simple`). | |
| `PIP_EXTRA_INDEX_URL` | Array of [extra URLs](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-extra-index-url) of package indexes to use in addition to `PIP_INDEX_URL`. Comma separated. | |
+## Interacting with the vulnerabilities
+
+Once a vulnerability is found, you can interact with it. Read more on how to
+[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
+
+## Solutions for vulnerabilities (auto-remediation)
+
+Some vulnerabilities can be fixed by applying the solution that GitLab
+automatically generates.
+
+Read more about the [solutions for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation).
+
+## Security Dashboard
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups, projects and pipelines. Read more about the
+[Security Dashboard](../security_dashboard/index.md).
+
+## Vulnerabilities database update
+
+For more information about the vulnerabilities database update, check the
+[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
+
+## 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).
+
## Reports JSON format
CAUTION: **Caution:**
-The JSON report artifacts are not a public API of Dependency Scanning and their format may change in future.
+The JSON report artifacts are not a public API of Dependency Scanning and their format may change in the future.
The Dependency Scanning tool emits a JSON report file. Here is an example of the report structure with all important parts of
it highlighted:
@@ -315,28 +341,6 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
-## Security Dashboard
-
-The Security Dashboard is a good place to get an overview of all the security
-vulnerabilities in your groups, projects and pipelines. Read more about the
-[Security Dashboard](../security_dashboard/index.md).
-
-## Interacting with the vulnerabilities
-
-Once a vulnerability is found, you can interact with it. Read more on how to
-[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
-
-## Vulnerabilities database update
-
-For more information about the vulnerabilities database update, check the
-[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
-
-## Dependency List **(ULTIMATE)**
-
-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
Please check the [Release Process documentation](https://gitlab.com/gitlab-org/security-products/release/blob/master/docs/release_process.md).
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 69529d7420b..f25d792cb90 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -71,8 +71,7 @@ entry, a detailed information will pop up with different possible options:
- [Create issue](#creating-an-issue-for-a-vulnerability): The new issue will
have the title and description pre-populated with the information from the
vulnerability report and will be created as [confidential](../project/issues/confidential_issues.md) by default.
-- [Solution](#solutions-for-vulnerabilities): For some vulnerabilities
- ([Dependency Scanning](dependency_scanning/index.md) and [Container Scanning](container_scanning/index.md))
+- [Solution](#solutions-for-vulnerabilities-auto-remediation): For some vulnerabilities,
a solution is provided for how to fix the vulnerability.
![Interacting with security reports](img/interactive_reports.png)
@@ -109,17 +108,16 @@ the vulnerability will now have an associated issue next to the name.
![Linked issue in the group security dashboard](img/issue.png)
-### Solutions for vulnerabilities
+### Solutions for vulnerabilities (auto-remediation)
-> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.7.
-
-CAUTION: **Warning:**
-Automatic Patch creation is only available for a subset of
-[Dependency Scanning](dependency_scanning/index.md). At the moment only Node.JS
-projects managed with yarn are supported.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5656) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.7.
Some vulnerabilities can be fixed by applying the solution that GitLab
-automatically generates.
+automatically generates. The following scanners are supported:
+
+- [Dependency Scanning](dependency_scanning/index.md):
+ Automatic Patch creation is only available for Node.JS projects managed with
+ `yarn`.
#### Manually applying the suggested patch
@@ -136,13 +134,12 @@ generated by GitLab. To apply the fix:
#### Creating a merge request from a vulnerability
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9224) in
-> [GitLab Ultimate](https://about.gitlab.com/pricing) 11.9.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9224) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.9.
In certain cases, GitLab will allow you to create a merge request that will
automatically remediate the vulnerability. Any vulnerability that has a
-[solution](#solutions-for-vulnerabilities) can have a merge request created to
-automatically solve the issue.
+[solution](#solutions-for-vulnerabilities-auto-remediation) can have a merge
+request created to automatically solve the issue.
If this action is available there will be a **Create merge request** button in the vulnerability modal.
Clicking on this button will create a merge request to apply the solution onto the source branch.
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 6b748981106..a198d83fac5 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -36,6 +36,9 @@ Thread resolution helps keep track of progress during planning or code review.
Every standard comment or thread in merge requests, commits, commit diffs, and
snippets is initially displayed as unresolved. They can then be individually resolved by anyone
with at least Developer access to the project or by the author of the change being reviewed.
+If the thread has been resolved and a non-member unresolves their own response,
+this will also unresolve the discussion thread.
+If the non-member then resolves this same response, this will resolve the discussion thread.
The need to resolve all standard comments or threads prevents you from forgetting
to address feedback and lets you hide threads that are no longer relevant.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index edf2fedab3c..d3cbe4d545f 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -997,16 +997,18 @@ These details <em>will</em> remain <strong>hidden</strong> until expanded.
Markdown inside these tags is supported as well, as long as you have a blank line
after the `</summary>` tag and before the `</details>` tag, as shown in the example:
-```html
+````html
<details>
<summary>Click me to collapse/fold.</summary>
These details _will_ remain **hidden** until expanded.
+```
PASTE LOGS HERE
+```
</details>
-```
+````
<!-- Note: The example below uses HTML to force correct rendering on docs.gitlab.com, markdown will be fine in GitLab -->
@@ -1015,7 +1017,7 @@ PASTE LOGS HERE
These details <em>will</em> remain <b>hidden</b> until expanded.
-PASTE LOGS HERE
+<pre><code>PASTE LOGS HERE</code></pre>
</details>
@@ -1161,16 +1163,15 @@ GFM will autolink almost any URL you put into your text:
### Lists
-Ordered and unordered lists can be easily created. Add the number you want the list
+Ordered and unordered lists can be easily created.
+
+For an ordered list, add the number you want the list
to start with, like `1.`, followed by a space, at the start of each line for ordered lists.
After the first number, it does not matter what number you use, ordered lists will be
numbered automatically by vertical order, so repeating `1.` for all items in the
same list is common. If you start with a number other than `1.`, it will use that as the first
number, and count up from there.
-Add a `*`, `-` or `+`, followed by a space, at the start of each line for unordered lists, but
-you should not use a mix of them.
-
Examples:
```md
@@ -1181,15 +1182,10 @@ Examples:
1. Ordered sub-list
1. Next ordered sub-list item
4. And another item.
-
-* Unordered lists can use asterisks
-
-- Or minuses
-
-+ Or pluses
```
-<!-- The "2." and "4." in the example above are changed to "1." below, only to match the standards on docs.gitlab.com -->
+<!-- The "2." and "4." in the example above are changed to "1." below, to match the style standards on docs.gitlab.com -->
+<!-- See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists -->
1. First ordered list item
1. Another item
@@ -1199,11 +1195,43 @@ Examples:
1. Next ordered sub-list item
1. And another item.
-- Unordered lists can use asterisks
+For an unordered list, add a `-`, `*` or `+`, followed by a space, at the start of
+each line for unordered lists, but you should not use a mix of them.
+
+```md
+Unordered lists can:
+
+- use
+- minuses
+
+They can also:
+
+* use
+* asterisks
+
+They can even:
+
++ use
++ pluses
+```
+
+<!-- The "*" and "+" in the example above are changed to "-" below, to match the style standards on docs.gitlab.com -->
+<!-- See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists -->
+
+Unordered lists can:
+
+- use
+- minuses
+
+They can also:
+
+- use
+- asterisks
-- Or minuses
+They can even:
-- Or pluses
+- use
+- pluses
---
@@ -1292,7 +1320,7 @@ Example:
Additionally, you can choose the alignment of text within columns by adding colons (`:`)
to the sides of the "dash" lines in the second row. This will affect every cell in the column.
-> Note that the headers are always right aligned [within GitLab itself itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#tables).
+> Note that the headers are always right aligned [within GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#tables).
```markdown
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 02b5f7437ee..46a3090e268 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -50,6 +50,7 @@ The following table depicts the various user permission levels in a project.
| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Security reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View licenses in Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
@@ -151,13 +152,9 @@ which visibility level you select on project settings.
### Protected branches
-To prevent people from messing with history or pushing code without
-review, we've created protected branches. Read through the documentation on
-[protected branches](project/protected_branches.md)
-to learn more.
-
-Additionally, you can allow or forbid users with Maintainer and/or
-Developer permissions to push to a protected branch. Read through the documentation on
+Additional restrictions can be applied on a per-branch basis with [protected branches](project/protected_branches.md).
+Additionally, you can customize permissions to allow or prevent project
+Maintainers and Developers from pushing to a protected branch. Read through the documentation on
[Allowed to Merge and Allowed to Push settings](project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings)
to learn more.
diff --git a/doc/user/profile/account/create_accounts.md b/doc/user/profile/account/create_accounts.md
new file mode 100644
index 00000000000..c0a887d0779
--- /dev/null
+++ b/doc/user/profile/account/create_accounts.md
@@ -0,0 +1,36 @@
+---
+type: reference
+---
+
+# Creating users **(CORE ONLY)**
+
+You can create users:
+
+- Manually through the sign in page or Admin Area.
+- Automatically through user authentication integrations.
+
+## Create users on sign in page
+
+If you have [sign-up enabled](../../admin_area/settings/sign_up_restrictions.md), users can create their own accounts using the **Register** tab on the sign in page.
+
+![Register Tab](img/register_tab.png)
+
+## Create users in admin area
+
+As an admin user, you can manually create users by:
+
+1. Navigating to **Admin Area > Overview > Users** (`/admin/users` page).
+1. Selecting the **New User** button.
+
+You can also [create users through the API](../../../api/users.md) as an admin.
+
+![Admin User Button](img/admin_user_button.png)
+
+![Admin User Form](img/admin_user_form.png)
+
+## Create users through integrations
+
+Users will be:
+
+- Automatically created upon first login with the [LDAP integration](../../../administration/auth/ldap.md).
+- Created when first logging in via an [OmniAuth provider](../../../integration/omniauth.md) if the `allow_single_sign_on` setting is present.
diff --git a/doc/user/profile/account/img/admin_user_button.png b/doc/user/profile/account/img/admin_user_button.png
new file mode 100644
index 00000000000..6be9c1e266a
--- /dev/null
+++ b/doc/user/profile/account/img/admin_user_button.png
Binary files differ
diff --git a/doc/user/profile/account/img/admin_user_form.png b/doc/user/profile/account/img/admin_user_form.png
new file mode 100644
index 00000000000..ede96373c73
--- /dev/null
+++ b/doc/user/profile/account/img/admin_user_form.png
Binary files differ
diff --git a/doc/user/profile/account/img/register_tab.png b/doc/user/profile/account/img/register_tab.png
new file mode 100644
index 00000000000..73faa3edd1c
--- /dev/null
+++ b/doc/user/profile/account/img/register_tab.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index e5ccc8ee758..40b7294c3bb 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -8,6 +8,10 @@ Each GitLab account has a user profile, and settings. Your [profile](#user-profi
contains information about you, and your GitLab activity. Your [settings](#profile-settings)
allow you to customize some aspects of GitLab to suit yourself.
+## Creating users
+
+There are several ways to create users on GitLab. See the [creating users documentation](account/create_accounts.md) for more details.
+
## Signing in
There are several ways to sign into your GitLab account.
diff --git a/doc/user/project/integrations/github.md b/doc/user/project/integrations/github.md
index d0399f9193b..1a2c45a3b33 100644
--- a/doc/user/project/integrations/github.md
+++ b/doc/user/project/integrations/github.md
@@ -30,19 +30,18 @@ with `repo:status` access granted:
1. Select the "Active" checkbox.
1. Paste the token you've generated on GitHub
1. Enter the path to your project on GitHub, such as `https://github.com/username/repository`
-1. Optionally check "Static status check names" checkbox to enable static status check names.
+1. Optionally uncheck **Static status check names** checkbox to disable static status check names.
1. Save or optionally click "Test Settings".
#### Static / dynamic status check names
-Since GitLab 11.5 it is possible to opt-in to using static status check names.
+> - Introduced in GitLab 11.5: using static status check names as opt-in option.
+> - [In GitLab 12.4](https://gitlab.com/gitlab-org/gitlab-ee/issues/9931), static status check names is default behavior for new projects.
This makes it possible to mark these status checks as _Required_ on GitHub.
-If you check "Static status check names" checkbox on the integration page, your
+With **Static status check names** enabled on the integration page, your
GitLab instance host name is going to be appended to a status check name,
whereas in case of dynamic status check names, a branch name is going to be
appended.
-Dynamic status check name is a default behavior.
-
![Configure GitHub Project Integration](img/github_configuration.png)
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 3583c0554ee..56176da382f 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -38,6 +38,23 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma
![Managed Prometheus Deploy](img/prometheus_deploy.png)
+#### Getting metrics to display on the Metrics Dashboard
+
+After completing the steps above, you will also need deployments in order to view the
+**Operations > Metrics** page. Setting up [Auto DevOps](../../../topics/autodevops/index.md)
+will help you to quickly create a deployment:
+
+1. Navigate to your project's **Operations > Kubernetes** page, and ensure that,
+ in addition to "Prometheus" and "Helm Tiller", you also have "Runner" and "Ingress"
+ installed. Once "Ingress" is installed, copy its endpoint.
+1. Navigate to your project's **Settings > CI/CD** page. In the Auto DevOps section,
+ select a deployment strategy and save your changes.
+1. On the same page, in the Variables section, add a variable named `KUBE_INGRESS_BASE_DOMAIN`
+ with the value of the Ingress endpoint you have copied in the previous step. Leave the type
+ as "Variable".
+1. Navigate to your project's **CI/CD > Pipelines** page, and run a pipeline on any branch.
+1. When the pipeline has run successfully, graphs will be available on the **Operations > Metrics** page.
+
#### About managed Prometheus deployments
Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/).
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index f5bc6cbd988..b508d904606 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1001,6 +1001,7 @@ X-Gitlab-Event: Pipeline Hook
"tag": false,
"sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
"before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "source": "merge_request_event",
"status": "success",
"stages":[
"build",
@@ -1017,6 +1018,18 @@ X-Gitlab-Event: Pipeline Hook
}
]
},
+ "merge_request": {
+ "id": 1,
+ "iid": 1,
+ "title": "Test",
+ "source_branch": "test",
+ "source_project_id": 1,
+ "target_branch": "master",
+ "target_project_id": 1,
+ "state": "opened",
+ "merge_status": "can_be_merged",
+ "url": "http://192.168.64.1:3005/gitlab-org/gitlab-test/merge_requests/1"
+ },
"user":{
"name": "Administrator",
"username": "root",
@@ -1327,3 +1340,6 @@ console:
example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
- -> /
```
+
+NOTE: **Note:**
+You may need to [allow requests to the local network](../../../security/webhooks.md) for this receiver to be added.
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 21016dca358..2f8394eb104 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -68,22 +68,26 @@ invitation, change their access level, or even delete them.
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
-## Request access to a project
+## Project membership and requesting access
-As a project owner you can enable or disable non members to request access to
-your project. Go to the project settings and click on **Allow users to request access**.
+Project owners can :
-As a user, you can request to be a member of a project. Go to the project you'd
-like to be a member of, and click the **Request Access** button on the right
+- Allow non-members to request access to the project.
+- Prevent non-members from requesting access.
+
+To configure this, go to the project settings and click on **Allow users to request access**.
+
+GitLab users can request to become a member of a project. Go to the project you'd
+like to be a member of and click the **Request Access** button on the right
side of your screen.
![Request access button](img/request_access_button.png)
-Once access is requested:
+After access is requested:
-- Up to ten project maintainers are notified of your request via email.
+- Up to ten project maintainers are notified of the request via email.
Email is sent to the most recently active project maintainers.
-- Any project maintainer can approve or decline your request on the members page.
+- Any project maintainer can approve or decline the request on the members page.
NOTE: **Note:**
If a project does not have any maintainers, the notification is sent to the
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 3c6b660c63d..e7ecd173597 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -1,6 +1,5 @@
---
type: reference, howto
-disqus_identifier: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html'
---
# Code Quality **(STARTER)**
@@ -18,7 +17,7 @@ Code Quality:
- Runs in [pipelines](../../../ci/pipelines.md) using an Docker image built in
[GitLab Code
Quality](https://gitlab.com/gitlab-org/security-products/codequality) project.
-- Can make use of a [template](#template-and-examples).
+- Can make use of a [template](#example-configuration).
- Is available with [Auto
DevOps](../../../topics/autodevops/index.md#auto-code-quality-starter).
@@ -42,14 +41,112 @@ For instance, consider the following workflow:
1. You approve the merge request and authorize its deployment to staging.
1. Once verified, their changes are deployed to production.
-## Template and examples
+## Example configuration
-For most GitLab instances, the supplied template is the preferred method of
-implementing Code Quality. See
-[Analyze your project's Code Quality](../../../ci/examples/code_quality.md) for:
+CAUTION: **Caution:**
+The job definition shown below is supported on GitLab 11.11 and later versions. It
+also requires the GitLab Runner 11.5 or later. For earlier versions, use the
+[previous job definitions](#previous-job-definitions).
-- Information on the builtin GitLab Code Quality template.
-- Examples of manual GitLab configuration for earlier GitLab versions.
+This example shows how to run Code Quality on your code by using GitLab CI/CD and Docker.
+
+First, you need GitLab Runner with
+[docker-in-docker executor](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+
+Once you set up the Runner, include the CodeQuality template in your CI config:
+
+```yaml
+include:
+ - template: Code-Quality.gitlab-ci.yml
+```
+
+The above example will create a `code_quality` job in your CI/CD pipeline which
+will scan your source code for code quality issues. The report will be saved as a
+[Code Quality report artifact](../../../ci/yaml/README.md#artifactsreportscodequality-starter)
+that you can later download and analyze. Due to implementation limitations we always
+take the latest Code Quality artifact available.
+
+TIP: **Tip:**
+This information will be automatically extracted and shown right in the merge request widget.
+
+CAUTION: **Caution:**
+On self-managed instances, if a malicious actor compromises the Code Quality job
+definition they will be able to execute privileged docker commands on the Runner
+host. Having proper access control policies mitigates this attack vector by
+allowing access only to trusted actors.
+
+### Previous job definitions
+
+CAUTION: **Caution:**
+Before GitLab 11.5, Code Quality job and artifact had to be named specifically to
+automatically extract report data and show it in the merge request widget. While these
+old job definitions are still maintained they have been deprecated and may be removed
+in the next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
+configuration to reflect that change.
+
+For GitLab 11.5 and earlier, the job should look like:
+
+```yaml
+code_quality:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ artifacts:
+ reports:
+ codequality: gl-code-quality-report.json
+```
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+code_quality:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ artifacts:
+ paths: [gl-code-quality-report.json]
+```
+
+Alternatively the job name could be `codeclimate` or `codequality` and the artifact
+name could be `codeclimate.json`. These names have been deprecated with GitLab 11.0
+and may be removed in the next major release, GitLab 12.0.
+
+For GitLab 10.3 and earlier, the job should look like:
+
+```yaml
+codequality:
+ image: docker:latest
+ variables:
+ DOCKER_DRIVER: overlay
+ services:
+ - docker:dind
+ script:
+ - docker pull codeclimate/codeclimate:0.69.0
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init
+ - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true
+ artifacts:
+ paths: [codeclimate.json]
+```
## Configuring jobs using variables
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index aa58e971cc3..c19e96b4482 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -630,10 +630,7 @@ troubleshooting steps.
### Merge request cannot retrieve the pipeline status
-This can occur for one of two reasons:
-
-- Sidekiq doesn't pick up the changes fast enough
-- Because of the bug described in [#41545](https://gitlab.com/gitlab-org/gitlab-ce/issues/41545)
+This can occur if Sidekiq doesn't pick up the changes fast enough.
#### Sidekiq
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index e9869e39b79..a18c2d3a21f 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -184,6 +184,10 @@ the merge request. To enable this feature:
When this feature is enabled, all merge requests will need approval
from one code owner per matched rule before it can be merged.
+NOTE: **Note:** Only the `CODEOWNERS` file on the default branch is evaluated for
+Merge Request approvals. If `CODEOWNERS` is changed on a non-default branch, those
+changes will not affect approvals until merged to the default branch.
+
## Overriding the merge request approvals default settings
> Introduced in GitLab Enterprise Edition 9.4.
diff --git a/doc/user/project/operations/tracing.md b/doc/user/project/operations/tracing.md
index 3fb3be3c21f..91d0ae9d272 100644
--- a/doc/user/project/operations/tracing.md
+++ b/doc/user/project/operations/tracing.md
@@ -26,7 +26,7 @@ and [OpenShift](https://github.com/jaegertracing/jaeger-openshift).
GitLab provides an easy way to open the Jaeger UI from within your project:
-1. [Set up Jaeger](#deploying-jaeger) and configure your application using one of the
+1. [Set up Jaeger](https://www.jaegertracing.io) and configure your application using one of the
[client libraries](https://www.jaegertracing.io/docs/latest/client-libraries/).
1. Navigate to your project's **Settings > Operations** and provide the Jaeger URL.
1. Click **Save changes** for the changes to take effect.
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index d844d4222b1..80fa64b162d 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -53,9 +53,9 @@ write in the `.gitlab-ci.yml` the script you want to run so
GitLab Runner will do it for you. It looks more complicated than it
is. What you need to tell the Runner:
-```
-$ gem install jekyll
-$ jekyll build
+```sh
+gem install jekyll
+jekyll build
```
### Script
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 895c8ac88e7..1de3e3b5387 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -5,9 +5,8 @@ type: reference, howto
# Protected Branches
[Permissions](../permissions.md) in GitLab are fundamentally defined around the
-idea of having read or write permission to the repository and branches. To
-prevent people from messing with history or pushing code without review, we've
-created protected branches.
+idea of having read or write permission to the repository and branches. To impose
+further restrictions on certain branches, they can be protected.
## Overview
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 58ccd8bf2ae..e0fb5c57784 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -32,6 +32,8 @@ links will be missing from the sidebar UI.
You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see
Issues or Merge Requests, both of which use Labels and Milestones, then you shouldn't be denied access to Labels and Milestones pages.
+Project [Snippets](../../snippets.md) are enabled by default.
+
#### Disabling email notifications
You can disable all email notifications related to the project by selecting the
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 7b580a057f2..74f58a1a92b 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -33,6 +33,11 @@ overview that shows snippets you created and allows you to explore all snippets.
If you want to discover snippets that belong to a specific project, you can navigate
to the Snippets page via the left side navigation on the project page.
+Project snippets are enabled and available by default, but they can
+be disabled by navigating to your project's **Settings**, expanding
+**Visibility, project features, permissions** and scrolling down to
+**Snippets**. From there, you can toggle to disable them or select a
+different visibility level from the dropdown menu.
## Snippet comments
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2f365e42cc9..136e05281a6 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -1,4 +1,3 @@
-
# Introduction to GitLab Flow
![GitLab Flow](img/gitlab_flow.png)
@@ -60,7 +59,7 @@ For example, many projects do releases but don't need to do hotfixes.
In reaction to Git flow, GitHub created a simpler alternative.
[GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch.
This flow is clean and straightforward, and many organizations have adopted it with great success.
-Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/archives/simple-git-workflow-simple), although they rebase feature branches.
+Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/git/simple-git-workflow-is-simple), although they rebase feature branches.
Merging everything into the `master` branch and frequently deploying means you minimize the amount of unreleased code, which is in line with lean and continuous delivery best practices.
However, this flow still leaves a lot of questions unanswered regarding deployments, environments, releases, and integrations with issues.
With GitLab flow, we offer additional guidance for these questions.
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 7e880b3009d..8db62997b51 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -134,13 +134,13 @@ There are two ways to manually do the same thing as automatic uploading (describ
**Option 1: rake task**
-```
-$ rake gitlab:lfs:migrate
+```sh
+rake gitlab:lfs:migrate
```
**Option 2: rails console**
-```
+```sh
$ sudo gitlab-rails console # Login to rails console
> # Upload LFS files manually
diff --git a/lib/api/entities/internal.rb b/lib/api/entities/internal.rb
new file mode 100644
index 00000000000..8f79bd14833
--- /dev/null
+++ b/lib/api/entities/internal.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Internal
+ module Pages
+ class LookupPath < Grape::Entity
+ expose :project_id, :access_control,
+ :source, :https_only, :prefix
+ end
+
+ class VirtualDomain < Grape::Entity
+ expose :certificate, :key
+ expose :lookup_paths, using: LookupPath
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 5755f4b8d74..e61b25721fb 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -5,10 +5,10 @@ module API
include Gitlab::Utils
include Helpers::Pagination
- SUDO_HEADER = "HTTP_SUDO".freeze
- GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret".freeze
+ SUDO_HEADER = "HTTP_SUDO"
+ GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret"
SUDO_PARAM = :sudo
- API_USER_ENV = 'gitlab.api.user'.freeze
+ API_USER_ENV = 'gitlab.api.user'
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 51b7cf05c8f..c1e7af33235 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -38,7 +38,8 @@ module API
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
- optional :avatar, type: File, desc: 'Avatar image for project'
+ # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960
+ optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads
optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 5b87eccf860..41ee34a8fdf 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -3,7 +3,7 @@
module API
module Helpers
module Runner
- JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
+ JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
JOB_TOKEN_PARAM = :token
def runner_registration_token_valid?
diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb
index 6ea048bde03..eaa434cff51 100644
--- a/lib/api/internal/pages.rb
+++ b/lib/api/internal/pages.rb
@@ -18,7 +18,12 @@ module API
namespace 'internal' do
namespace 'pages' do
get "/" do
- status :ok
+ host = PagesDomain.find_by_domain(params[:host])
+ not_found! unless host
+
+ virtual_domain = host.pages_virtual_domain
+
+ present virtual_domain, with: Entities::Internal::Pages::VirtualDomain
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 64ee82cd775..4c092f10729 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -317,6 +317,26 @@ module API
present paginate(pipelines), with: Entities::PipelineBasic
end
+ desc 'Create a pipeline for merge request' do
+ success Entities::Pipeline
+ end
+ post ':id/merge_requests/:merge_request_iid/pipelines' do
+ authorize! :create_pipeline, user_project
+
+ pipeline = ::MergeRequests::CreatePipelineService
+ .new(user_project, current_user, allow_duplicate: true)
+ .execute(find_merge_request_with_access(params[:merge_request_iid]))
+
+ if pipeline.nil?
+ not_allowed!
+ elsif pipeline.persisted?
+ status :ok
+ present pipeline, with: Entities::Pipeline
+ else
+ render_validation_error!(pipeline)
+ end
+ end
+
desc 'Update a merge request' do
success Entities::MergeRequest
end
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index 4227a106a95..40b133e8959 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -90,8 +90,11 @@ module API
end
params do
requires :domain, type: String, desc: 'The domain'
+ # rubocop:disable Scalability/FileUploads
+ # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key
+ # rubocop:enable Scalability/FileUploads
all_or_none_of :user_provided_certificate, :user_provided_key
end
post ":id/pages/domains" do
@@ -111,8 +114,11 @@ module API
desc 'Updates a pages domain'
params do
requires :domain, type: String, desc: 'The domain'
+ # rubocop:disable Scalability/FileUploads
+ # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960
optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key
+ # rubocop:enable Scalability/FileUploads
end
put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
authorize! :update_pages, user_project
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index bb1b037c08f..9b5e0727184 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -27,7 +27,8 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :path, type: String, desc: 'The new project path and name'
- requires :file, type: File, desc: 'The project export file to be imported'
+ # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960
+ requires :file, type: File, desc: 'The project export file to be imported' # rubocop:disable Scalability/FileUploads
optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace."
optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it'
optional :override_params,
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 3073c14b341..63bfa8db61c 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -478,7 +478,8 @@ module API
desc 'Upload a file'
params do
- requires :file, type: File, desc: 'The file to be uploaded'
+ # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960
+ requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads
end
post ":id/uploads" do
UploadService.new(user_project, params[:file]).execute.to_h
diff --git a/lib/api/users.rb b/lib/api/users.rb
index a4ac5b629b8..99295888c8c 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -50,7 +50,8 @@ module API
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
- optional :avatar, type: File, desc: 'Avatar image for user'
+ # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab-ee/issues/14960
+ optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads
optional :private_profile, type: Boolean, default: false, desc: 'Flag indicating the user has a private profile'
all_or_none_of :extern_uid, :provider
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index aeaf61cda39..c0390959269 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -4,7 +4,7 @@ module Backup
class Manager
ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry].freeze
FOLDERS_TO_BACKUP = %w[repositories db].freeze
- FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
+ FILE_NAME_SUFFIX = '_gitlab_backup.tar'
attr_reader :progress
diff --git a/lib/banzai/filter/color_filter.rb b/lib/banzai/filter/color_filter.rb
index 6d9bdb9cbd3..0aca7441638 100644
--- a/lib/banzai/filter/color_filter.rb
+++ b/lib/banzai/filter/color_filter.rb
@@ -5,7 +5,7 @@ module Banzai
# HTML filter that renders `color` followed by a color "chip".
#
class ColorFilter < HTML::Pipeline::Filter
- COLOR_CHIP_CLASS = 'gfm-color_chip'.freeze
+ COLOR_CHIP_CLASS = 'gfm-color_chip'
def call
doc.css('code').each do |node|
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index fb721fe12b1..67019454e44 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -5,8 +5,8 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
SCHEMES = ['http', 'https', nil].freeze
- RTLO = "\u202E".freeze
- ENCODED_RTLO = '%E2%80%AE'.freeze
+ RTLO = "\u202E"
+ ENCODED_RTLO = '%E2%80%AE'
def call
links.each do |node|
diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb
index de133774dfa..5474242e03c 100644
--- a/lib/banzai/filter/footnote_filter.rb
+++ b/lib/banzai/filter/footnote_filter.rb
@@ -17,8 +17,8 @@ module Banzai
#
class FootnoteFilter < HTML::Pipeline::Filter
INTEGER_PATTERN = /\A\d+\z/.freeze
- FOOTNOTE_ID_PREFIX = 'fn'.freeze
- FOOTNOTE_LINK_ID_PREFIX = 'fnref'.freeze
+ FOOTNOTE_ID_PREFIX = 'fn'
+ FOOTNOTE_LINK_ID_PREFIX = 'fnref'
FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_START_NUMBER = 1
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index 8dd5a8979c8..c915f0ee35b 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -11,14 +11,14 @@ module Banzai
#
class MathFilter < HTML::Pipeline::Filter
# Attribute indicating inline or display math.
- STYLE_ATTRIBUTE = 'data-math-style'.freeze
+ STYLE_ATTRIBUTE = 'data-math-style'
# Class used for tagging elements that should be rendered
- TAG_CLASS = 'js-render-math'.freeze
+ TAG_CLASS = 'js-render-math'
- INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
+ INLINE_CLASSES = "code math #{TAG_CLASS}"
- DOLLAR_SIGN = '$'.freeze
+ DOLLAR_SIGN = '$'
def call
doc.css('code').each do |code|
diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb
index 3d40abfc540..ae093580001 100644
--- a/lib/banzai/filter/suggestion_filter.rb
+++ b/lib/banzai/filter/suggestion_filter.rb
@@ -5,7 +5,7 @@ module Banzai
module Filter
class SuggestionFilter < HTML::Pipeline::Filter
# Class used for tagging elements that should be rendered
- TAG_CLASS = 'js-render-suggestion'.freeze
+ TAG_CLASS = 'js-render-suggestion'
def call
return doc unless suggestions_filter_enabled?
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 9b66759a5fb..c1a5f33c225 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -10,8 +10,8 @@ module Banzai
class SyntaxHighlightFilter < HTML::Pipeline::Filter
include OutputSafety
- PARAMS_DELIMITER = ':'.freeze
- LANG_PARAMS_ATTR = 'data-lang-params'.freeze
+ PARAMS_DELIMITER = ':'
+ LANG_PARAMS_ATTR = 'data-lang-params'
def call
doc.search('pre:not([data-math-style]) > code').each do |node|
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index 341dbb74fe0..31bb4f2e01c 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -11,8 +11,8 @@ module Banzai
class IssuableExtractor
attr_reader :context
- ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'.freeze
- MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'.freeze
+ ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'
+ MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'
# context - An instance of Banzai::RenderContext.
def initialize(context)
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index 0041634f9e3..9937236fc44 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -2,8 +2,8 @@
module Bitbucket
class Connection
- DEFAULT_API_VERSION = '2.0'.freeze
- DEFAULT_BASE_URI = 'https://api.bitbucket.org/'.freeze
+ DEFAULT_API_VERSION = '2.0'
+ DEFAULT_BASE_URI = 'https://api.bitbucket.org/'
DEFAULT_QUERY = {}.freeze
attr_reader :expires_at, :expires_in, :refresh_token, :token
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index e8b938e46b1..582541c0679 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -29,13 +29,13 @@ module Gitlab
if result.status.success?
result.stdout.chomp.freeze
else
- "Unknown".freeze
+ "Unknown"
end
end
end
end
- COM_URL = 'https://gitlab.com'.freeze
+ COM_URL = 'https://gitlab.com'
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}.freeze
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze
VERSION = File.read(root.join("VERSION")).strip.freeze
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index 176766d1a8b..aca8804b04c 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -24,7 +24,9 @@ module Gitlab
end
def find_sessionless_user(request_format)
- find_user_from_web_access_token(request_format) || find_user_from_feed_token(request_format)
+ find_user_from_web_access_token(request_format) ||
+ find_user_from_feed_token(request_format) ||
+ find_user_from_static_object_token(request_format)
rescue Gitlab::Auth::AuthenticationError
nil
end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index 97755117edc..76d41eede23 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -28,6 +28,15 @@ module Gitlab
current_request.env['warden']&.authenticate if verified_request?
end
+ def find_user_from_static_object_token(request_format)
+ return unless valid_static_objects_format?(request_format)
+
+ token = current_request.params[:token].presence || current_request.headers['X-Gitlab-Static-Object-Token'].presence
+ return unless token
+
+ User.find_by_static_object_token(token) || raise(UnauthorizedError)
+ end
+
def find_user_from_feed_token(request_format)
return unless valid_rss_format?(request_format)
@@ -154,6 +163,15 @@ module Gitlab
end
end
+ def valid_static_objects_format?(request_format)
+ case request_format
+ when :archive
+ archive_request?
+ else
+ false
+ end
+ end
+
def rss_request?
current_request.path.ends_with?('.atom') || current_request.format.atom?
end
@@ -165,6 +183,10 @@ module Gitlab
def api_request?
current_request.path.starts_with?("/api/")
end
+
+ def archive_request?
+ current_request.path.include?('/-/archive/')
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids.rb b/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids.rb
new file mode 100644
index 00000000000..1a80ccdee92
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This migration updates discussion ids for epics that were promoted from issue so that the discussion id on epics
+ # is different from discussion id on issue, which was causing problems when replying to epic discussions as it would
+ # identify the discussion as related to an issue and complaint about missing project_id
+ class FixPromotedEpicsDiscussionIds
+ # notes model to iterate through the notes to be updated
+ class Note < ActiveRecord::Base
+ self.table_name = 'notes'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def perform(discussion_ids)
+ Note.where(noteable_type: 'Epic')
+ .where(discussion_id: discussion_ids)
+ .update_all("discussion_id=MD5(discussion_id)||substring(discussion_id from 1 for 8)")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index e1e813849bf..da3d6c47431 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -9,6 +9,7 @@ module Gitlab
{
object_kind: 'pipeline',
object_attributes: hook_attrs(pipeline),
+ merge_request: pipeline.merge_request && merge_request_attrs(pipeline.merge_request),
user: pipeline.user.try(:hook_attrs),
project: pipeline.project.hook_attrs(backward: false),
commit: pipeline.commit.try(:hook_attrs),
@@ -23,6 +24,7 @@ module Gitlab
tag: pipeline.tag,
sha: pipeline.sha,
before_sha: pipeline.before_sha,
+ source: pipeline.source,
status: pipeline.status,
detailed_status: pipeline.detailed_status(nil).label,
stages: pipeline.stages_names,
@@ -33,6 +35,21 @@ module Gitlab
}
end
+ def merge_request_attrs(merge_request)
+ {
+ id: merge_request.id,
+ iid: merge_request.iid,
+ title: merge_request.title,
+ source_branch: merge_request.source_branch,
+ source_project_id: merge_request.source_project_id,
+ target_branch: merge_request.target_branch,
+ target_project_id: merge_request.target_project_id,
+ state: merge_request.state,
+ merge_status: merge_request.merge_status,
+ url: Gitlab::UrlBuilder.build(merge_request)
+ }
+ end
+
def build_hook_attrs(build)
{
id: build.id,
diff --git a/lib/gitlab/database/obsolete_ignored_columns.rb b/lib/gitlab/database/obsolete_ignored_columns.rb
new file mode 100644
index 00000000000..6266b6a4b65
--- /dev/null
+++ b/lib/gitlab/database/obsolete_ignored_columns.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # Checks which `ignored_columns` can be safely removed by scanning
+ # the current schema for all `ApplicationRecord` descendants.
+ class ObsoleteIgnoredColumns
+ def initialize(base = ApplicationRecord)
+ @base = base
+ end
+
+ def execute
+ @base.descendants.map do |klass|
+ next if klass.abstract_class?
+
+ safe_to_remove = ignored_columns_safe_to_remove_for(klass)
+ next if safe_to_remove.empty?
+
+ [klass.name, safe_to_remove]
+ end.compact.sort_by(&:first)
+ end
+
+ private
+
+ def ignored_columns_safe_to_remove_for(klass)
+ ignored = klass.ignored_columns.map(&:to_s)
+
+ return [] if ignored.empty?
+
+ schema = klass.connection.schema_cache.columns_hash(klass.table_name)
+ existing = schema.values.map(&:name)
+
+ used = ignored & existing
+ ignored - used
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb
index 3e9a035010f..c0687cd9b79 100644
--- a/lib/gitlab/i18n/po_linter.rb
+++ b/lib/gitlab/i18n/po_linter.rb
@@ -170,13 +170,13 @@ module Gitlab
end
def translate_plural(entry)
- used_variables = entry.plural_id.scan(VARIABLE_REGEX)
- variables = fill_in_variables(used_variables)
-
numbers_covering_all_plurals.map do |number|
translation = FastGettext::Translation.n_(entry.msgid, entry.plural_id, number)
+ index = index_for_pluralization(number)
+ used_variables = index == 0 ? entry.msgid.scan(VARIABLE_REGEX) : entry.plural_id.scan(VARIABLE_REGEX)
+ variables = fill_in_variables(used_variables)
- translation % variables if used_variables.any?
+ translation % variables if variables.any?
end
end
diff --git a/lib/gitlab/issuable_metadata.rb b/lib/gitlab/issuable_metadata.rb
index be73bcd5506..6f760751b0f 100644
--- a/lib/gitlab/issuable_metadata.rb
+++ b/lib/gitlab/issuable_metadata.rb
@@ -19,7 +19,7 @@ module Gitlab
return {} if issuable_ids.empty?
- issuable_note_count = ::Note.count_for_collection(issuable_ids, collection_type)
+ issuable_notes_count = ::Note.count_for_collection(issuable_ids, collection_type)
issuable_votes_count = ::AwardEmoji.votes_for_collection(issuable_ids, collection_type)
issuable_merge_requests_count =
if collection_type == 'Issue'
@@ -31,7 +31,7 @@ module Gitlab
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
- notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+ notes = issuable_notes_count.find { |notes| notes.noteable_id == id }
merge_requests = issuable_merge_requests_count.find { |mr| mr.first == id }
issuable_meta[id] = ::Issuable::IssuableMeta.new(
diff --git a/lib/gitlab/noteable_metadata.rb b/lib/gitlab/noteable_metadata.rb
new file mode 100644
index 00000000000..f3f8933b81f
--- /dev/null
+++ b/lib/gitlab/noteable_metadata.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module NoteableMetadata
+ def noteable_meta_data(noteable_collection, collection_type)
+ # ActiveRecord uses Object#extend for null relations.
+ if !(noteable_collection.singleton_class < ActiveRecord::NullRelation) &&
+ noteable_collection.respond_to?(:limit_value) &&
+ noteable_collection.limit_value.nil?
+
+ raise 'Collection must have a limit applied for preloading meta-data'
+ end
+
+ # map has to be used here since using pluck or select will
+ # throw an error when ordering noteables which inserts
+ # a new order into the collection.
+ # We cannot use reorder to not mess up the paginated collection.
+ noteable_ids = noteable_collection.map(&:id)
+
+ return {} if noteable_ids.empty?
+
+ noteable_notes_count = ::Note.count_for_collection(noteable_ids, collection_type)
+
+ noteable_ids.each_with_object({}) do |id, noteable_meta|
+ notes = noteable_notes_count.find { |notes| notes.noteable_id == id }
+
+ noteable_meta[id] = ::Noteable::NoteableMeta.new(
+ notes.try(:count).to_i
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/patch/chronic_duration.rb b/lib/gitlab/patch/chronic_duration.rb
deleted file mode 100644
index ab3cba3657f..00000000000
--- a/lib/gitlab/patch/chronic_duration.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-# Fixes a bug where parsing months doesn't take into account
-# the ChronicDuration.days_per_week setting
-#
-# We can remove this when we do a refactor and push upstream in
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/66637
-
-module Gitlab
- module Patch
- module ChronicDuration
- extend ActiveSupport::Concern
-
- class_methods do
- def duration_units_seconds_multiplier(unit)
- return 0 unless duration_units_list.include?(unit)
-
- case unit
- when 'months'
- 3600 * ::ChronicDuration.hours_per_day * ::ChronicDuration.days_per_month
- else
- super
- end
- end
-
- # ChronicDuration#output uses 1mo = 4w as the conversion so we do the same here.
- # We do need to add a special case for the default days_per_week value because
- # we want to retain existing behavior for the default case
- def days_per_month
- ::ChronicDuration.days_per_week == 7 ? 30 : ::ChronicDuration.days_per_week * 4
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index e40c366ed02..b2dc92ce010 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -23,6 +23,49 @@ module Gitlab
end
end
+ # Caches and strongly memoizes the method as a Redis Set.
+ #
+ # This only works for methods that do not take any arguments. The method
+ # should return an Array of Strings to be cached.
+ #
+ # In addition to overriding the named method, a "name_include?" method is
+ # defined. This uses the "SISMEMBER" query to efficiently check membership
+ # without needing to load the entire set into memory.
+ #
+ # name - The name of the method to be cached.
+ # fallback - A value to fall back to if the repository does not exist, or
+ # in case of a Git error. Defaults to nil.
+ #
+ # It is not safe to use this method prior to the release of 12.3, since
+ # 12.2 does not correctly invalidate the redis set cache value. A mixed
+ # code environment containing both 12.2 and 12.3 nodes breaks, while a
+ # mixed code environment containing both 12.3 and 12.4 nodes will work.
+ def cache_method_as_redis_set(name, fallback: nil)
+ uncached_name = alias_uncached_method(name)
+
+ define_method(name) do
+ cache_method_output_as_redis_set(name, fallback: fallback) do
+ __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ # Attempt to determine whether a value is in the set of values being
+ # cached, by performing a redis SISMEMBERS query if appropriate.
+ #
+ # If the full list is already in-memory, we're better using it directly.
+ #
+ # If the cache is not yet populated, querying it directly will give the
+ # wrong answer. We handle that by querying the full list - which fills
+ # the cache - and using it directly to answer the question.
+ define_method("#{name}_include?") do |value|
+ if strong_memoized?(name) || !redis_set_cache.exist?(name)
+ return __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ redis_set_cache.include?(name, value)
+ end
+ end
+
# Caches truthy values from the method. All values are strongly memoized,
# and cached in RequestStore.
#
@@ -84,6 +127,11 @@ module Gitlab
raise NotImplementedError
end
+ # RepositorySetCache to be used. Should be overridden by the including class
+ def redis_set_cache
+ raise NotImplementedError
+ end
+
# List of cached methods. Should be overridden by the including class
def cached_methods
raise NotImplementedError
@@ -100,6 +148,18 @@ module Gitlab
end
end
+ # Caches and strongly memoizes the supplied block as a Redis Set. The result
+ # will be provided as a sorted array.
+ #
+ # name - The name of the method to be cached.
+ # fallback - A value to fall back to if the repository does not exist, or
+ # in case of a Git error. Defaults to nil.
+ def cache_method_output_as_redis_set(name, fallback: nil, &block)
+ memoize_method_output(name, fallback: fallback) do
+ redis_set_cache.fetch(name, &block).sort
+ end
+ end
+
# Caches truthy values from the supplied block. All values are strongly
# memoized, and cached in RequestStore.
#
@@ -154,6 +214,7 @@ module Gitlab
clear_memoization(memoizable_name(name))
end
+ expire_redis_set_method_caches(methods)
expire_request_store_method_caches(methods)
end
@@ -169,6 +230,10 @@ module Gitlab
end
end
+ def expire_redis_set_method_caches(methods)
+ methods.each { |name| redis_set_cache.expire(name) }
+ end
+
# All cached repository methods depend on the existence of a Git repository,
# so if the repository doesn't exist, we already know not to call it.
def fallback_early?(method_name)
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
new file mode 100644
index 00000000000..6d3ac53a787
--- /dev/null
+++ b/lib/gitlab/repository_set_cache.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# Interface to the Redis-backed cache store for keys that use a Redis set
+module Gitlab
+ class RepositorySetCache
+ attr_reader :repository, :namespace, :expires_in
+
+ def initialize(repository, extra_namespace: nil, expires_in: 2.weeks)
+ @repository = repository
+ @namespace = "#{repository.full_path}:#{repository.project.id}"
+ @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace
+ @expires_in = expires_in
+ end
+
+ def cache_key(type)
+ "#{type}:#{namespace}:set"
+ end
+
+ def expire(key)
+ with { |redis| redis.del(cache_key(key)) }
+ end
+
+ def exist?(key)
+ with { |redis| redis.exists(cache_key(key)) }
+ end
+
+ def read(key)
+ with { |redis| redis.smembers(cache_key(key)) }
+ end
+
+ def write(key, value)
+ full_key = cache_key(key)
+
+ with do |redis|
+ redis.multi do
+ redis.del(full_key)
+
+ # Splitting into groups of 1000 prevents us from creating a too-long
+ # Redis command
+ value.each_slice(1000) { |subset| redis.sadd(full_key, subset) }
+
+ redis.expire(full_key, expires_in)
+ end
+ end
+
+ value
+ end
+
+ def fetch(key, &block)
+ if exist?(key)
+ read(key)
+ else
+ write(key, yield)
+ end
+ end
+
+ def include?(key, value)
+ with { |redis| redis.sismember(cache_key(key), value) }
+ end
+
+ private
+
+ def with(&blk)
+ Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb
new file mode 100644
index 00000000000..bbfca130425
--- /dev/null
+++ b/lib/gitlab/sidekiq_daemon/monitor.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqDaemon
+ class Monitor < Daemon
+ include ::Gitlab::Utils::StrongMemoize
+
+ NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'
+ CANCEL_DEADLINE = 24.hours.seconds
+ RECONNECT_TIME = 3.seconds
+
+ # We use exception derived from `Exception`
+ # to consider this as an very low-level exception
+ # that should not be caught by application
+ CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException
+
+ attr_reader :jobs_thread
+ attr_reader :jobs_mutex
+
+ def initialize
+ super
+
+ @jobs_thread = {}
+ @jobs_mutex = Mutex.new
+ end
+
+ def within_job(jid, queue)
+ jobs_mutex.synchronize do
+ jobs_thread[jid] = Thread.current
+ end
+
+ if cancelled?(jid)
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'run',
+ queue: queue,
+ jid: jid,
+ canceled: true
+ )
+ raise CancelledError
+ end
+
+ yield
+ ensure
+ jobs_mutex.synchronize do
+ jobs_thread.delete(jid)
+ end
+ end
+
+ def self.cancel_job(jid)
+ payload = {
+ action: 'cancel',
+ jid: jid
+ }.to_json
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1)
+ redis.publish(NOTIFICATION_CHANNEL, payload)
+ end
+ end
+
+ private
+
+ def start_working
+ Sidekiq.logger.info(
+ class: self.class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon'
+ )
+
+ while enabled?
+ process_messages
+ sleep(RECONNECT_TIME)
+ end
+
+ ensure
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'stop',
+ message: 'Stopping Monitor Daemon'
+ )
+ end
+
+ def stop_working
+ thread.raise(Interrupt) if thread.alive?
+ end
+
+ def process_messages
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.subscribe(NOTIFICATION_CHANNEL) do |on|
+ on.message do |channel, message|
+ process_message(message)
+ end
+ end
+ end
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'exception',
+ message: e.message
+ )
+
+ # we re-raise system exceptions
+ raise e unless e.is_a?(StandardError)
+ end
+
+ def process_message(message)
+ Sidekiq.logger.info(
+ class: self.class.to_s,
+ channel: NOTIFICATION_CHANNEL,
+ message: 'Received payload on channel',
+ payload: message
+ )
+
+ message = safe_parse(message)
+ return unless message
+
+ case message['action']
+ when 'cancel'
+ process_job_cancel(message['jid'])
+ else
+ # unknown message
+ end
+ end
+
+ def safe_parse(message)
+ JSON.parse(message)
+ rescue JSON::ParserError
+ end
+
+ def process_job_cancel(jid)
+ return unless jid
+
+ # try to find thread without lock
+ return unless find_thread_unsafe(jid)
+
+ Thread.new do
+ # try to find a thread, but with guaranteed
+ # that handle for thread corresponds to actually
+ # running job
+ find_thread_with_lock(jid) do |thread|
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'cancel',
+ message: 'Canceling thread with CancelledError',
+ jid: jid,
+ thread_id: thread.object_id
+ )
+
+ thread&.raise(CancelledError)
+ end
+ end
+ end
+
+ # This method needs to be thread-safe
+ # This is why it passes thread in block,
+ # to ensure that we do process this thread
+ def find_thread_unsafe(jid)
+ jobs_thread[jid]
+ end
+
+ def find_thread_with_lock(jid)
+ # don't try to lock if we cannot find the thread
+ return unless find_thread_unsafe(jid)
+
+ jobs_mutex.synchronize do
+ find_thread_unsafe(jid).tap do |thread|
+ yield(thread) if thread
+ end
+ end
+ end
+
+ def cancelled?(jid)
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.exists(self.class.cancel_job_key(jid))
+ end
+ end
+
+ def self.cancel_job_key(jid)
+ "sidekiq:cancel:#{jid}"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/monitor.rb b/lib/gitlab/sidekiq_middleware/monitor.rb
index 53a6132edac..00965bf5506 100644
--- a/lib/gitlab/sidekiq_middleware/monitor.rb
+++ b/lib/gitlab/sidekiq_middleware/monitor.rb
@@ -4,10 +4,10 @@ module Gitlab
module SidekiqMiddleware
class Monitor
def call(worker, job, queue)
- Gitlab::SidekiqMonitor.instance.within_job(job['jid'], queue) do
+ Gitlab::SidekiqDaemon::Monitor.instance.within_job(job['jid'], queue) do
yield
end
- rescue Gitlab::SidekiqMonitor::CancelledError
+ rescue Gitlab::SidekiqDaemon::Monitor::CancelledError
# push job to DeadSet
payload = ::Sidekiq.dump_json(job)
::Sidekiq::DeadSet.new.kill(payload, notify_failure: false)
diff --git a/lib/gitlab/sidekiq_monitor.rb b/lib/gitlab/sidekiq_monitor.rb
deleted file mode 100644
index a58b33534bf..00000000000
--- a/lib/gitlab/sidekiq_monitor.rb
+++ /dev/null
@@ -1,182 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- class SidekiqMonitor < Daemon
- include ::Gitlab::Utils::StrongMemoize
-
- NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'
- CANCEL_DEADLINE = 24.hours.seconds
- RECONNECT_TIME = 3.seconds
-
- # We use exception derived from `Exception`
- # to consider this as an very low-level exception
- # that should not be caught by application
- CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException
-
- attr_reader :jobs_thread
- attr_reader :jobs_mutex
-
- def initialize
- super
-
- @jobs_thread = {}
- @jobs_mutex = Mutex.new
- end
-
- def within_job(jid, queue)
- jobs_mutex.synchronize do
- jobs_thread[jid] = Thread.current
- end
-
- if cancelled?(jid)
- Sidekiq.logger.warn(
- class: self.class.to_s,
- action: 'run',
- queue: queue,
- jid: jid,
- canceled: true
- )
- raise CancelledError
- end
-
- yield
- ensure
- jobs_mutex.synchronize do
- jobs_thread.delete(jid)
- end
- end
-
- def self.cancel_job(jid)
- payload = {
- action: 'cancel',
- jid: jid
- }.to_json
-
- ::Gitlab::Redis::SharedState.with do |redis|
- redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1)
- redis.publish(NOTIFICATION_CHANNEL, payload)
- end
- end
-
- private
-
- def start_working
- Sidekiq.logger.info(
- class: self.class.to_s,
- action: 'start',
- message: 'Starting Monitor Daemon'
- )
-
- while enabled?
- process_messages
- sleep(RECONNECT_TIME)
- end
-
- ensure
- Sidekiq.logger.warn(
- class: self.class.to_s,
- action: 'stop',
- message: 'Stopping Monitor Daemon'
- )
- end
-
- def stop_working
- thread.raise(Interrupt) if thread.alive?
- end
-
- def process_messages
- ::Gitlab::Redis::SharedState.with do |redis|
- redis.subscribe(NOTIFICATION_CHANNEL) do |on|
- on.message do |channel, message|
- process_message(message)
- end
- end
- end
- rescue Exception => e # rubocop:disable Lint/RescueException
- Sidekiq.logger.warn(
- class: self.class.to_s,
- action: 'exception',
- message: e.message
- )
-
- # we re-raise system exceptions
- raise e unless e.is_a?(StandardError)
- end
-
- def process_message(message)
- Sidekiq.logger.info(
- class: self.class.to_s,
- channel: NOTIFICATION_CHANNEL,
- message: 'Received payload on channel',
- payload: message
- )
-
- message = safe_parse(message)
- return unless message
-
- case message['action']
- when 'cancel'
- process_job_cancel(message['jid'])
- else
- # unknown message
- end
- end
-
- def safe_parse(message)
- JSON.parse(message)
- rescue JSON::ParserError
- end
-
- def process_job_cancel(jid)
- return unless jid
-
- # try to find thread without lock
- return unless find_thread_unsafe(jid)
-
- Thread.new do
- # try to find a thread, but with guaranteed
- # that handle for thread corresponds to actually
- # running job
- find_thread_with_lock(jid) do |thread|
- Sidekiq.logger.warn(
- class: self.class.to_s,
- action: 'cancel',
- message: 'Canceling thread with CancelledError',
- jid: jid,
- thread_id: thread.object_id
- )
-
- thread&.raise(CancelledError)
- end
- end
- end
-
- # This method needs to be thread-safe
- # This is why it passes thread in block,
- # to ensure that we do process this thread
- def find_thread_unsafe(jid)
- jobs_thread[jid]
- end
-
- def find_thread_with_lock(jid)
- # don't try to lock if we cannot find the thread
- return unless find_thread_unsafe(jid)
-
- jobs_mutex.synchronize do
- find_thread_unsafe(jid).tap do |thread|
- yield(thread) if thread
- end
- end
- end
-
- def cancelled?(jid)
- ::Gitlab::Redis::SharedState.with do |redis|
- redis.exists(self.class.cancel_job_key(jid))
- end
- end
-
- def self.cancel_job_key(jid)
- "sidekiq:cancel:#{jid}"
- end
- end
-end
diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb
index 302da91328a..31883527135 100644
--- a/lib/gitlab/time_tracking_formatter.rb
+++ b/lib/gitlab/time_tracking_formatter.rb
@@ -4,37 +4,38 @@ module Gitlab
module TimeTrackingFormatter
extend self
- def parse(string)
- with_custom_config do
- string = string.sub(/\A-/, '')
+ # We may want to configure it through project settings in a future version.
+ CUSTOM_DAY_AND_WEEK_LENGTH = { hours_per_day: 8, days_per_month: 20 }.freeze
- seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil
- seconds *= -1 if seconds && Regexp.last_match
- seconds
- end
+ def parse(string)
+ string = string.sub(/\A-/, '')
+
+ seconds =
+ begin
+ ChronicDuration.parse(
+ string,
+ CUSTOM_DAY_AND_WEEK_LENGTH.merge(default_unit: 'hours'))
+ rescue
+ nil
+ end
+
+ seconds *= -1 if seconds && Regexp.last_match
+ seconds
end
def output(seconds)
- with_custom_config do
- ChronicDuration.output(seconds, format: :short, limit_to_hours: limit_to_hours_setting, weeks: true) rescue nil
- end
+ ChronicDuration.output(
+ seconds,
+ CUSTOM_DAY_AND_WEEK_LENGTH.merge(
+ format: :short,
+ limit_to_hours: limit_to_hours_setting,
+ weeks: true))
+ rescue
+ nil
end
private
- def with_custom_config
- # We may want to configure it through project settings in a future version.
- ChronicDuration.hours_per_day = 8
- ChronicDuration.days_per_week = 5
-
- result = yield
-
- ChronicDuration.hours_per_day = 24
- ChronicDuration.days_per_week = 7
-
- result
- end
-
def limit_to_hours_setting
Gitlab::CurrentSettings.time_tracking_limit_to_hours
end
diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb
index 3021a91dd83..483bfe12c68 100644
--- a/lib/gitlab/utils/strong_memoize.rb
+++ b/lib/gitlab/utils/strong_memoize.rb
@@ -24,13 +24,17 @@ module Gitlab
# end
#
def strong_memoize(name)
- if instance_variable_defined?(ivar(name))
+ if strong_memoized?(name)
instance_variable_get(ivar(name))
else
instance_variable_set(ivar(name), yield)
end
end
+ def strong_memoized?(name)
+ instance_variable_defined?(ivar(name))
+ end
+
def clear_memoization(name)
remove_instance_variable(ivar(name)) if instance_variable_defined?(ivar(name))
end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index b5f99ea012b..9f01a3f97ce 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -8,9 +8,9 @@ require 'google/apis/cloudresourcemanager_v1'
module GoogleApi
module CloudPlatform
class Client < GoogleApi::Auth
- SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
+ SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
LEAST_TOKEN_LIFE_TIME = 10.minutes
- CLUSTER_MASTER_AUTH_USERNAME = 'admin'.freeze
+ CLUSTER_MASTER_AUTH_USERNAME = 'admin'
class << self
def session_key_for_token
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index 60d79b52680..a65657dadd0 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -53,7 +53,7 @@ module Quality
end
def pattern(level)
- @patterns[level] ||= "#{prefix}spec/{#{TEST_LEVEL_FOLDERS.fetch(level).join(',')}}{,/**/}*_spec.rb".freeze
+ @patterns[level] ||= "#{prefix}spec/{#{TEST_LEVEL_FOLDERS.fetch(level).join(',')}}{,/**/}*_spec.rb"
end
def regexp(level)
diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb
index 569c41df6e4..7e2e168af8b 100644
--- a/lib/system_check/app/init_script_up_to_date_check.rb
+++ b/lib/system_check/app/init_script_up_to_date_check.rb
@@ -3,7 +3,7 @@
module SystemCheck
module App
class InitScriptUpToDateCheck < SystemCheck::BaseCheck
- SCRIPT_PATH = '/etc/init.d/gitlab'.freeze
+ SCRIPT_PATH = '/etc/init.d/gitlab'
set_name 'Init script up-to-date?'
set_skip_reason 'skipped (omnibus-gitlab has no init script)'
diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb
index 890f8b44d13..da695cc9272 100644
--- a/lib/system_check/app/redis_version_check.rb
+++ b/lib/system_check/app/redis_version_check.rb
@@ -3,7 +3,7 @@
module SystemCheck
module App
class RedisVersionCheck < SystemCheck::BaseCheck
- MIN_REDIS_VERSION = '2.8.0'.freeze
+ MIN_REDIS_VERSION = '2.8.0'
set_name "Redis version >= #{MIN_REDIS_VERSION}?"
def check?
diff --git a/lib/tasks/db_obsolete_ignored_columns.rake b/lib/tasks/db_obsolete_ignored_columns.rake
new file mode 100644
index 00000000000..184e407f28c
--- /dev/null
+++ b/lib/tasks/db_obsolete_ignored_columns.rake
@@ -0,0 +1,21 @@
+desc 'Show a list of obsolete `ignored_columns`'
+task 'db:obsolete_ignored_columns' => :environment do
+ list = Gitlab::Database::ObsoleteIgnoredColumns.new.execute
+
+ if list.empty?
+ puts 'No obsolete `ignored_columns` found.'
+ else
+ puts 'The following `ignored_columns` are obsolete and can be removed:'
+
+ list.each do |name, ignored_columns|
+ puts "- #{name}: #{ignored_columns.join(', ')}"
+ end
+
+ puts <<~TEXT
+
+ WARNING: Removing columns is tricky because running GitLab processes may still be using the columns.
+
+ See also https://docs.gitlab.com/ee/development/what_requires_downtime.html#dropping-columns
+ TEXT
+ end
+end
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index c53d55ceea2..fd8df015903 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -3,8 +3,8 @@
return if Rails.env.production?
namespace :gitlab do
- OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference").freeze
- TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'.freeze
+ OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference")
+ TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'
namespace :graphql do
desc 'GitLab | Generate GraphQL docs'
diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake
index 18fb8afe455..2eeb694d341 100644
--- a/lib/tasks/gitlab/uploads/legacy.rake
+++ b/lib/tasks/gitlab/uploads/legacy.rake
@@ -11,7 +11,7 @@ namespace :gitlab do
include ::EachBatch
end
- migration = 'LegacyUploadsMigrator'.freeze
+ migration = 'LegacyUploadsMigrator'
batch_size = 5000
delay_interval = 5.minutes.to_i
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 32deab7dd68..81ff65a0c5e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -517,6 +517,9 @@ msgstr ""
msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
msgstr ""
+msgid "A secure token that identifies an external storage request."
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -568,6 +571,9 @@ msgstr ""
msgid "AccessTokens|Access Tokens"
msgstr ""
+msgid "AccessTokens|Are you sure?"
+msgstr ""
+
msgid "AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working."
msgstr ""
@@ -586,6 +592,9 @@ msgstr ""
msgid "AccessTokens|It cannot be used to access any other data."
msgstr ""
+msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can access repository static objects as if they were you. You should %{reset_link_start}reset it%{reset_link_end} if that ever happens."
+msgstr ""
+
msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens."
msgstr ""
@@ -595,6 +604,9 @@ msgstr ""
msgid "AccessTokens|Personal Access Tokens"
msgstr ""
+msgid "AccessTokens|Static object token"
+msgstr ""
+
msgid "AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled."
msgstr ""
@@ -610,6 +622,9 @@ msgstr ""
msgid "AccessTokens|Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses."
msgstr ""
+msgid "AccessTokens|Your static object token is used to authenticate you when repository static objects (e.g. archives, blobs, ...) are being served from an external storage."
+msgstr ""
+
msgid "AccessTokens|reset it"
msgstr ""
@@ -1146,6 +1161,9 @@ msgstr ""
msgid "An error occurred while triggering the job."
msgstr ""
+msgid "An error occurred while trying to run a new pipeline for this Merge Request."
+msgstr ""
+
msgid "An error occurred while validating username"
msgstr ""
@@ -3440,9 +3458,6 @@ msgstr ""
msgid "Could not remove the trigger."
msgstr ""
-msgid "Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}"
-msgstr ""
-
msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
@@ -4894,6 +4909,12 @@ msgstr ""
msgid "External authorization request timeout"
msgstr ""
+msgid "External storage URL"
+msgstr ""
+
+msgid "External storage authentication token"
+msgstr ""
+
msgid "ExternalAuthorizationService|Classification label"
msgstr ""
@@ -5436,7 +5457,6 @@ msgstr ""
msgid "Go to file (MRs only)"
msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -8231,6 +8251,9 @@ msgstr ""
msgid "Pipeline|Commit"
msgstr ""
+msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}"
+msgstr ""
+
msgid "Pipeline|Coverage"
msgstr ""
@@ -8297,18 +8320,12 @@ msgstr ""
msgid "Pipeline|for"
msgstr ""
-msgid "Pipeline|into"
-msgstr ""
-
msgid "Pipeline|on"
msgstr ""
msgid "Pipeline|success"
msgstr ""
-msgid "Pipeline|with"
-msgstr ""
-
msgid "Pipeline|with stage"
msgstr ""
@@ -8720,6 +8737,9 @@ msgstr ""
msgid "Profiles|Some options are unavailable for LDAP accounts"
msgstr ""
+msgid "Profiles|Static object token was successfully reset"
+msgstr ""
+
msgid "Profiles|Tell us about yourself in fewer than 250 characters"
msgstr ""
@@ -9742,6 +9762,9 @@ msgstr ""
msgid "Repository mirror"
msgstr ""
+msgid "Repository static objects"
+msgstr ""
+
msgid "Repository storage"
msgstr ""
@@ -10293,6 +10316,9 @@ msgstr ""
msgid "September"
msgstr ""
+msgid "Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN)."
+msgstr ""
+
msgid "Server supports batch API only, please update your Git LFS client to version 1.0.1 and up."
msgstr ""
@@ -12482,6 +12508,9 @@ msgstr ""
msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
msgstr ""
+msgid "URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...)."
+msgstr ""
+
msgid "Unable to apply suggestions to a deleted line."
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index 8b38011486b..b38c39a621f 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -54,7 +54,7 @@ module QA
autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork'
autoload :DeployKey, 'qa/resource/deploy_key'
autoload :DeployToken, 'qa/resource/deploy_token'
- autoload :Branch, 'qa/resource/branch'
+ autoload :ProtectedBranch, 'qa/resource/protected_branch'
autoload :CiVariable, 'qa/resource/ci_variable'
autoload :Runner, 'qa/resource/runner'
autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
@@ -66,6 +66,7 @@ module QA
autoload :Fork, 'qa/resource/fork'
autoload :SSHKey, 'qa/resource/ssh_key'
autoload :Snippet, 'qa/resource/snippet'
+ autoload :ProjectMember, 'qa/resource/project_member'
module Events
autoload :Base, 'qa/resource/events/base'
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 84353e3d0c7..24b9fc67dd9 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -85,6 +85,8 @@ module QA
end
def add_file(name, contents)
+ FileUtils.mkdir_p(::File.dirname(name))
+
::File.write(name, contents)
if use_lfs?
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 94245bbfcba..65d83926f38 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -148,6 +148,12 @@ module QA
click_element :saml_login_button
end
+ def sign_out_and_sign_in_as(user:)
+ Menu.perform(&:sign_out)
+ has_sign_in_tab?
+ sign_in_using_credentials(user)
+ end
+
private
def sign_in_using_gitlab_credentials(user)
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 72f8e1c3ef0..6e550805f9f 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -119,7 +119,7 @@ module QA
has_element?(:description, text: description)
end
- def merge!
+ def try_to_merge!
# The merge button is disabled on load
wait do
has_element?(:merge_button)
@@ -131,6 +131,10 @@ module QA
end
merge_immediately
+ end
+
+ def merge!
+ try_to_merge!
success = wait do
has_text?('The changes were merged into')
diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb
index c53d805c61d..65149e631f3 100644
--- a/qa/qa/page/project/sub_menus/repository.rb
+++ b/qa/qa/page/project/sub_menus/repository.rb
@@ -44,3 +44,5 @@ module QA
end
end
end
+
+QA::Page::Project::SubMenus::Repository.prepend_if_ee('QA::EE::Page::Project::SubMenus::Repository')
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index d1d75b6179e..e4f708dc251 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -11,6 +11,7 @@ module QA
ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
+ ResourceNotDeletedError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response
attr_writer :api_client
@@ -30,6 +31,10 @@ module QA
resource_web_url(api_post)
end
+ def remove_via_api!
+ api_delete
+ end
+
def eager_load_api_client!
return unless api_client.nil?
@@ -79,6 +84,17 @@ module QA
process_api_response(parse_body(response))
end
+ def api_delete
+ url = Runtime::API::Request.new(api_client, api_delete_path).url
+ response = delete(url)
+
+ unless response.code == HTTP_STATUS_NO_CONTENT
+ raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`."
+ end
+
+ response
+ end
+
def api_client
@api_client ||= begin
Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http'), user: user)
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 283fc6cdbcb..88069df6ade 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -47,6 +47,18 @@ module QA
end
end
+ def self.remove_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ resource.eager_load_api_client!
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.remove_via_api! }
+ end
+ end
+
def fabricate!(*_args)
raise NotImplementedError
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 53126c67ba3..fe7eeeed37a 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -17,6 +17,7 @@ module QA
:labels,
:file_name,
:file_content
+ attr_writer :no_preparation
attribute :project do
Project.fabricate! do |resource|
@@ -58,6 +59,7 @@ module QA
@file_name = "added_file.txt"
@file_content = "File Added"
@target_new_branch = true
+ @no_preparation = false
end
def fabricate!
@@ -80,7 +82,7 @@ module QA
end
def fabricate_via_api!
- populate(:target, :source)
+ populate(:target, :source) unless @no_preparation
super
end
diff --git a/qa/qa/resource/project_member.rb b/qa/qa/resource/project_member.rb
new file mode 100644
index 00000000000..dfaa157038c
--- /dev/null
+++ b/qa/qa/resource/project_member.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class ProjectMember < Base
+ attr_accessor :user, :project, :access_level
+ attr_reader :level
+
+ def initialize
+ @level = {
+ guest: 10,
+ reporter: 20,
+ developer: 30,
+ maintainer: 40,
+ owner: 50
+ }
+ end
+
+ def api_get_path
+ "/projects/#{project.api_resource[:id]}/members/#{user.api_resource[:id]}"
+ end
+
+ def api_post_path
+ "/projects/#{project.api_resource[:id]}/members"
+ end
+
+ def api_post_body
+ {
+ user_id: user.api_resource[:id],
+ access_level: access_level
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/protected_branch.rb
index a45dd030625..c27647cf3ce 100644
--- a/qa/qa/resource/branch.rb
+++ b/qa/qa/resource/protected_branch.rb
@@ -2,13 +2,24 @@
module QA
module Resource
- class Branch < Base
- attr_accessor :project, :branch_name,
- :allow_to_push, :allow_to_merge, :protected
+ class ProtectedBranch < Base
+ attr_accessor :branch_name, :allow_to_push, :allow_to_merge, :protected
attribute :project do
- Project.fabricate! do |resource|
+ Project.fabricate_via_api! do |resource|
resource.name = 'protected-branch-project'
+ resource.initialize_with_readme = true
+ end
+ end
+
+ attribute :branch do
+ Repository::ProjectPush.fabricate! do |project_push|
+ project_push.project = project
+ project_push.file_name = 'new_file.md'
+ project_push.commit_message = 'Add new file'
+ project_push.branch_name = branch_name
+ project_push.new_branch = true
+ project_push.remote_branch = @branch_name
end
end
@@ -20,32 +31,16 @@ module QA
end
def fabricate!
- project.visit!
-
- Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'kick-off.txt'
- resource.commit_message = 'First commit'
- end
-
- branch = Repository::ProjectPush.fabricate! do |resource|
- resource.project = project
- resource.file_name = 'README.md'
- resource.commit_message = 'Add readme'
- resource.branch_name = 'master'
- resource.new_branch = false
- resource.remote_branch = @branch_name
- end
+ populate(:branch)
- Page::Project::Show.perform do |page|
- page.wait { page.has_content?(branch_name) }
- end
+ project.wait_for_push_new_branch @branch_name
# The upcoming process will make it access the Protected Branches page,
# select the already created branch and protect it according
# to `allow_to_push` variable.
return branch unless @protected
+ project.visit!
Page::Project::Menu.perform(&:go_to_repository_settings)
Page::Project::Settings::Repository.perform do |setting|
@@ -72,6 +67,18 @@ module QA
end
end
end
+
+ def self.unprotect_via_api!(&block)
+ self.remove_via_api!(&block)
+ end
+
+ def api_get_path
+ "/projects/#{@project.api_resource[:id]}/protected_branches/#{@branch_name}"
+ end
+
+ def api_delete_path
+ "/projects/#{@project.api_resource[:id]}/protected_branches/#{@branch_name}"
+ end
end
end
end
diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb
index e98880ce195..c84ade3a140 100644
--- a/qa/qa/resource/repository/project_push.rb
+++ b/qa/qa/resource/repository/project_push.rb
@@ -33,7 +33,6 @@ module QA
def fabricate!
super
project.wait_for_push @commit_message if @wait_for_push
- project.visit!
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
index e159e517cbb..dd80905d184 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb
@@ -8,6 +8,7 @@ module QA
let(:project) do
Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
+ resource.initialize_with_readme = true
end
end
@@ -42,7 +43,7 @@ module QA
end
def create_protected_branch(allow_to_push:)
- Resource::Branch.fabricate! do |resource|
+ Resource::ProtectedBranch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = allow_to_push
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index 7c1d4489c47..2952a54ff5d 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -58,7 +58,7 @@ module QA
paths:
- my-artifacts/
EOF
- end
+ end.project.visit!
expect(page).to have_content('Add .gitlab-ci.yml')
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index 203064b2665..d0ff1f8bc2c 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -5,6 +5,7 @@ module QA
module Api
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
+ HTTP_STATUS_NO_CONTENT = 204
def post(url, payload)
RestClient::Request.execute(
diff --git a/rubocop/cop/scalability/file_uploads.rb b/rubocop/cop/scalability/file_uploads.rb
new file mode 100644
index 00000000000..83017217e32
--- /dev/null
+++ b/rubocop/cop/scalability/file_uploads.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Scalability
+ # This cop checks for `File` params in API
+ #
+ # @example
+ #
+ # # bad
+ # params do
+ # requires :file, type: File
+ # end
+ #
+ # params do
+ # optional :file, type: File
+ # end
+ #
+ # # good
+ # params do
+ # requires :file, type: ::API::Validations::Types::WorkhorseFile
+ # end
+ #
+ # params do
+ # optional :file, type: ::API::Validations::Types::WorkhorseFile
+ # end
+ #
+ class FileUploads < RuboCop::Cop::Cop
+ MSG = 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html'
+
+ def_node_search :file_type_params?, <<~PATTERN
+ (send nil? {:requires :optional} (sym _) (hash <(pair (sym :type)(const nil? :File)) ...>))
+ PATTERN
+
+ def_node_search :file_types_params?, <<~PATTERN
+ (send nil? {:requires :optional} (sym _) (hash <(pair (sym :types)(array <(const nil? :File) ...>)) ...>))
+ PATTERN
+
+ def be_file_param_usage?(node)
+ file_type_params?(node) || file_types_params?(node)
+ end
+
+ def on_send(node)
+ return unless be_file_param_usage?(node)
+
+ add_offense(find_file_param(node), location: :expression)
+ end
+
+ private
+
+ def find_file_param(node)
+ node.each_descendant.find { |children| file_node_pattern.match(children) }
+ end
+
+ def file_node_pattern
+ @file_node_pattern ||= RuboCop::NodePattern.new("(const nil? :File)")
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index c342df6d6c9..9d97aa86bf6 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -37,6 +37,7 @@ require_relative 'cop/rspec/factories_in_migration_specs'
require_relative 'cop/rspec/top_level_describe_path'
require_relative 'cop/qa/element_with_pattern'
require_relative 'cop/sidekiq_options_queue'
+require_relative 'cop/scalability/file_uploads'
require_relative 'cop/destroy_all'
require_relative 'cop/ruby_interpolation_in_translation'
require_relative 'code_reuse_helpers'
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
new file mode 100644
index 00000000000..403c1df97aa
--- /dev/null
+++ b/scripts/review_apps/base-config.yaml
@@ -0,0 +1,151 @@
+global:
+ appConfig:
+ enableUsagePing: false
+ imagePullPolicy: Always
+ ingress:
+ annotations:
+ external-dns.alpha.kubernetes.io/ttl: 10
+ configureCertmanager: false
+ tls:
+ secretName: tls-cert
+certmanager:
+ install: false
+gitlab:
+ gitaly:
+ resources:
+ requests:
+ cpu: 300m
+ memory: 200M
+ limits:
+ cpu: 600m
+ memory: 420M
+ persistence:
+ size: 10G
+ gitlab-exporter:
+ enabled: false
+ mailroom:
+ enabled: false
+ migrations:
+ resources:
+ requests:
+ cpu: 350m
+ memory: 200M
+ limits:
+ cpu: 700m
+ gitlab-shell:
+ resources:
+ requests:
+ cpu: 70m
+ memory: 20M
+ limits:
+ cpu: 140m
+ memory: 40M
+ sidekiq:
+ resources:
+ requests:
+ cpu: 300m
+ memory: 800M
+ limits:
+ cpu: 400m
+ memory: 1.6G
+ task-runner:
+ resources:
+ requests:
+ cpu: 50m
+ memory: 350M
+ limits:
+ cpu: 100m
+ memory: 700M
+ unicorn:
+ resources:
+ requests:
+ cpu: 600m
+ memory: 1.4G
+ limits:
+ cpu: 1.2G
+ memory: 2.8G
+ workhorse:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 100M
+ limits:
+ cpu: 200m
+ memory: 200M
+gitlab-runner:
+ resources:
+ requests:
+ cpu: 300m
+ memory: 300M
+ limits:
+ cpu: 600m
+ memory: 600M
+minio:
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128M
+ limits:
+ cpu: 200m
+ memory: 280M
+nginx-ingress:
+ controller:
+ config:
+ ssl-ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4
+ replicaCount: 2
+ resources:
+ requests:
+ cpu: 150m
+ memory: 250M
+ limits:
+ cpu: 300m
+ memory: 500M
+ minAvailable: 1
+ service:
+ enableHttp: false
+ livenessProbe:
+ timeoutSeconds: 5
+ readinessProbe:
+ timeoutSeconds: 5
+ defaultBackend:
+ resources:
+ requests:
+ cpu: 5m
+ memory: 12M
+ limits:
+ cpu: 10m
+ memory: 24M
+ replicaCount: 1
+postgresql:
+ metrics:
+ enabled: false
+ resources:
+ requests:
+ cpu: 250m
+ memory: 256M
+ limits:
+ cpu: 500m
+prometheus:
+ install: false
+redis:
+ metrics:
+ resources:
+ enabled: false
+ resources:
+ requests:
+ cpu: 100m
+ memory: 60M
+ limits:
+ cpu: 200m
+ memory: 130M
+redis-ha:
+ enabled: false
+registry:
+ minReplicas: 1
+ resources:
+ requests:
+ cpu: 50m
+ memory: 32M
+ limits:
+ cpu: 100m
+ memory: 64M
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index a9549171b54..76eb67b1a2e 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -49,6 +49,26 @@ function delete_release() {
helm delete --purge "$name"
}
+function delete_failed_release() {
+ if [ -z "$CI_ENVIRONMENT_SLUG" ]; then
+ echoerr "No release given, aborting the delete!"
+ return
+ fi
+
+ if ! deploy_exists "${KUBE_NAMESPACE}" "${CI_ENVIRONMENT_SLUG}"; then
+ echoinfo "No Review App with ${CI_ENVIRONMENT_SLUG} is currently deployed."
+ else
+ # Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
+ if previous_deploy_failed "$CI_ENVIRONMENT_SLUG" ; then
+ echoinfo "Review App deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG"
+ delete_release
+ else
+ echoinfo "Review App deployment in good state"
+ fi
+ fi
+}
+
+
function get_pod() {
local app_name="${1}"
local status="${2-Running}"
@@ -193,22 +213,15 @@ function deploy() {
HELM_CMD=$(cat << EOF
helm upgrade --install \
- --force \
--wait \
--timeout 900 \
+ --set ci.branch="$CI_COMMIT_REF_NAME" \
+ --set ci.commit.sha="$CI_COMMIT_SHORT_SHA" \
+ --set ci.job.url="$CI_JOB_URL" \
+ --set ci.pipeline.url="$CI_PIPELINE_URL" \
--set releaseOverride="$CI_ENVIRONMENT_SLUG" \
- --set global.appConfig.enableUsagePing=false \
- --set global.imagePullPolicy=Always \
--set global.hosts.hostSuffix="$HOST_SUFFIX" \
--set global.hosts.domain="$REVIEW_APPS_DOMAIN" \
- --set global.ingress.configureCertmanager=false \
- --set global.ingress.tls.secretName=tls-cert \
- --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" \
- --set certmanager.install=false \
- --set prometheus.install=false \
- --set nginx-ingress.controller.service.enableHttp=false \
- --set nginx-ingress.controller.replicaCount=2 \
- --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \
--set gitlab.migrations.image.repository="$gitlab_migrations_image_repository" \
--set gitlab.migrations.image.tag="$CI_COMMIT_REF_SLUG" \
--set gitlab.gitaly.image.repository="$gitlab_gitaly_image_repository" \
@@ -226,91 +239,11 @@ HELM_CMD=$(cat << EOF
EOF
)
-# Default requested: CPU => 100m, memory => 100Mi
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set nginx-ingress.controller.resources.limits.cpu=200m \
- --set nginx-ingress.controller.resources.requests.memory=210M \
- --set nginx-ingress.controller.resources.limits.memory=420M
-EOF
-)
-
-# Default requested: CPU => 5m, memory => 5Mi
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set nginx-ingress.defaultBackend.resources.limits.cpu=10m \
- --set nginx-ingress.defaultBackend.resources.requests.memory=12M \
- --set nginx-ingress.defaultBackend.resources.limits.memory=24M
-EOF
-)
-
-# Default requested: CPU => 100m, memory => 200Mi
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set gitlab.gitaly.resources.requests.cpu=150m \
- --set gitlab.gitaly.resources.limits.cpu=300m \
- --set gitlab.gitaly.resources.limits.memory=420M
-EOF
-)
-
-# Default requested: CPU => 0, memory => 6M
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set gitlab.gitlab-shell.resources.requests.cpu=70m \
- --set gitlab.gitlab-shell.resources.limits.cpu=140m \
- --set gitlab.gitlab-shell.resources.requests.memory=20M \
- --set gitlab.gitlab-shell.resources.limits.memory=40M
-EOF
-)
-
-# Default requested: CPU => 50m, memory => 650M
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set gitlab.sidekiq.resources.requests.cpu=200m \
- --set gitlab.sidekiq.resources.limits.cpu=300m \
- --set gitlab.sidekiq.resources.requests.memory=800M \
- --set gitlab.sidekiq.resources.limits.memory=1.2G
-EOF
-)
-
-# Default requested: CPU => 300m + 100m (workhorse), memory => 1.2G + 100M (workhorse)
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set gitlab.unicorn.resources.limits.cpu=800m \
- --set gitlab.unicorn.resources.limits.memory=2.6G
-EOF
-)
-
-# Default requested: CPU => 100m, memory => 64Mi
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set redis.resources.limits.cpu=200m \
- --set redis.resources.limits.memory=130M
-EOF
-)
-
-# Default requested: CPU => 100m, memory => 128Mi
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set minio.resources.limits.cpu=200m \
- --set minio.resources.limits.memory=280M
-EOF
-)
-
-# Default requested: CPU => 0, memory => 0
-HELM_CMD=$(cat << EOF
- $HELM_CMD \
- --set gitlab-runner.resources.requests.cpu=300m \
- --set gitlab-runner.resources.limits.cpu=600m \
- --set gitlab-runner.resources.requests.memory=300M \
- --set gitlab-runner.resources.limits.memory=600M
-EOF
-)
-
HELM_CMD=$(cat << EOF
$HELM_CMD \
--namespace="$KUBE_NAMESPACE" \
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
+ -f "../scripts/review_apps/base-config.yaml" \
"$name" .
EOF
)
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
index 9c9f658a0bd..2f3c7da484b 100644
--- a/spec/controllers/admin/applications_controller_spec.rb
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -10,6 +10,16 @@ describe Admin::ApplicationsController do
sign_in(admin)
end
+ describe 'GET #index' do
+ render_views
+
+ it 'renders the application form' do
+ get :index
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
describe 'GET #new' do
it 'renders the application form' do
get :new
diff --git a/spec/controllers/concerns/static_object_external_storage_spec.rb b/spec/controllers/concerns/static_object_external_storage_spec.rb
new file mode 100644
index 00000000000..3a0219ddaa1
--- /dev/null
+++ b/spec/controllers/concerns/static_object_external_storage_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StaticObjectExternalStorage do
+ controller(Projects::ApplicationController) do
+ include StaticObjectExternalStorage # rubocop:disable RSpec/DescribedClass
+
+ before_action :redirect_to_external_storage, if: :static_objects_external_storage_enabled?
+
+ def show
+ head :ok
+ end
+ end
+
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user, static_object_token: 'hunter1') }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'when external storage is not configured' do
+ it 'calls the action normally' do
+ expect(Gitlab::CurrentSettings.static_objects_external_storage_url).to be_blank
+
+ do_request
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when external storage is configured' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
+ allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_auth_token).and_return('letmein')
+
+ routes.draw { get '/:namespace_id/:id' => 'projects/application#show' }
+ end
+
+ context 'when external storage token is empty' do
+ let(:base_redirect_url) { "https://cdn.gitlab.com/#{project.namespace.to_param}/#{project.to_param}" }
+
+ context 'when project is public' do
+ it 'redirects to external storage URL without adding a token parameter' do
+ do_request
+
+ expect(response).to redirect_to(base_redirect_url)
+ end
+ end
+
+ context 'when project is not public' do
+ let(:project) { create(:project, :private) }
+
+ it 'redirects to external storage URL a token parameter added' do
+ do_request
+
+ expect(response).to redirect_to("#{base_redirect_url}?token=#{user.static_object_token}")
+ end
+
+ context 'when path includes extra parameters' do
+ it 'includes the parameters in the redirect URL' do
+ do_request(foo: 'bar')
+
+ expect(response.location).to eq("#{base_redirect_url}?foo=bar&token=#{user.static_object_token}")
+ end
+ end
+ end
+ end
+
+ context 'when external storage token is present' do
+ context 'when token is correct' do
+ it 'calls the action normally' do
+ request.headers['X-Gitlab-External-Storage-Token'] = 'letmein'
+ do_request
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when token is incorrect' do
+ it 'return 403' do
+ request.headers['X-Gitlab-External-Storage-Token'] = 'donotletmein'
+ do_request
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ def do_request(extra_params = {})
+ get :show, params: { namespace_id: project.namespace, id: project }.merge(extra_params)
+ end
+end
diff --git a/spec/controllers/dashboard/snippets_controller_spec.rb b/spec/controllers/dashboard/snippets_controller_spec.rb
new file mode 100644
index 00000000000..2d839094d34
--- /dev/null
+++ b/spec/controllers/dashboard/snippets_controller_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Dashboard::SnippetsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it_behaves_like 'paginated collection' do
+ let(:collection) { Snippet.all }
+
+ before do
+ create(:personal_snippet, :public, author: user)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 9a3fbfaac51..c5af04f72ee 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -82,35 +82,15 @@ describe Dashboard::TodosController do
end
end
- context 'when using pagination' do
- let(:last_page) { user.todos.page.total_pages }
+ it_behaves_like 'paginated collection' do
let!(:issues) { create_list(:issue, 3, project: project, assignees: [user]) }
+ let(:collection) { user.todos }
before do
issues.each { |issue| todo_service.new_issue(issue, user) }
allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
- it 'redirects to last_page if page number is larger than number of pages' do
- get :index, params: { page: (last_page + 1).to_param }
-
- expect(response).to redirect_to(dashboard_todos_path(page: last_page))
- end
-
- it 'goes to the correct page' do
- get :index, params: { page: last_page }
-
- expect(assigns(:todos).current_page).to eq(last_page)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not redirect to external sites when provided a host field' do
- external_host = "www.example.com"
- get :index, params: { page: (last_page + 1).to_param, host: external_host }
-
- expect(response).to redirect_to(dashboard_todos_path(page: last_page))
- end
-
context 'when providing no filters' do
it 'does not perform a query to get the page count, but gets that from the user' do
allow(controller).to receive(:current_user).and_return(user)
diff --git a/spec/controllers/explore/snippets_controller_spec.rb b/spec/controllers/explore/snippets_controller_spec.rb
new file mode 100644
index 00000000000..fa659c6df7f
--- /dev/null
+++ b/spec/controllers/explore/snippets_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Explore::SnippetsController do
+ describe 'GET #index' do
+ it_behaves_like 'paginated collection' do
+ let(:collection) { Snippet.all }
+
+ before do
+ create(:personal_snippet, :public)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 5ac5279e997..80b5eb9a7ee 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
- let(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:forked_project) { Projects::ForkService.new(project, user, name: 'Some name').execute }
let(:group) { create(:group) }
before do
@@ -13,11 +13,12 @@ describe Projects::ForksController do
end
describe 'GET index' do
- def get_forks
+ def get_forks(search: nil)
get :index,
params: {
namespace_id: project.namespace,
- project_id: project
+ project_id: project,
+ search: search
}
end
@@ -31,6 +32,41 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
+
+ it 'forks counts are correct' do
+ get_forks
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(1)
+ expect(assigns[:internal_forks_count]).to eq(0)
+ expect(assigns[:private_forks_count]).to eq(0)
+ end
+
+ context 'after search' do
+ it 'forks counts are correct' do
+ get_forks(search: 'Non-matching query')
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(1)
+ expect(assigns[:internal_forks_count]).to eq(0)
+ expect(assigns[:private_forks_count]).to eq(0)
+ end
+ end
+ end
+
+ context 'when fork is internal' do
+ before do
+ forked_project.update(visibility_level: Project::INTERNAL, group: group)
+ end
+
+ it 'forks counts are correct' do
+ get_forks
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(0)
+ expect(assigns[:internal_forks_count]).to eq(1)
+ expect(assigns[:private_forks_count]).to eq(0)
+ end
end
context 'when fork is private' do
@@ -38,12 +74,25 @@ describe Projects::ForksController do
forked_project.update(visibility_level: Project::PRIVATE, group: group)
end
- it 'is not be visible for non logged in users' do
+ shared_examples 'forks counts' do
+ it 'forks counts are correct' do
+ get_forks
+
+ expect(assigns[:total_forks_count]).to eq(1)
+ expect(assigns[:public_forks_count]).to eq(0)
+ expect(assigns[:internal_forks_count]).to eq(0)
+ expect(assigns[:private_forks_count]).to eq(1)
+ end
+ end
+
+ it 'is not visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_blank
end
+ include_examples 'forks counts'
+
context 'when user is logged in' do
before do
sign_in(project.creator)
@@ -67,6 +116,8 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
+
+ include_examples 'forks counts'
end
context 'when user is a member of the Group' do
@@ -79,6 +130,8 @@ describe Projects::ForksController do
expect(assigns[:forks]).to be_present
end
+
+ include_examples 'forks counts'
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 608131dcbc8..367bd641f5d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -71,9 +71,16 @@ describe Projects::IssuesController do
end
end
- context 'with page param' do
- let(:last_page) { project.issues.page.total_pages }
+ it_behaves_like 'paginated collection' do
let!(:issue_list) { create_list(:issue, 2, project: project) }
+ let(:collection) { project.issues }
+ let(:params) do
+ {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ state: 'opened'
+ }
+ end
before do
sign_in(user)
@@ -81,51 +88,10 @@ describe Projects::IssuesController do
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
- it 'redirects to last_page if page number is larger than number of pages' do
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: (last_page + 1).to_param
- }
-
- expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
- end
-
- it 'redirects to specified page' do
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: last_page.to_param
- }
-
- expect(assigns(:issues).current_page).to eq(last_page)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not redirect to external sites when provided a host field' do
- external_host = "www.example.com"
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: (last_page + 1).to_param,
- host: external_host
- }
-
- expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
- end
-
it 'does not use pagination if disabled' do
allow(controller).to receive(:pagination_disabled?).and_return(true)
- get :index,
- params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- page: (last_page + 1).to_param
- }
+ get :index, params: params.merge(page: last_page + 1)
expect(response).to have_gitlab_http_status(200)
expect(assigns(:issues).size).to eq(2)
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index fcab4d73dca..084644484c5 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -125,5 +125,59 @@ describe Projects::RepositoriesController do
end
end
end
+
+ context 'as a sessionless user' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when no token is provided' do
+ it 'redirects to sign in page' do
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when a token param is present' do
+ context 'when token is correct' do
+ it 'calls the action normally' do
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: user.static_object_token }, format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when token is incorrect' do
+ it 'redirects to sign in page' do
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: 'foobar' }, format: 'zip'
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
+
+ context 'when a token header is present' do
+ context 'when token is correct' do
+ it 'calls the action normally' do
+ request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'when token is incorrect' do
+ it 'redirects to sign in page' do
+ request.headers['X-Gitlab-Static-Object-Token'] = 'foobar'
+ get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 9b5d7317c11..b13534b9088 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -13,31 +13,17 @@ describe Projects::SnippetsController do
end
describe 'GET #index' do
- context 'when page param' do
- let(:last_page) { project.snippets.page.total_pages }
- let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) }
-
- it 'redirects to last_page if page number is larger than number of pages' do
- get :index,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- page: (last_page + 1).to_param
- }
-
- expect(response).to redirect_to(namespace_project_snippets_path(page: last_page))
+ it_behaves_like 'paginated collection' do
+ let(:collection) { project.snippets }
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
end
- it 'redirects to specified page' do
- get :index,
- params: {
- namespace_id: project.namespace,
- project_id: project,
- page: last_page.to_param
- }
-
- expect(assigns(:snippets).current_page).to eq(last_page)
- expect(response).to have_gitlab_http_status(200)
+ before do
+ create(:project_snippet, :public, project: project, author: user)
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index b0092bc8994..1b3a8965342 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -9,6 +9,15 @@ describe SnippetsController do
let(:user) { create(:user) }
context 'when username parameter is present' do
+ it_behaves_like 'paginated collection' do
+ let(:collection) { Snippet.all }
+ let(:params) { { username: user.username } }
+
+ before do
+ create(:personal_snippet, :public, author: user)
+ end
+ end
+
it 'renders snippets of a user when username is present' do
get :index, params: { username: user.username }
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 3f2a676462b..d19835741e3 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -189,26 +189,20 @@ describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'shows head pipeline information' do
- within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
- "for #{pipeline.short_sha} " \
- "on #{merge_request.to_reference} " \
- "with #{merge_request.source_branch}")
+ shared_examples 'pipeline widget' do
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Detached merge request pipeline ##{pipeline.id} pending for #{pipeline.short_sha}")
+ end
end
end
+ include_examples 'pipeline widget'
+
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
- it 'shows head pipeline information' do
- within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
- "for #{pipeline.short_sha} " \
- "on #{merge_request.to_reference} " \
- "with #{merge_request.source_branch}")
- end
- end
+ include_examples 'pipeline widget'
end
end
@@ -234,29 +228,21 @@ describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'shows head pipeline information' do
- within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
- "for #{pipeline.short_sha} " \
- "on #{merge_request.to_reference} " \
- "with #{merge_request.source_branch} " \
- "into #{merge_request.target_branch}")
+ shared_examples 'pipeline widget' do
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Merged result pipeline ##{pipeline.id} pending for #{pipeline.short_sha}")
+ end
end
end
+ include_examples 'pipeline widget'
+
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
let(:merge_sha) { source_project.commit.sha }
- it 'shows head pipeline information' do
- within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
- "for #{pipeline.short_sha} " \
- "on #{merge_request.to_reference} " \
- "with #{merge_request.source_branch} " \
- "into #{merge_request.target_branch}")
- end
- end
+ include_examples 'pipeline widget'
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index f04317a59ee..7a8b938486a 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -45,6 +45,38 @@ describe 'Merge request > User sees pipelines', :js do
expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
end
+
+ context 'with a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+
+ it 'displays the Run Pipeline button' do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+
+ wait_for_requests
+
+ expect(page.find('.js-run-mr-pipeline')).to have_text('Run Pipeline')
+ end
+ end
+
+ context 'with a merged results pipeline' do
+ let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+
+ it 'displays the Run Pipeline button' do
+ visit project_merge_request_path(project, merge_request)
+
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+
+ wait_for_requests
+
+ expect(page.find('.js-run-mr-pipeline')).to have_text('Run Pipeline')
+ end
+ end
end
context 'without pipelines' do
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 401425187b0..e0b0e22823e 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -29,6 +29,11 @@ describe 'Download buttons in branches page' do
end
describe 'when checking branches' do
+ it_behaves_like 'archive download buttons' do
+ let(:ref) { 'binary-encoding' }
+ let(:path_to_visit) { project_branches_filtered_path(project, state: 'all', search: ref) }
+ end
+
context 'with artifacts' do
before do
visit project_branches_filtered_path(project, state: 'all', search: 'binary-encoding')
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index a4889f8d4c4..871f5212ddd 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -24,11 +24,17 @@ describe 'Projects > Files > Download buttons in files tree' do
before do
sign_in(user)
project.add_developer(user)
+ end
- visit project_tree_path(project, project.default_branch)
+ it_behaves_like 'archive download buttons' do
+ let(:path_to_visit) { project_tree_path(project, project.default_branch) }
end
context 'with artifacts' do
+ before do
+ visit project_tree_path(project, project.default_branch)
+ end
+
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 2aed402652b..6792a6e2af0 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -121,7 +121,6 @@ describe 'Project fork' do
end
expect(page).not_to have_content("#{another_project_fork.namespace.human_name} / #{another_project_fork.name}")
- expect(page).to have_content("1 private fork")
end
end
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index 5e7453bcdb7..0d609069426 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -29,6 +29,8 @@ describe 'Projects > Show > Download buttons' do
end
describe 'when checking project main page' do
+ it_behaves_like 'archive download buttons'
+
context 'with artifacts' do
before do
visit project_path(project)
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index 76b2704ae49..64141cf5dc9 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -30,6 +30,11 @@ describe 'Download buttons in tags page' do
end
describe 'when checking tags' do
+ it_behaves_like 'archive download buttons' do
+ let(:path_to_visit) { project_tags_path(project) }
+ let(:ref) { tag }
+ end
+
context 'with artifacts' do
before do
visit project_tags_path(project)
diff --git a/spec/fixtures/api/schemas/internal/pages/lookup_path.json b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
new file mode 100644
index 00000000000..b2b3d3f9d0a
--- /dev/null
+++ b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
@@ -0,0 +1,25 @@
+{
+ "type": "object",
+ "required": [
+ "project_id",
+ "https_only",
+ "access_control",
+ "source",
+ "prefix"
+ ],
+ "properties": {
+ "project_id": { "type": "integer" },
+ "https_only": { "type": "boolean" },
+ "access_control": { "type": "boolean" },
+ "source": { "type": "object",
+ "required": ["type", "path"],
+ "properties" : {
+ "type": { "type": "string", "enum": ["file"] },
+ "path": { "type": "string" }
+ },
+ "additionalProperties": false
+ },
+ "prefix": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/internal/pages/virtual_domain.json b/spec/fixtures/api/schemas/internal/pages/virtual_domain.json
new file mode 100644
index 00000000000..02df69026b0
--- /dev/null
+++ b/spec/fixtures/api/schemas/internal/pages/virtual_domain.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required": [
+ "lookup_paths"
+ ],
+ "optional": [
+ "certificate",
+ "key"
+ ],
+ "properties": {
+ "certificate": { "type": ["string", "null"] },
+ "key": { "type": ["string", "null"] },
+ "lookup_paths": { "type": "array", "items": { "$ref": "lookup_path.json" } }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/valid.po b/spec/fixtures/valid.po
index 155b6cbb95d..28826f05595 100644
--- a/spec/fixtures/valid.po
+++ b/spec/fixtures/valid.po
@@ -1128,3 +1128,8 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "padre"
msgstr[1] "padres"
+
+msgid "CycleAnalytics|%{stageName}"
+msgid_plural "CycleAnalytics|%d stages selected"
+msgstr[0] "%{stageName}"
+msgstr[1] "%d stages selected"
diff --git a/spec/frontend/transfer_edit_spec.js b/spec/frontend/transfer_edit_spec.js
new file mode 100644
index 00000000000..3a1ede49068
--- /dev/null
+++ b/spec/frontend/transfer_edit_spec.js
@@ -0,0 +1,53 @@
+import $ from 'jquery';
+
+import setupTransferEdit from '~/transfer_edit';
+import { loadHTMLFixture } from 'helpers/fixtures';
+
+describe('setupTransferEdit', () => {
+ const formSelector = '.js-project-transfer-form';
+ const targetSelector = 'select.select2';
+
+ beforeEach(() => {
+ loadHTMLFixture('projects/edit.html');
+ setupTransferEdit(formSelector, targetSelector);
+ });
+
+ it('disables submit button on load', () => {
+ expect(
+ $(formSelector)
+ .find(':submit')
+ .prop('disabled'),
+ ).toBe(true);
+ });
+
+ it('enables submit button when selection changes to non-empty value', () => {
+ const nonEmptyValue = $(formSelector)
+ .find(targetSelector)
+ .find('option')
+ .not(':empty')
+ .val();
+ $(formSelector)
+ .find(targetSelector)
+ .val(nonEmptyValue)
+ .trigger('change');
+
+ expect(
+ $(formSelector)
+ .find(':submit')
+ .prop('disabled'),
+ ).toBeFalsy();
+ });
+
+ it('disables submit button when selection changes to empty value', () => {
+ $(formSelector)
+ .find(targetSelector)
+ .val('')
+ .trigger('change');
+
+ expect(
+ $(formSelector)
+ .find(':submit')
+ .prop('disabled'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js
new file mode 100644
index 00000000000..551abe3cb41
--- /dev/null
+++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js
@@ -0,0 +1,24 @@
+import toast from '~/vue_shared/plugins/global_toast';
+import Vue from 'vue';
+
+describe('Global toast', () => {
+ let spyFunc;
+
+ beforeEach(() => {
+ spyFunc = jest.spyOn(Vue.toasted, 'show').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ spyFunc.mockRestore();
+ });
+
+ it('should pass all args to Vue toasted', () => {
+ const arg1 = 'TestMessage';
+ const arg2 = { className: 'foo' };
+
+ toast(arg1, arg2);
+
+ expect(Vue.toasted.show).toHaveBeenCalledTimes(1);
+ expect(Vue.toasted.show).toHaveBeenCalledWith(arg1, arg2);
+ });
+});
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index b81249a1e29..4a3ff7e0095 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -195,4 +195,41 @@ describe ApplicationHelper do
end
end
end
+
+ describe '#external_storage_url_or_path' do
+ let(:project) { create(:project) }
+
+ context 'when external storage is disabled' do
+ it 'returns the passed path' do
+ expect(helper.external_storage_url_or_path('/foo/bar', project)).to eq('/foo/bar')
+ end
+ end
+
+ context 'when external storage is enabled' do
+ let(:user) { create(:user, static_object_token: 'hunter1') }
+
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'returns the external storage URL prepended to the path' do
+ expect(helper.external_storage_url_or_path('/foo/bar', project)).to eq("https://cdn.gitlab.com/foo/bar?token=#{user.static_object_token}")
+ end
+
+ it 'preserves the path query parameters' do
+ url = helper.external_storage_url_or_path('/foo/bar?unicode=1', project)
+
+ expect(url).to eq("https://cdn.gitlab.com/foo/bar?token=#{user.static_object_token}&unicode=1")
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'returns does not append a token parameter' do
+ expect(helper.external_storage_url_or_path('/foo/bar', project)).to eq('https://cdn.gitlab.com/foo/bar')
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 3d15306d4d2..e062c841717 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -27,7 +27,7 @@ describe EventsHelper do
end
describe '#event_feed_url' do
- let(:event) { create(:event) }
+ let(:event) { create(:event).present }
let(:project) { create(:project, :public, :repository) }
context 'issue' do
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
new file mode 100644
index 00000000000..ff820b3cc95
--- /dev/null
+++ b/spec/helpers/releases_helper_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ReleasesHelper do
+ describe '#illustration' do
+ it 'returns the correct image path' do
+ expect(helper.illustration).to match(/illustrations\/releases-(\w+)\.svg/)
+ end
+ end
+
+ describe '#help_page' do
+ it 'returns the correct link to the help page' do
+ expect(helper.help_page).to include('user/project/releases/index')
+ end
+ end
+
+ context 'url helpers' do
+ let(:project) { build(:project, namespace: create(:group)) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ describe '#url_for_merge_requests' do
+ it 'returns the the correct link with the correct parameters' do
+ path = "#{project.group.path}/#{project.path}/merge_requests?scope=all&state=opened"
+ expect(helper.url_for_merge_requests).to include(path)
+ end
+ end
+
+ describe '#url_for_issues' do
+ it 'returns the the correct link with the correct parameters' do
+ path = "#{project.group.path}/#{project.path}/issues?scope=all&state=opened"
+ expect(helper.url_for_issues).to include(path)
+ end
+ end
+
+ describe '#data_for_releases_page' do
+ it 'has the needed data to display release blocks' do
+ keys = %i(project_id illustration_path documentation_path merge_requests_url issues_url)
+ expect(helper.data_for_releases_page.keys).to eq(keys)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index fec01b1f0a3..46aca2b7f03 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import Api from '~/api';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -10,6 +11,13 @@ describe('Pipelines table in Commits and Merge requests', function() {
let PipelinesTable;
let mock;
let vm;
+ const props = {
+ endpoint: 'endpoint.json',
+ helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
+ autoDevopsHelpPath: 'foo',
+ };
preloadFixtures(jsonFixtureName);
@@ -32,13 +40,7 @@ describe('Pipelines table in Commits and Merge requests', function() {
beforeEach(function() {
mock.onGet('endpoint.json').reply(200, []);
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
});
it('should render the empty state', function(done) {
@@ -54,13 +56,7 @@ describe('Pipelines table in Commits and Merge requests', function() {
describe('with pipelines', () => {
beforeEach(() => {
mock.onGet('endpoint.json').reply(200, [pipeline]);
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
});
it('should render a table with the received pipelines', done => {
@@ -111,30 +107,145 @@ describe('Pipelines table in Commits and Merge requests', function() {
done();
});
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
element.appendChild(vm.$el);
});
});
});
+ describe('run pipeline button', () => {
+ let pipelineCopy;
+
+ beforeEach(() => {
+ pipelineCopy = Object.assign({}, pipeline);
+ });
+
+ describe('when latest pipeline has detached flag and canRunPipeline is true', () => {
+ it('renders the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: true,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).not.toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('when latest pipeline has detached flag and canRunPipeline is false', () => {
+ it('does not render the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: false,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('when latest pipeline does not have detached flag and canRunPipeline is true', () => {
+ it('does not render the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = false;
+ pipelineCopy.flags.merge_request_pipeline = false;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: true,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('when latest pipeline does not have detached flag and merge_request_pipeline is true', () => {
+ it('does not render the run pipeline button', done => {
+ pipelineCopy.flags.detached_merge_request_pipeline = false;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: false,
+ }),
+ );
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('on click', () => {
+ beforeEach(() => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ vm = mountComponent(
+ PipelinesTable,
+ Object.assign({}, props, {
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
+ }),
+ );
+ });
+
+ it('updates the loading state', done => {
+ spyOn(Api, 'postMergeRequestPipeline').and.returnValue(Promise.resolve());
+
+ setTimeout(() => {
+ vm.$el.querySelector('.js-run-mr-pipeline').click();
+
+ vm.$nextTick(() => {
+ expect(vm.state.isRunningMergeRequestPipeline).toBe(true);
+
+ setTimeout(() => {
+ expect(vm.state.isRunningMergeRequestPipeline).toBe(false);
+
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+
describe('unsuccessfull request', () => {
beforeEach(() => {
mock.onGet('endpoint.json').reply(500, []);
- vm = mountComponent(PipelinesTable, {
- endpoint: 'endpoint.json',
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- autoDevopsHelpPath: 'foo',
- });
+ vm = mountComponent(PipelinesTable, props);
});
it('should render error state', function(done) {
diff --git a/spec/javascripts/reports/components/modal_open_name_spec.js b/spec/javascripts/reports/components/modal_open_name_spec.js
index b18b3ef03d1..53ae6453915 100644
--- a/spec/javascripts/reports/components/modal_open_name_spec.js
+++ b/spec/javascripts/reports/components/modal_open_name_spec.js
@@ -3,6 +3,8 @@ import Vuex from 'vuex';
import component from '~/reports/components/modal_open_name.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+Vue.use(Vuex);
+
describe('Modal open name', () => {
const Component = Vue.extend(component);
let vm;
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 7f20b0da991..3ee97b978fd 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -210,14 +210,4 @@ const mockData = {
},
};
-mockData.sidebarMockInterceptor = function(request, next) {
- const body = this.responseMap[request.method.toUpperCase()][request.url];
-
- next(
- request.respondWith(JSON.stringify(body), {
- status: 200,
- }),
- );
-}.bind(mockData);
-
export default mockData;
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
index 016f5e033a5..e808f4003ff 100644
--- a/spec/javascripts/sidebar/sidebar_assignees_spec.js
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -1,4 +1,3 @@
-import _ from 'underscore';
import Vue from 'vue';
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
@@ -14,8 +13,6 @@ describe('sidebar assignees', () => {
preloadFixtures('issues/open-issue.html');
beforeEach(() => {
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
-
loadFixtures('issues/open-issue.html');
mediator = new SidebarMediator(Mock.mediator);
@@ -38,7 +35,6 @@ describe('sidebar assignees', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
- Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
});
it('calls the mediator when saves the assignees', () => {
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 6c69c08e733..b0412105e3f 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -1,31 +1,37 @@
-import _ from 'underscore';
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
import Mock from './mock_data';
+const { mediator: mediatorMockData } = Mock;
+
describe('Sidebar mediator', function() {
+ let mock;
+
beforeEach(() => {
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- this.mediator = new SidebarMediator(Mock.mediator);
+ mock = new MockAdapter(axios);
+
+ this.mediator = new SidebarMediator(mediatorMockData);
});
afterEach(() => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
- Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+ mock.restore();
});
it('assigns yourself ', () => {
this.mediator.assignYourself();
- expect(this.mediator.store.currentUser).toEqual(Mock.mediator.currentUser);
- expect(this.mediator.store.assignees[0]).toEqual(Mock.mediator.currentUser);
+ expect(this.mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
+ expect(this.mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
});
it('saves assignees', done => {
+ mock.onPut(mediatorMockData.endpoint).reply(200, {});
this.mediator
.saveAssignees('issue[assignee_ids]')
.then(resp => {
@@ -36,8 +42,8 @@ describe('Sidebar mediator', function() {
});
it('fetches the data', done => {
- const mockData =
- Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras'];
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
spyOn(this.mediator, 'processFetchedData').and.callThrough();
this.mediator
@@ -50,8 +56,7 @@ describe('Sidebar mediator', function() {
});
it('processes fetched data', () => {
- const mockData =
- Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar_extras'];
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
this.mediator.processFetchedData(mockData);
expect(this.mediator.store.assignees).toEqual(mockData.assignees);
@@ -74,6 +79,7 @@ describe('Sidebar mediator', function() {
it('fetches autocomplete projects', done => {
const searchTerm = 'foo';
+ mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough();
spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough();
@@ -88,7 +94,9 @@ describe('Sidebar mediator', function() {
});
it('moves issue', done => {
+ const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
const moveToProjectId = 7;
+ mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
this.mediator.store.setMoveToProjectId(moveToProjectId);
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
@@ -97,7 +105,7 @@ describe('Sidebar mediator', function() {
.moveIssue()
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ expect(visitUrl).toHaveBeenCalledWith(mockData.web_url);
})
.then(done)
.catch(done.fail);
@@ -105,6 +113,7 @@ describe('Sidebar mediator', function() {
it('toggle subscription', done => {
this.mediator.store.setSubscribedState(false);
+ mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
this.mediator
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
index 230e0a933a9..ec712450f2e 100644
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import _ from 'underscore';
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
@@ -8,8 +8,12 @@ import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import Mock from './mock_data';
describe('SidebarMoveIssue', function() {
+ let mock;
+
beforeEach(() => {
- Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
+ mock = new MockAdapter(axios);
+ const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15'];
+ mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData);
this.mediator = new SidebarMediator(Mock.mediator);
this.$content = $(`
<div class="dropdown">
@@ -37,8 +41,7 @@ describe('SidebarMoveIssue', function() {
SidebarMediator.singleton = null;
this.sidebarMoveIssue.destroy();
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+ mock.restore();
});
describe('init', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index fe831094ecf..67e85763fae 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -69,7 +69,6 @@ describe('MRWidgetPipeline', () => {
vm = mountComponent(Component, {
pipeline: mockData.pipeline,
hasCi: true,
- ciStatus: null,
troubleshootingDocsPath: 'help',
});
@@ -208,71 +207,66 @@ describe('MRWidgetPipeline', () => {
});
});
- describe('without pipeline.merge_request', () => {
- it('should render info that includes the commit and branch details', () => {
- const mockCopy = JSON.parse(JSON.stringify(mockData));
- delete mockCopy.pipeline.merge_request;
- const { pipeline } = mockCopy;
-
- vm = mountComponent(Component, {
- pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
- sourceBranchLink: mockCopy.source_branch_link,
- });
-
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockCopy.source_branch_link}`;
+ describe('for each type of pipeline', () => {
+ let pipeline;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+ beforeEach(() => {
+ ({ pipeline } = JSON.parse(JSON.stringify(mockData)));
- expect(actual).toBe(expected);
+ pipeline.details.name = 'Pipeline';
+ pipeline.merge_request_event_type = undefined;
+ pipeline.ref.tag = false;
+ pipeline.ref.branch = false;
});
- });
-
- describe('with pipeline.merge_request and flags.merge_request_pipeline', () => {
- it('should render info that includes the commit, MR, source branch, and target branch details', () => {
- const mockCopy = JSON.parse(JSON.stringify(mockData));
- const { pipeline } = mockCopy;
- pipeline.flags.merge_request_pipeline = true;
- pipeline.flags.detached_merge_request_pipeline = false;
+ const factory = () => {
vm = mountComponent(Component, {
pipeline,
hasCi: true,
ciStatus: 'success',
troubleshootingDocsPath: 'help',
- sourceBranchLink: mockCopy.source_branch_link,
+ sourceBranchLink: mockData.source_branch_link,
});
+ };
+
+ describe('for a branch pipeline', () => {
+ it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
+ pipeline.ref.branch = true;
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`;
+ factory();
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+ const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
- expect(actual).toBe(expected);
+ expect(actual).toBe(expected);
+ });
});
- });
- describe('with pipeline.merge_request and flags.detached_merge_request_pipeline', () => {
- it('should render info that includes the commit, MR, and source branch details', () => {
- const mockCopy = JSON.parse(JSON.stringify(mockData));
- const { pipeline } = mockCopy;
- pipeline.flags.merge_request_pipeline = false;
- pipeline.flags.detached_merge_request_pipeline = true;
+ describe('for a tag pipeline', () => {
+ it('renders a pipeline widget that reads "Pipeline <ID> <status> for <SHA> on <branch>"', () => {
+ pipeline.ref.tag = true;
- vm = mountComponent(Component, {
- pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
- sourceBranchLink: mockCopy.source_branch_link,
+ factory();
+
+ const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
});
+ });
+
+ describe('for a detached merge request pipeline', () => {
+ it('renders a pipeline widget that reads "Detached merge request pipeline <ID> <status> for <SHA>"', () => {
+ pipeline.details.name = 'Detached merge request pipeline';
+ pipeline.merge_request_event_type = 'detached';
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`;
+ factory();
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+ const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
- expect(actual).toBe(expected);
+ expect(actual).toBe(expected);
+ });
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index a55d5537df7..2f79806652b 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -259,6 +259,8 @@ export const mockStore = {
tooltip: 'passed',
},
},
+ flags: {},
+ ref: {},
},
mergePipeline: {
id: 1,
@@ -276,6 +278,8 @@ export const mockStore = {
tooltip: 'passed',
},
},
+ flags: {},
+ ref: {},
},
targetBranch: 'target-branch',
sourceBranch: 'source-branch',
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 02d73e1849a..da984175f9f 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -1,6 +1,10 @@
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
import Vue from 'vue';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import fieldComponent from '~/vue_shared/components/markdown/field.vue';
+import { TEST_HOST } from 'spec/test_constants';
function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) {
expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite);
@@ -9,9 +13,13 @@ function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) {
}
describe('Markdown field component', () => {
+ const markdownPreviewPath = `${TEST_HOST}/preview`;
+ const markdownDocsPath = `${TEST_HOST}/docs`;
+ let axiosMock;
let vm;
beforeEach(done => {
+ axiosMock = new AxiosMockAdapter(axios);
vm = new Vue({
components: {
fieldComponent,
@@ -23,8 +31,8 @@ describe('Markdown field component', () => {
},
template: `
<field-component
- markdown-preview-path="/preview"
- markdown-docs-path="/docs"
+ markdown-preview-path="${markdownPreviewPath}"
+ markdown-docs-path="${markdownDocsPath}"
>
<textarea
slot="textarea"
@@ -37,7 +45,13 @@ describe('Markdown field component', () => {
Vue.nextTick(done);
});
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
describe('mounted', () => {
+ const previewHTML = '<p>markdown preview</p>';
+
it('renders textarea inside backdrop', () => {
expect(vm.$el.querySelector('.zen-backdrop textarea')).not.toBeNull();
});
@@ -47,20 +61,7 @@ describe('Markdown field component', () => {
let writeLink;
beforeEach(() => {
- spyOn(Vue.http, 'post').and.callFake(
- () =>
- new Promise(resolve => {
- setTimeout(() => {
- resolve({
- json() {
- return {
- body: '<p>markdown preview</p>',
- };
- },
- });
- });
- }),
- );
+ axiosMock.onPost(markdownPreviewPath).replyOnce(200, { body: previewHTML });
previewLink = vm.$el.querySelector('.nav-links .js-preview-link');
writeLink = vm.$el.querySelector('.nav-links .js-write-link');
@@ -92,9 +93,7 @@ describe('Markdown field component', () => {
previewLink.click();
setTimeout(() => {
- expect(vm.$el.querySelector('.md-preview-holder').innerHTML).toContain(
- '<p>markdown preview</p>',
- );
+ expect(vm.$el.querySelector('.md-preview-holder').innerHTML).toContain(previewHTML);
done();
});
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
index 41265da97a4..dd8070c1240 100644
--- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -115,6 +115,60 @@ describe Gitlab::Auth::UserAuthFinders do
end
end
+ describe '#find_user_from_static_object_token' do
+ context 'when request format is archive' do
+ before do
+ env['SCRIPT_NAME'] = 'project/-/archive/master.zip'
+ end
+
+ context 'when token header param is present' do
+ context 'when token is correct' do
+ it 'returns the user' do
+ request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
+
+ expect(find_user_from_static_object_token(:archive)).to eq(user)
+ end
+ end
+
+ context 'when token is incorrect' do
+ it 'returns the user' do
+ request.headers['X-Gitlab-Static-Object-Token'] = 'foobar'
+
+ expect { find_user_from_static_object_token(:archive) }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+ end
+
+ context 'when token query param is present' do
+ context 'when token is correct' do
+ it 'returns the user' do
+ set_param(:token, user.static_object_token)
+
+ expect(find_user_from_static_object_token(:archive)).to eq(user)
+ end
+ end
+
+ context 'when token is incorrect' do
+ it 'returns the user' do
+ set_param(:token, 'foobar')
+
+ expect { find_user_from_static_object_token(:archive) }.to raise_error(Gitlab::Auth::UnauthorizedError)
+ end
+ end
+ end
+ end
+
+ context 'when request format is not archive' do
+ before do
+ env['script_name'] = 'url'
+ end
+
+ it 'returns nil' do
+ expect(find_user_from_static_object_token(:foo)).to be_nil
+ end
+ end
+ end
+
describe '#find_user_from_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
diff --git a/spec/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids_spec.rb b/spec/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids_spec.rb
new file mode 100644
index 00000000000..73c855ac184
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_promoted_epics_discussion_ids_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::FixPromotedEpicsDiscussionIds, :migration, schema: 20190715193142 do
+ let(:namespaces) { table(:namespaces) }
+ let(:users) { table(:users) }
+ let(:epics) { table(:epics) }
+ let(:notes) { table(:notes) }
+
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+ let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+ let(:epic1) { epics.create!(id: 1, author_id: user.id, iid: 1, group_id: namespace.id, title: 'Epic with discussion', title_html: 'Epic with discussion') }
+
+ def create_note(discussion_id)
+ notes.create!(note: 'note comment',
+ noteable_id: epic1.id,
+ noteable_type: 'Epic',
+ discussion_id: discussion_id)
+ end
+
+ def expect_valid_discussion_id(id)
+ expect(id).to match(/\A\h{40}\z/)
+ end
+
+ describe '#perform with batch of discussion ids' do
+ it 'updates discussion ids' do
+ note1 = create_note('00000000')
+ note2 = create_note('00000000')
+ note3 = create_note('10000000')
+
+ subject.perform(%w(00000000 10000000))
+
+ expect_valid_discussion_id(note1.reload.discussion_id)
+ expect_valid_discussion_id(note2.reload.discussion_id)
+ expect_valid_discussion_id(note3.reload.discussion_id)
+ expect(note1.discussion_id).to eq(note2.discussion_id)
+ expect(note1.discussion_id).not_to eq(note3.discussion_id)
+ end
+
+ it 'skips notes with discussion id not in range' do
+ note4 = create_note('20000000')
+
+ subject.perform(%w(00000000 10000000))
+
+ expect(note4.reload.discussion_id).to eq('20000000')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index c25344ec1a4..18037a5612c 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -1,5 +1,5 @@
require 'fast_spec_helper'
-require 'chronic_duration'
+require 'gitlab_chronic_duration'
require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 4afb7195b7b..931477d19c2 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -28,12 +28,14 @@ describe Gitlab::DataBuilder::Pipeline do
expect(attributes[:sha]).to eq(pipeline.sha)
expect(attributes[:tag]).to eq(pipeline.tag)
expect(attributes[:id]).to eq(pipeline.id)
+ expect(attributes[:source]).to eq(pipeline.source)
expect(attributes[:status]).to eq(pipeline.status)
expect(attributes[:detailed_status]).to eq('passed')
expect(build_data).to be_a(Hash)
expect(build_data[:id]).to eq(build.id)
expect(build_data[:status]).to eq(build.status)
expect(project_data).to eq(project.hook_attrs(backward: false))
+ expect(data[:merge_request]).to be_nil
end
context 'pipeline without variables' do
@@ -60,6 +62,22 @@ describe Gitlab::DataBuilder::Pipeline do
it 'returns a source ref' do
expect(attributes[:ref]).to eq(merge_request.source_branch)
end
+
+ it 'returns merge request' do
+ merge_request_attrs = data[:merge_request]
+
+ expect(merge_request_attrs).to be_a(Hash)
+ expect(merge_request_attrs[:id]).to eq(merge_request.id)
+ expect(merge_request_attrs[:iid]).to eq(merge_request.iid)
+ expect(merge_request_attrs[:title]).to eq(merge_request.title)
+ expect(merge_request_attrs[:source_branch]).to eq(merge_request.source_branch)
+ expect(merge_request_attrs[:source_project_id]).to eq(merge_request.source_project_id)
+ expect(merge_request_attrs[:target_branch]).to eq(merge_request.target_branch)
+ expect(merge_request_attrs[:target_project_id]).to eq(merge_request.target_project_id)
+ expect(merge_request_attrs[:state]).to eq(merge_request.state)
+ expect(merge_request_attrs[:merge_status]).to eq(merge_request.merge_status)
+ expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/merge_requests/#{merge_request.iid}")
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
new file mode 100644
index 00000000000..6d38f7f1b95
--- /dev/null
+++ b/spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Database::ObsoleteIgnoredColumns do
+ module Testing
+ class MyBase < ApplicationRecord
+ end
+
+ class SomeAbstract < MyBase
+ self.abstract_class = true
+
+ self.table_name = 'projects'
+
+ self.ignored_columns += %i[unused]
+ end
+
+ class B < MyBase
+ self.table_name = 'issues'
+
+ self.ignored_columns += %i[id other]
+ end
+
+ class A < SomeAbstract
+ self.ignored_columns += %i[id also_unused]
+ end
+
+ class C < MyBase
+ self.table_name = 'users'
+ end
+ end
+
+ subject { described_class.new(Testing::MyBase) }
+
+ describe '#execute' do
+ it 'returns a list of class names and columns pairs' do
+ expect(subject.execute).to eq([
+ ['Testing::A', %w(unused also_unused)],
+ ['Testing::B', %w(other)]
+ ])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/noteable_metadata_spec.rb b/spec/lib/gitlab/noteable_metadata_spec.rb
new file mode 100644
index 00000000000..b12a1825f04
--- /dev/null
+++ b/spec/lib/gitlab/noteable_metadata_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::NoteableMetadata do
+ subject { Class.new { include Gitlab::NoteableMetadata }.new }
+
+ it 'returns an empty Hash if an empty collection is provided' do
+ expect(subject.noteable_meta_data(Snippet.none, 'Snippet')).to eq({})
+ end
+
+ it 'raises an error when given a collection with no limit' do
+ expect { subject.noteable_meta_data(Snippet.all, 'Snippet') }.to raise_error(/must have a limit/)
+ end
+
+ context 'snippets' do
+ let!(:snippet) { create(:personal_snippet) }
+ let!(:other_snippet) { create(:personal_snippet) }
+ let!(:note) { create(:note, noteable: snippet) }
+
+ it 'aggregates stats on snippets' do
+ data = subject.noteable_meta_data(Snippet.all.limit(10), 'Snippet')
+
+ expect(data.count).to eq(2)
+ expect(data[snippet.id].user_notes_count).to eq(1)
+ expect(data[other_snippet.id].user_notes_count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/patch/chronic_duration_spec.rb b/spec/lib/gitlab/patch/chronic_duration_spec.rb
deleted file mode 100644
index 541037ec1a2..00000000000
--- a/spec/lib/gitlab/patch/chronic_duration_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Patch::ChronicDuration do
- subject { ChronicDuration.parse('1mo') }
-
- it 'uses default conversions' do
- expect(subject).to eq(2_592_000)
- end
-
- context 'with custom conversions' do
- before do
- ChronicDuration.hours_per_day = 8
- ChronicDuration.days_per_week = 5
- end
-
- after do
- ChronicDuration.hours_per_day = 24
- ChronicDuration.days_per_week = 7
- end
-
- it 'uses custom conversions' do
- expect(subject).to eq(576_000)
- end
- end
-end
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 808eb865a21..fd1338b55a6 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -6,6 +6,7 @@ describe Gitlab::RepositoryCacheAdapter do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:cache) { repository.send(:cache) }
+ let(:redis_set_cache) { repository.send(:redis_set_cache) }
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
@@ -208,9 +209,11 @@ describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
expect(cache).to receive(:expire).with(:rendered_readme)
- expect(cache).to receive(:expire).with(:gitignore)
+ expect(cache).to receive(:expire).with(:branch_names)
+ expect(redis_set_cache).to receive(:expire).with(:rendered_readme)
+ expect(redis_set_cache).to receive(:expire).with(:branch_names)
- repository.expire_method_caches(%i(rendered_readme gitignore))
+ repository.expire_method_caches(%i(rendered_readme branch_names))
end
it 'does not expire caches for non-existent methods' do
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
new file mode 100644
index 00000000000..87e51f801e5
--- /dev/null
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:namespace) { "#{repository.full_path}:#{project.id}" }
+ let(:cache) { described_class.new(repository) }
+
+ describe '#cache_key' do
+ subject { cache.cache_key(:foo) }
+
+ it 'includes the namespace' do
+ is_expected.to eq("foo:#{namespace}:set")
+ end
+
+ context 'with a given namespace' do
+ let(:extra_namespace) { 'my:data' }
+ let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
+
+ it 'includes the full namespace' do
+ is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set")
+ end
+ end
+ end
+
+ describe '#expire' do
+ it 'expires the given key from the cache' do
+ cache.write(:foo, ['value'])
+
+ expect(cache.read(:foo)).to contain_exactly('value')
+ expect(cache.expire(:foo)).to eq(1)
+ expect(cache.read(:foo)).to be_empty
+ end
+ end
+
+ describe '#exist?' do
+ it 'checks whether the key exists' do
+ expect(cache.exist?(:foo)).to be(false)
+
+ cache.write(:foo, ['value'])
+
+ expect(cache.exist?(:foo)).to be(true)
+ end
+ end
+
+ describe '#fetch' do
+ let(:blk) { -> { ['block value'] } }
+
+ subject { cache.fetch(:foo, &blk) }
+
+ it 'fetches the key from the cache when filled' do
+ cache.write(:foo, ['value'])
+
+ is_expected.to contain_exactly('value')
+ end
+
+ it 'writes the value of the provided block when empty' do
+ cache.expire(:foo)
+
+ is_expected.to contain_exactly('block value')
+ expect(cache.read(:foo)).to contain_exactly('block value')
+ end
+ end
+
+ describe '#include?' do
+ it 'checks inclusion in the Redis set' do
+ cache.write(:foo, ['value'])
+
+ expect(cache.include?(:foo, 'value')).to be(true)
+ expect(cache.include?(:foo, 'bar')).to be(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_monitor_spec.rb b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
index bbd7bf90217..acbb09e3542 100644
--- a/spec/lib/gitlab/sidekiq_monitor_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Gitlab::SidekiqMonitor do
+describe Gitlab::SidekiqDaemon::Monitor do
let(:monitor) { described_class.new }
describe '#within_job' do
@@ -43,7 +43,7 @@ describe Gitlab::SidekiqMonitor do
before do
# we want to run at most once cycle
# we toggle `enabled?` flag after the first call
- stub_const('Gitlab::SidekiqMonitor::RECONNECT_TIME', 0)
+ stub_const('Gitlab::SidekiqDaemon::Monitor::RECONNECT_TIME', 0)
allow(monitor).to receive(:enabled?).and_return(true, false)
allow(Sidekiq.logger).to receive(:info)
diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
index 7319cdc2399..023df1a6391 100644
--- a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
@@ -10,8 +10,8 @@ describe Gitlab::SidekiqMiddleware::Monitor do
let(:job) { { 'jid' => 'job-id' } }
let(:queue) { 'my-queue' }
- it 'calls SidekiqMonitor' do
- expect(Gitlab::SidekiqMonitor.instance).to receive(:within_job)
+ it 'calls Gitlab::SidekiqDaemon::Monitor' do
+ expect(Gitlab::SidekiqDaemon::Monitor.instance).to receive(:within_job)
.with('job-id', 'my-queue')
.and_call_original
@@ -29,7 +29,7 @@ describe Gitlab::SidekiqMiddleware::Monitor do
context 'when cancel happens' do
subject do
monitor.call(worker, job, queue) do
- raise Gitlab::SidekiqMonitor::CancelledError
+ raise Gitlab::SidekiqDaemon::Monitor::CancelledError
end
end
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
index 26baaf873a8..624e799c5e9 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -52,6 +52,22 @@ describe Gitlab::Utils::StrongMemoize do
end
end
+ describe '#strong_memoized?' do
+ let(:value) { :anything }
+
+ subject { object.strong_memoized?(:method_name) }
+
+ it 'returns false if the value is uncached' do
+ is_expected.to be(false)
+ end
+
+ it 'returns true if the value is cached' do
+ object.method_name
+
+ is_expected.to be(true)
+ end
+ end
+
describe '#clear_memoization' do
let(:value) { 'mepmep' }
diff --git a/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb b/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb
new file mode 100644
index 00000000000..5e25d1aed82
--- /dev/null
+++ b/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190715193142_migrate_discussion_id_on_promoted_epics.rb')
+
+describe MigrateDiscussionIdOnPromotedEpics, :migration, :sidekiq do
+ let(:migration_class) { described_class::MIGRATION }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+ let(:issues) { table(:issues) }
+ let(:epics) { table(:epics) }
+ let(:notes) { table(:notes) }
+ let(:system_note_metadata) { table(:system_note_metadata) }
+
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+ let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+
+ def create_promotion_note(model, id)
+ note = create_note(model, id, { system: true,
+ note: 'promoted from issue XXX' })
+ system_note_metadata.create!(note_id: note.id, action: 'moved')
+ end
+
+ def create_epic
+ epics.create!(author_id: user.id, iid: 1,
+ group_id: namespace.id,
+ title: 'Epic with discussion',
+ title_html: 'Epic with discussion')
+ end
+
+ def create_note(model, id, extra_params = {})
+ params = {
+ note: 'note',
+ noteable_id: model.id,
+ noteable_type: model.class.name,
+ discussion_id: id
+ }.merge(extra_params)
+
+ notes.create!(params)
+ end
+
+ context 'with promoted epic' do
+ let(:epic1) { create_epic }
+ let!(:note1) { create_promotion_note(epic1, 'id1') }
+
+ it 'correctly schedules background migrations in batches' do
+ create_note(epic1, 'id2')
+ create_note(epic1, 'id3')
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(2.minutes, %w(id1 id2))
+ expect(migration_name).to be_scheduled_delayed_migration(4.minutes, %w(id3))
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+
+ it 'schedules only promoted epics' do
+ issue = issues.create!(description: 'first', state: 'opened')
+ create_promotion_note(issue, 'id2')
+ create_note(create_epic, 'id3')
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(2.minutes, %w(id1))
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 4f7a6d102b8..d12f9b9100a 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -48,6 +48,10 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:outbound_local_requests_whitelist) }
it { is_expected.to allow_value([]).for(:outbound_local_requests_whitelist) }
+ it { is_expected.to allow_value(nil).for(:static_objects_external_storage_url) }
+ it { is_expected.to allow_value(http).for(:static_objects_external_storage_url) }
+ it { is_expected.to allow_value(https).for(:static_objects_external_storage_url) }
+
context "when user accepted let's encrypt terms of service" do
before do
setting.update(lets_encrypt_terms_of_service_accepted: true)
@@ -420,6 +424,16 @@ describe ApplicationSetting do
end
end
end
+
+ context 'static objects external storage' do
+ context 'when URL is set' do
+ before do
+ subject.static_objects_external_storage_url = http
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:static_objects_external_storage_auth_token) }
+ end
+ end
end
context 'restrict creating duplicates' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 146e479adef..d5ad70194cb 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -579,14 +579,22 @@ describe Ci::Pipeline, :mailer do
end
describe 'Validations for merge request pipelines' do
- let(:pipeline) { build(:ci_pipeline, source: source, merge_request: merge_request) }
+ let(:pipeline) do
+ build(:ci_pipeline, source: source, merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master')
+ end
context 'when source is merge request' do
let(:source) { :merge_request_event }
context 'when merge request is specified' do
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
-
it { expect(pipeline).to be_valid }
end
@@ -601,8 +609,6 @@ describe Ci::Pipeline, :mailer do
let(:source) { :web }
context 'when merge request is specified' do
- let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
-
it { expect(pipeline).not_to be_valid }
end
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
new file mode 100644
index 00000000000..0a1c576a5e7
--- /dev/null
+++ b/spec/models/oauth_access_token_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe OauthAccessToken do
+ let(:user) { create(:user) }
+ let(:app_one) { create(:oauth_application) }
+ let(:app_two) { create(:oauth_application) }
+ let(:app_three) { create(:oauth_application) }
+ let(:tokens) { described_class.all }
+
+ before do
+ create(:oauth_access_token, application_id: app_one.id)
+ create_list(:oauth_access_token, 2, resource_owner: user, application_id: app_two.id)
+ end
+
+ it 'returns unique owners' do
+ expect(tokens.count).to eq(3)
+ expect(tokens.distinct_resource_owner_counts([app_one])).to eq({ app_one.id => 1 })
+ expect(tokens.distinct_resource_owner_counts([app_two])).to eq({ app_two.id => 1 })
+ expect(tokens.distinct_resource_owner_counts([app_three])).to eq({})
+ expect(tokens.distinct_resource_owner_counts([app_one, app_two]))
+ .to eq({
+ app_one.id => 1,
+ app_two.id => 1
+ })
+ end
+end
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
new file mode 100644
index 00000000000..2146b0c9abd
--- /dev/null
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Pages::LookupPath do
+ let(:project) do
+ instance_double(Project,
+ id: 12345,
+ private_pages?: true,
+ pages_https_only?: true,
+ full_path: 'the/full/path'
+ )
+ end
+
+ subject(:lookup_path) { described_class.new(project) }
+
+ describe '#project_id' do
+ it 'delegates to Project#id' do
+ expect(lookup_path.project_id).to eq(12345)
+ end
+ end
+
+ describe '#access_control' do
+ it 'delegates to Project#private_pages?' do
+ expect(lookup_path.access_control).to eq(true)
+ end
+ end
+
+ describe '#https_only' do
+ subject(:lookup_path) { described_class.new(project, domain: domain) }
+
+ context 'when no domain provided' do
+ let(:domain) { nil }
+
+ it 'delegates to Project#pages_https_only?' do
+ expect(lookup_path.https_only).to eq(true)
+ end
+ end
+
+ context 'when there is domain provided' do
+ let(:domain) { instance_double(PagesDomain, https?: false) }
+
+ it 'takes into account the https setting of the domain' do
+ expect(lookup_path.https_only).to eq(false)
+ end
+ end
+ end
+
+ describe '#source' do
+ it 'sets the source type to "file"' do
+ expect(lookup_path.source[:type]).to eq('file')
+ end
+
+ it 'sets the source path to the project full path suffixed with "public/' do
+ expect(lookup_path.source[:path]).to eq('the/full/path/public/')
+ end
+ end
+
+ describe '#prefix' do
+ it 'returns "/"' do
+ expect(lookup_path.prefix).to eq('/')
+ end
+ end
+end
diff --git a/spec/models/pages/virtual_domain_spec.rb b/spec/models/pages/virtual_domain_spec.rb
new file mode 100644
index 00000000000..eaa57b7acd6
--- /dev/null
+++ b/spec/models/pages/virtual_domain_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Pages::VirtualDomain do
+ describe '#certificate and #key pair' do
+ let(:domain) { nil }
+ let(:project) { instance_double(Project) }
+
+ subject(:virtual_domain) { described_class.new([project], domain: domain) }
+
+ it 'returns nil if there is no domain provided' do
+ expect(virtual_domain.certificate).to be_nil
+ expect(virtual_domain.key).to be_nil
+ end
+
+ context 'when Pages domain is provided' do
+ let(:domain) { instance_double(PagesDomain, certificate: 'certificate', key: 'key') }
+
+ it 'returns certificate and key from the provided domain' do
+ expect(virtual_domain.certificate).to eq('certificate')
+ expect(virtual_domain.key).to eq('key')
+ end
+ end
+ end
+
+ describe '#lookup_paths' do
+ let(:domain) { instance_double(PagesDomain) }
+ let(:project_a) { instance_double(Project) }
+ let(:project_z) { instance_double(Project) }
+ let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa') }
+ let(:pages_lookup_path_z) { instance_double(Pages::LookupPath, prefix: 'zzz') }
+
+ subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
+
+ it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
+ expect(project_a).to receive(:pages_lookup_path).with(domain: domain).and_return(pages_lookup_path_a)
+ expect(project_z).to receive(:pages_lookup_path).with(domain: domain).and_return(pages_lookup_path_z)
+
+ expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
+ end
+ end
+end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 5168064bb84..f745820a404 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -556,4 +556,16 @@ describe PagesDomain do
)
end
end
+
+ describe '.pages_virtual_domain' do
+ let(:project) { build(:project) }
+
+ subject(:pages_domain) { build(:pages_domain, project: project) }
+
+ it 'returns instance of Pages::VirtualDomain' do
+ expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
+
+ expect(pages_domain.pages_virtual_domain).to be_a(Pages::VirtualDomain)
+ end
+ end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index dc7a8433064..e6505bb4a51 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -174,4 +174,58 @@ describe ProjectFeature do
it { is_expected.to eq(ProjectFeature::ENABLED) }
end
end
+
+ describe '#public_pages?' do
+ it 'returns true if Pages access controll is not enabled' do
+ stub_config(pages: { access_control: false })
+
+ project_feature = described_class.new
+
+ expect(project_feature.public_pages?).to eq(true)
+ end
+
+ context 'Pages access control is enabled' do
+ before do
+ stub_config(pages: { access_control: true })
+ end
+
+ it 'returns true if Pages access level is public' do
+ project_feature = described_class.new(pages_access_level: described_class::PUBLIC)
+
+ expect(project_feature.public_pages?).to eq(true)
+ end
+
+ it 'returns true if Pages access level is enabled and the project is public' do
+ project = build(:project, :public)
+
+ project_feature = described_class.new(project: project, pages_access_level: described_class::ENABLED)
+
+ expect(project_feature.public_pages?).to eq(true)
+ end
+
+ it 'returns false if pages or the project are not public' do
+ project = build(:project, :private)
+
+ project_feature = described_class.new(project: project, pages_access_level: described_class::ENABLED)
+
+ expect(project_feature.public_pages?).to eq(false)
+ end
+ end
+
+ describe '#private_pages?' do
+ subject(:project_feature) { described_class.new }
+
+ it 'returns false if public_pages? is true' do
+ expect(project_feature).to receive(:public_pages?).and_return(true)
+
+ expect(project_feature.private_pages?).to eq(false)
+ end
+
+ it 'returns true if public_pages? is false' do
+ expect(project_feature).to receive(:public_pages?).and_return(false)
+
+ expect(project_feature.private_pages?).to eq(true)
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e2a684c42ae..5b4b9c516a0 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5012,6 +5012,17 @@ describe Project do
end
end
+ describe '#pages_lookup_path' do
+ let(:pages_domain) { build(:pages_domain) }
+ let(:project) { build(:project) }
+
+ it 'returns instance of Pages::LookupPath' do
+ expect(Pages::LookupPath).to receive(:new).with(project, domain: pages_domain).and_call_original
+
+ expect(project.pages_lookup_path(domain: pages_domain)).to be_a(Pages::LookupPath)
+ end
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6722a3c627d..c339fad778b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -945,6 +945,16 @@ describe User do
end
end
+ describe 'static object token' do
+ it 'ensures a static object token on read' do
+ user = create(:user, static_object_token: nil)
+ static_object_token = user.static_object_token
+
+ expect(static_object_token).not_to be_blank
+ expect(user.reload.static_object_token).to eq static_object_token
+ end
+ end
+
describe '#recently_sent_password_reset?' do
it 'is false when reset_password_sent_at is nil' do
user = build_stubbed(:user, reset_password_sent_at: nil)
diff --git a/spec/presenters/event_presenter_spec.rb b/spec/presenters/event_presenter_spec.rb
new file mode 100644
index 00000000000..79f5e359141
--- /dev/null
+++ b/spec/presenters/event_presenter_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EventPresenter do
+ include Gitlab::Routing.url_helpers
+
+ set(:group) { create(:group) }
+ set(:project) { create(:project, group: group) }
+ set(:target) { create(:milestone, project: project) }
+ set(:group_event) { create(:event, :created, project: nil, group: group, target: target) }
+ set(:project_event) { create(:event, :created, project: project, target: target) }
+
+ describe '#resource_parent_name' do
+ context 'with group event' do
+ subject { group_event.present.resource_parent_name }
+
+ it { is_expected.to eq(group.full_name) }
+ end
+
+ context 'with project label' do
+ subject { project_event.present.resource_parent_name }
+
+ it { is_expected.to eq(project.full_name) }
+ end
+ end
+
+ describe '#target_link_options' do
+ context 'with group event' do
+ subject { group_event.present.target_link_options }
+
+ it { is_expected.to eq([group, target]) }
+ end
+
+ context 'with project label' do
+ subject { project_event.present.target_link_options }
+
+ it { is_expected.to eq([group.becomes(Namespace), project, target]) }
+ end
+ end
+end
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index e47166544d9..53fc3096751 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::Applications, :api do
- include ApiHelpers
-
let(:admin_user) { create(:user, admin: true) }
let(:user) { create(:user, admin: false) }
let!(:application) { create(:application, name: 'another_application', redirect_uri: 'http://other_application.url', scopes: '') }
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 018691e8099..2fc772b12af 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::Events do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index aceff9b4aa6..68df02d4d8d 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::ImportGithub do
- include ApiHelpers
-
let(:token) { "asdasd12345" }
let(:provider) { :github }
let(:access_params) { { github_access_token: token } }
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 0b3c5be9c45..e1b563b92f4 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -43,10 +43,32 @@ describe API::Internal::Pages do
super(host, headers)
end
- it 'responds with 200 OK' do
- query_host('pages.gitlab.io')
+ context 'not existing host' do
+ it 'responds with 404 Not Found' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'custom domain' do
+ let(:namespace) { create(:namespace, name: 'gitlab-org') }
+ let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
+ let!(:pages_domain) { create(:pages_domain, domain: 'pages.gitlab.io', project: project) }
+
+ it 'responds with the correct domain configuration' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ expect(json_response['certificate']).to eq(pages_domain.certificate)
+ expect(json_response['key']).to eq(pages_domain.key)
- expect(response).to have_gitlab_http_status(200)
+ lookup_path = json_response['lookup_paths'][0]
+ expect(lookup_path['prefix']).to eq('/')
+ expect(lookup_path['source']['path']).to eq('gitlab-org/gitlab-ce/public/')
+ end
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 15d6db42760..8179da2f97c 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1033,6 +1033,70 @@ describe API::MergeRequests do
end
end
+ describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:ci_yaml_file)
+ .and_return(YAML.dump({
+ rspec: {
+ script: 'ls',
+ only: ['merge_requests']
+ }
+ }))
+ end
+
+ let(:project) do
+ create(:project, :private, :repository,
+ creator: user,
+ namespace: user.namespace,
+ only_allow_merge_if_pipeline_succeeds: false)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, :with_detached_merge_request_pipeline,
+ milestone: milestone1,
+ author: user,
+ assignees: [user],
+ source_project: project,
+ target_project: project,
+ title: 'Test',
+ created_at: base_time)
+ end
+
+ let(:merge_request_iid) { merge_request.iid }
+ let(:authenticated_user) { user }
+
+ let(:request) do
+ post api("/projects/#{project.id}/merge_requests/#{merge_request_iid}/pipelines", authenticated_user)
+ end
+
+ context 'when authorized' do
+ it 'creates and returns the new Pipeline' do
+ expect { request }.to change(Ci::Pipeline, :count).by(1)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_a Hash
+ end
+ end
+
+ context 'when unauthorized' do
+ let(:authenticated_user) { create(:user) }
+
+ it 'responds with a blank 404' do
+ expect { request }.not_to change(Ci::Pipeline, :count)
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when the merge request does not exist' do
+ let(:merge_request_iid) { 777 }
+
+ it 'responds with a blank 404' do
+ expect { request }.not_to change(Ci::Pipeline, :count)
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
describe 'POST /projects/:id/merge_requests' do
context 'support for deprecated assignee_id' do
let(:params) do
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
index 43df9993eb9..8c2db6e4c62 100644
--- a/spec/requests/api/project_events_spec.rb
+++ b/spec/requests/api/project_events_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::ProjectEvents do
- include ApiHelpers
-
let(:user) { create(:user) }
let(:non_member) { create(:user) }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 8a3de2a52fc..7e2d70d6eb5 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -530,15 +530,22 @@ describe 'project routing' do
end
end
- # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/}
+ # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /[^\0]+/, project_id: /[^\/]+/}
describe Projects::BlameController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/blame/master/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blame', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/#{newline_file}" })
end
end
- # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/}
+ # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /[^\0]+/, project_id: /[^\/]+/}
describe Projects::BlobController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
@@ -547,28 +554,56 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
expect(get('/gitlab/gitlabhq/blob/master/blob/index.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/blob/index.js')
expect(get('/gitlab/gitlabhq/blob/blob/master/blob/index.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'blob/master/blob/index.js')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/blob/blob/master/blob/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blob', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "blob/master/blob/#{newline_file}" })
end
end
- # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/}
+ # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /[^\0]+/, project_id: /[^\/]+/}
describe Projects::TreeController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss')
expect(get('/gitlab/gitlabhq/tree/master/tree/files')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/tree/files')
expect(get('/gitlab/gitlabhq/tree/tree/master/tree/files')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'tree/master/tree/files')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/tree/master/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/tree', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/#{newline_file}" })
end
end
- # project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
+ # project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/[^\0]+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/}
describe Projects::FindFileController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/find_file/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/find_file', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "#{newline_file}" })
end
it 'to #list' do
expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.json')
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/files/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/find_file', action: 'list',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "#{newline_file}" })
end
end
@@ -578,6 +613,13 @@ describe 'project routing' do
route_to('projects/blob#edit',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: 'master/app/models/project.rb'))
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/edit/master/docs/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blob', action: 'edit',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/docs/#{newline_file}" })
end
it 'to #preview' do
@@ -585,6 +627,26 @@ describe 'project routing' do
route_to('projects/blob#preview',
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: 'master/app/models/project.rb'))
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/edit/master/docs/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/blob', action: 'edit',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/docs/#{newline_file}" })
+ end
+ end
+
+ # project_raw GET /:project_id/raw/:id(.:format) raw#show {id: /[^\0]+/, project_id: /[^\/]+/}
+ describe Projects::RawController, 'routing' do
+ it 'to #show' do
+ newline_file = "new\n\nline.txt"
+ url_encoded_newline_file = ERB::Util.url_encode(newline_file)
+ assert_routing({ path: "/gitlab/gitlabhq/raw/master/#{url_encoded_newline_file}",
+ method: :get },
+ { controller: 'projects/raw', action: 'show',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "master/#{newline_file}" })
end
end
diff --git a/spec/rubocop/cop/scalability/file_uploads_spec.rb b/spec/rubocop/cop/scalability/file_uploads_spec.rb
new file mode 100644
index 00000000000..2a94fde5ba2
--- /dev/null
+++ b/spec/rubocop/cop/scalability/file_uploads_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/scalability/file_uploads'
+
+describe RuboCop::Cop::Scalability::FileUploads do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+ let(:message) { 'Do not upload files without workhorse acceleration. Please refer to https://docs.gitlab.com/ee/development/uploads.html' }
+
+ context 'with required params' do
+ it 'detects File in types array' do
+ expect_offense(<<~PATTERN.strip_indent)
+ params do
+ requires :certificate, allow_blank: false, types: [String, File]
+ ^^^^ #{message}
+ end
+ PATTERN
+ end
+
+ it 'detects File as type argument' do
+ expect_offense(<<~PATTERN.strip_indent)
+ params do
+ requires :attachment, type: File
+ ^^^^ #{message}
+ end
+ PATTERN
+ end
+ end
+
+ context 'with optional params' do
+ it 'detects File in types array' do
+ expect_offense(<<~PATTERN.strip_indent)
+ params do
+ optional :certificate, allow_blank: false, types: [String, File]
+ ^^^^ #{message}
+ end
+ PATTERN
+ end
+
+ it 'detects File as type argument' do
+ expect_offense(<<~PATTERN.strip_indent)
+ params do
+ optional :attachment, type: File
+ ^^^^ #{message}
+ end
+ PATTERN
+ end
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index fe7c6fe4700..281c7438eee 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -40,7 +40,7 @@ describe Ci::RetryBuildService do
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines artifacts_file_store artifacts_metadata_store
metadata runner_session trace_chunks upstream_pipeline_id
- artifacts_file artifacts_metadata artifacts_size].freeze
+ artifacts_file artifacts_metadata artifacts_size commands].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index f18239f6d39..d546a092680 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -49,6 +49,22 @@ describe MergeRequests::BuildService do
allow(project).to receive(:commit).and_return(commit_2)
end
+ shared_examples 'allows the merge request to be created' do
+ it do
+ expect(merge_request.can_be_created).to eq(true)
+ end
+ end
+
+ shared_examples 'forbids the merge request from being created' do
+ it 'returns that the merge request cannot be created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly(*Array(error_message))
+ end
+ end
+
describe '#execute' do
it 'calls the compare service with the correct arguments' do
allow_any_instance_of(described_class).to receive(:projects_and_branches_valid?).and_return(true)
@@ -79,12 +95,8 @@ describe MergeRequests::BuildService do
context 'missing source branch' do
let(:source_branch) { '' }
- it 'forbids the merge request from being created' do
- expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds an error message to the merge request' do
- expect(merge_request.errors).to contain_exactly('You must select source and target branch')
+ it_behaves_like 'forbids the merge request from being created' do
+ let(:error_message) { 'You must select source and target branch' }
end
end
@@ -96,25 +108,44 @@ describe MergeRequests::BuildService do
stub_compare
end
- it 'creates compare object with target branch as default branch' do
- expect(merge_request.compare).to be_present
- expect(merge_request.target_branch).to eq(project.default_branch)
- end
+ context 'when source branch' do
+ context 'is not the repository default branch' do
+ it 'creates compare object with target branch as default branch' do
+ expect(merge_request.compare).to be_present
+ expect(merge_request.target_branch).to eq(project.default_branch)
+ end
+
+ it_behaves_like 'allows the merge request to be created'
+ end
+
+ context 'the repository default branch' do
+ let(:source_branch) { 'master' }
+
+ it_behaves_like 'forbids the merge request from being created' do
+ let(:error_message) { 'You must select source and target branch' }
+ end
- it 'allows the merge request to be created' do
- expect(merge_request.can_be_created).to eq(true)
+ context 'when source project is different from the target project' do
+ let(:target_project) { create(:project, :public, :repository) }
+ let!(:project) { fork_project(target_project, user, namespace: user.namespace, repository: true) }
+ let(:source_project) { project }
+
+ it 'creates compare object with target branch as default branch' do
+ expect(merge_request.compare).to be_present
+ expect(merge_request.target_branch).to eq(project.default_branch)
+ end
+
+ it_behaves_like 'allows the merge request to be created'
+ end
+ end
end
end
context 'same source and target branch' do
let(:source_branch) { 'master' }
- it 'forbids the merge request from being created' do
- expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds an error message to the merge request' do
- expect(merge_request.errors).to contain_exactly('You must select different branches')
+ it_behaves_like 'forbids the merge request from being created' do
+ let(:error_message) { 'You must select different branches' }
end
end
@@ -125,9 +156,7 @@ describe MergeRequests::BuildService do
stub_compare
end
- it 'allows the merge request to be created' do
- expect(merge_request.can_be_created).to eq(true)
- end
+ it_behaves_like 'allows the merge request to be created'
it 'adds a WIP prefix to the merge request title' do
expect(merge_request.title).to eq('WIP: Feature branch')
@@ -142,9 +171,7 @@ describe MergeRequests::BuildService do
stub_compare
end
- it 'allows the merge request to be created' do
- expect(merge_request.can_be_created).to eq(true)
- end
+ it_behaves_like 'allows the merge request to be created'
it 'uses the title of the commit as the title of the merge request' do
expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first)
@@ -254,9 +281,7 @@ describe MergeRequests::BuildService do
stub_compare
end
- it 'allows the merge request to be created' do
- expect(merge_request.can_be_created).to eq(true)
- end
+ it_behaves_like 'allows the merge request to be created'
it 'uses the title of the branch as the merge request title' do
expect(merge_request.title).to eq('Feature branch')
@@ -340,12 +365,8 @@ describe MergeRequests::BuildService do
allow(project).to receive(:commit).with(target_branch).and_return(commit_1)
end
- it 'forbids the merge request from being created' do
- expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds an error message to the merge request' do
- expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist')
+ it_behaves_like 'forbids the merge request from being created' do
+ let(:error_message) { 'Source branch "feature-branch" does not exist' }
end
end
@@ -355,12 +376,8 @@ describe MergeRequests::BuildService do
allow(project).to receive(:commit).with(target_branch).and_return(nil)
end
- it 'forbids the merge request from being created' do
- expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds an error message to the merge request' do
- expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist')
+ it_behaves_like 'forbids the merge request from being created' do
+ let(:error_message) { 'Target branch "master" does not exist' }
end
end
@@ -369,15 +386,10 @@ describe MergeRequests::BuildService do
allow(project).to receive(:commit).and_return(nil)
end
- it 'forbids the merge request from being created' do
- expect(merge_request.can_be_created).to eq(false)
- end
-
- it 'adds both error messages to the merge request' do
- expect(merge_request.errors).to contain_exactly(
- 'Source branch "feature-branch" does not exist',
- 'Target branch "master" does not exist'
- )
+ it_behaves_like 'forbids the merge request from being created' do
+ let(:error_message) do
+ ['Source branch "feature-branch" does not exist', 'Target branch "master" does not exist']
+ end
end
end
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
index 9479439bde8..576e8498e4d 100644
--- a/spec/services/merge_requests/create_pipeline_service_spec.rb
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -38,6 +38,10 @@ describe MergeRequests::CreatePipelineService do
expect(subject).to be_detached_merge_request_pipeline
end
+ it 'defaults to merge_request_event' do
+ expect(subject.source).to eq('merge_request_event')
+ end
+
context 'when service is called multiple times' do
it 'creates a pipeline once' do
expect do
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index 62a70ccd3da..0f829df90b3 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -42,6 +42,14 @@ describe Search::GlobalService do
expect(results.objects('projects')).to match_array [found_project]
end
+
+ it 'does not return archived projects' do
+ archived_project = create(:project, :public, archived: true, name: 'archived_project')
+
+ results = described_class.new(user, search: "archived").execute
+
+ expect(results.objects('projects')).not_to include(archived_project)
+ end
end
end
end
diff --git a/spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb b/spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb
new file mode 100644
index 00000000000..bd84bd1093f
--- /dev/null
+++ b/spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+shared_examples 'paginated collection' do
+ let(:collection) { nil }
+ let(:last_page) { collection.page.total_pages }
+ let(:action) { :index }
+ let(:params) { {} }
+
+ it 'renders a page number that is not ouf of range' do
+ get action, params: params.merge(page: last_page)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'redirects to last_page if page number is larger than number of pages' do
+ get action, params: params.merge(page: last_page + 1)
+
+ expect(response).to redirect_to(params.merge(page: last_page))
+ end
+
+ it 'does not redirect to external sites when provided a host field' do
+ external_host = 'www.example.com'
+
+ get action, params: params.merge(page: last_page + 1, host: external_host)
+
+ expect(response).to redirect_to(params.merge(page: last_page))
+ end
+end
diff --git a/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb b/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb
new file mode 100644
index 00000000000..920fcbde483
--- /dev/null
+++ b/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+shared_examples 'archive download buttons' do
+ let(:formats) { %w(zip tar.gz tar.bz2 tar) }
+ let(:path_to_visit) { project_path(project) }
+ let(:ref) { project.default_branch }
+
+ context 'when static objects external storage is enabled' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
+ visit path_to_visit
+ end
+
+ context 'private project' do
+ it 'shows archive download buttons with external storage URL prepended and user token appended to their href' do
+ formats.each do |format|
+ path = archive_path(project, ref, format)
+ uri = URI('https://cdn.gitlab.com')
+ uri.path = path
+ uri.query = "token=#{user.static_object_token}"
+
+ expect(page).to have_link format, href: uri.to_s
+ end
+ end
+ end
+
+ context 'public project' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'shows archive download buttons with external storage URL prepended to their href' do
+ formats.each do |format|
+ path = archive_path(project, ref, format)
+ uri = URI('https://cdn.gitlab.com')
+ uri.path = path
+
+ expect(page).to have_link format, href: uri.to_s
+ end
+ end
+ end
+ end
+
+ context 'when static objects external storage is disabled' do
+ before do
+ visit path_to_visit
+ end
+
+ it 'shows default archive download buttons' do
+ formats.each do |format|
+ path = archive_path(project, ref, format)
+
+ expect(page).to have_link format, href: path
+ end
+ end
+ end
+
+ def archive_path(project, ref, format)
+ project_archive_path(project, id: "#{ref}/#{project.path}-#{ref}", path: nil, format: format)
+ end
+end