summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore2
-rw-r--r--.gitignore1
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml9
-rw-r--r--.gitlab/issue_templates/Feature proposal.md12
-rw-r--r--.markdownlint.json1
-rw-r--r--.rubocop.yml5
-rw-r--r--CHANGELOG.md7
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock8
-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/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/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue38
-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/vue_shared/components/gl_toggle_vuex.vue49
-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/pages/settings.scss10
-rw-r--r--app/assets/stylesheets/utilities.scss6
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/controllers/admin/applications_controller.rb4
-rw-r--r--app/controllers/clusters/clusters_controller.rb13
-rw-r--r--app/controllers/concerns/issuable_collections.rb28
-rw-r--r--app/controllers/concerns/paginated_collection.rb19
-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/projects/forks_controller.rb15
-rw-r--r--app/controllers/projects/services_controller.rb27
-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_settings_helper.rb4
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/releases_helper.rb38
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/service.rb7
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/user.rb8
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/event_presenter.rb20
-rw-r--r--app/presenters/instance_clusterable_presenter.rb4
-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/applications/index.html.haml3
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml8
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml11
-rw-r--r--app/views/clusters/clusters/eks/_index.html.haml1
-rw-r--r--app/views/clusters/clusters/new.html.haml44
-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/projects/buttons/_fork.html.haml2
-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.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/views/shared/projects/_list.html.haml6
-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/46686-bump-kubeclient-version-qa.yml5
-rw-r--r--changelogs/unreleased/60724-watch-button.yml5
-rw-r--r--changelogs/unreleased/64122-documentation-lacks-how-to-enable-project-snippets.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/api_settings.yml5
-rw-r--r--changelogs/unreleased/fj-11777-lower-search-count-limits.yml5
-rw-r--r--changelogs/unreleased/fj-62807-not-prefill-target-branch.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.62.0.yml5
-rw-r--r--changelogs/unreleased/issue-64738.yml5
-rw-r--r--changelogs/unreleased/issue-67127.yml5
-rw-r--r--changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml5
-rw-r--r--changelogs/unreleased/job-rules-e2e.yml5
-rw-r--r--changelogs/unreleased/kamil-improve-import-export.yml5
-rw-r--r--changelogs/unreleased/pl-project-service-json.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--config/gitlab.yml.example5
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/routes/repository.rb2
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--danger/commit_messages/Dangerfile29
-rw-r--r--db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb21
-rw-r--r--db/migrate/20190826100605_add_group_column_to_events.rb14
-rw-r--r--db/post_migrate/20190910000130_add_index_on_application_id_on_oauth_access_tokens.rb17
-rw-r--r--db/schema.rb5
-rw-r--r--doc/administration/database_load_balancing.md6
-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.md1
-rw-r--r--doc/administration/repository_storage_types.md2
-rw-r--r--doc/administration/smime_signing_email.md51
-rw-r--r--doc/administration/troubleshooting/sidekiq.md2
-rw-r--r--doc/api/merge_requests.md60
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/tags.md9
-rw-r--r--doc/ci/variables/predefined_variables.md1
-rw-r--r--doc/ci/yaml/README.md4
-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/styleguide.md43
-rw-r--r--doc/development/fe_guide/vuex.md2
-rw-r--r--doc/development/instrumentation.md4
-rw-r--r--doc/development/performance.md26
-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/aws/index.md6
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/migrate_ci_to_ce/README.md3
-rw-r--r--doc/raketasks/backup_restore.md47
-rw-r--r--doc/raketasks/import.md10
-rw-r--r--doc/security/asset_proxy.md72
-rw-r--r--doc/topics/autodevops/index.md10
-rw-r--r--doc/update/mysql_to_postgresql.md10
-rw-r--r--doc/user/application_security/dependency_scanning/index.md70
-rw-r--r--doc/user/application_security/index.md25
-rw-r--r--doc/user/clusters/applications.md42
-rw-r--r--doc/user/markdown.md64
-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/canary_deployments.md2
-rw-r--r--doc/user/project/import/github.md29
-rw-r--r--doc/user/project/integrations/webhooks.md3
-rw-r--r--doc/user/project/merge_requests/index.md5
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md6
-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/settings/index.md2
-rw-r--r--doc/user/snippets.md5
-rw-r--r--doc/workflow/lfs/lfs_administration.md6
-rw-r--r--doc/workflow/repository_mirroring.md2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb4
-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.rb27
-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/settings.rb8
-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/ci/build/rules.rb9
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb10
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb4
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb1
-rw-r--r--lib/gitlab/i18n/po_linter.rb8
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb19
-rw-r--r--lib/gitlab/import_export/fast_hash_serializer.rb108
-rw-r--r--lib/gitlab/import_export/import_export.yml10
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb8
-rw-r--r--lib/gitlab/issuable_metadata.rb4
-rw-r--r--lib/gitlab/noteable_metadata.rb33
-rw-r--r--lib/gitlab/pages.rb17
-rw-r--r--lib/gitlab/repository_cache_adapter.rb65
-rw-r--r--lib/gitlab/repository_set_cache.rb67
-rw-r--r--lib/gitlab/search_results.rb5
-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/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/gitlab/graphql.rake4
-rw-r--r--lib/tasks/gitlab/uploads/legacy.rake2
-rw-r--r--locale/gitlab.pot25
-rw-r--r--qa/qa.rb1
-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/issue/show.rb5
-rw-r--r--qa/qa/page/project/milestone/index.rb2
-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/branch.rb12
-rw-r--r--qa/qa/resource/issue.rb3
-rw-r--r--qa/qa/resource/merge_request.rb4
-rw-r--r--qa/qa/resource/project_member.rb35
-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/admin/clusters_controller_spec.rb19
-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/groups/clusters_controller_spec.rb19
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb23
-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/services_controller_spec.rb130
-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_pipelines_spec.rb32
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb3
-rw-r--r--spec/features/projects/clusters_spec.rb24
-rw-r--r--spec/features/projects/fork_spec.rb1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/tag.json3
-rw-r--r--spec/fixtures/valid.po5
-rw-r--r--spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js115
-rw-r--r--spec/frontend/vue_shared/plugins/global_toast_spec.js24
-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/monitoring/components/graph_group_spec.js47
-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/lib/gitlab/ci/pipeline/seed/build_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb38
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/attributes_finder_spec.rb67
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb272
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb53
-rw-r--r--spec/lib/gitlab/noteable_metadata_spec.rb29
-rw-r--r--spec/lib/gitlab/pages_spec.rb29
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb6
-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/search_results_spec.rb17
-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/snippet_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb16
-rw-r--r--spec/models/ci/pipeline_spec.rb16
-rw-r--r--spec/models/oauth_access_token_spec.rb28
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/presenters/event_presenter_spec.rb41
-rw-r--r--spec/requests/api/internal/pages_spec.rb54
-rw-r--r--spec/requests/api/merge_requests_spec.rb64
-rw-r--r--spec/requests/api/settings_spec.rb38
-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/create_pipeline_service_spec.rb335
-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/helpers/search_helpers.rb4
-rw-r--r--spec/support/import_export/import_export.yml4
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/controllers/paginated_collection_shared_examples.rb30
301 files changed, 4676 insertions, 1174 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/.gitignore b/.gitignore
index fcbb4c352a9..7310c04d117 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,7 @@ eslint-report.html
/vendor/gitaly-ruby
/builds*
/.gitlab_workhorse_secret
+/.gitlab_pages_shared_secret
/webpack-report/
/knapsack/
/rspec_flaky/
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 9c021b23db6..ca89c4e2c89 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -28,7 +28,8 @@ package-and-qa-manual:master:
extends: .package-and-qa-base
only:
refs:
- - master
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
when: manual
needs: ["build-qa-image", "gitlab:assets:compile"]
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 6f1505b5c0d..8e612243371 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -134,6 +134,15 @@ 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
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index 5d93758595a..68f60cb52d4 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -5,7 +5,17 @@
### Intended users
<!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
-Personas can be found at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
+
+* [Parker (Product Manager)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#parker-product-manager)
+* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#delaney-development-team-lead)
+* [Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
+* [Presley (Product Designer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#presley-product-designer)
+* [Devon (DevOps Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#devon-devops-engineer)
+* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sidney-systems-administrator)
+* [Sam (Security Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sam-security-analyst)
+* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
+
+Personas are described at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
### Further details
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/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 91951fd8ad7..76d05362056 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.61.0
+1.62.0
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 9f9d8f487fe..3ead3cb5f36 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]
@@ -234,7 +236,7 @@ gem 'asana', '~> 0.8.1'
gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
-gem 'kubeclient', '~> 4.2.2'
+gem 'kubeclient', '~> 4.4.0'
# Sanitize user input
gem 'sanitize', '~> 4.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 011a365110c..301b54f9a9f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -398,7 +398,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)
@@ -505,7 +505,7 @@ GEM
kramdown (2.1.0)
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
- kubeclient (4.2.2)
+ kubeclient (4.4.0)
http (~> 3.0)
recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0)
@@ -1142,7 +1142,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)
@@ -1164,7 +1164,7 @@ DEPENDENCIES
jwt (~> 2.1.0)
kaminari (~> 1.0)
knapsack (~> 1.17)
- kubeclient (~> 4.2.2)
+ kubeclient (~> 4.4.0)
letter_opener_web (~> 1.3.4)
license_finder (~> 5.4)
licensee (~> 8.9)
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/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/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/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index 0f5c5b3d60f..72ddd8d4fcf 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -1,5 +1,10 @@
<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
export default {
+ components: {
+ Icon,
+ },
props: {
name: {
type: String,
@@ -15,15 +20,42 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ showGroup: true,
+ };
+ },
+ computed: {
+ caretIcon() {
+ return this.collapseGroup && this.showGroup ? 'angle-down' : 'angle-right';
+ },
+ },
+ created() {
+ this.showGroup = this.collapseGroup;
+ },
+ methods: {
+ collapse() {
+ this.showGroup = !this.showGroup;
+ },
+ },
};
</script>
<template>
<div v-if="showPanels" class="card prometheus-panel">
- <div class="card-header">
- <h4>{{ name }}</h4>
+ <div class="card-header d-flex align-items-center">
+ <h4 class="flex-grow-1">{{ name }}</h4>
+ <a role="button" @click="collapse">
+ <icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" />
+ </a>
+ </div>
+ <div
+ v-if="collapseGroup"
+ v-show="collapseGroup && showGroup"
+ class="card-body prometheus-graph-group"
+ >
+ <slot></slot>
</div>
- <div v-if="collapseGroup" class="card-body prometheus-graph-group"><slot></slot></div>
</div>
<div v-else class="prometheus-graph-group"><slot></slot></div>
</template>
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/vue_shared/components/gl_toggle_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue
new file mode 100644
index 00000000000..b649dac029a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlToggle } from '@gitlab/ui';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+export default {
+ name: 'GlToggleVuex',
+ components: {
+ GlToggle,
+ },
+ props: {
+ stateProperty: {
+ type: String,
+ required: true,
+ },
+ storeModule: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ setAction: {
+ type: String,
+ required: false,
+ default() {
+ return `set${capitalizeFirstCharacter(this.stateProperty)}`;
+ },
+ },
+ },
+ computed: {
+ value: {
+ get() {
+ const { state } = this.$store;
+ const { stateProperty, storeModule } = this;
+ return storeModule ? state[storeModule][stateProperty] : state[stateProperty];
+ },
+ set(value) {
+ const { stateProperty, storeModule, setAction } = this;
+ const action = storeModule ? `${storeModule}/${setAction}` : setAction;
+ this.$store.dispatch(action, { key: stateProperty, value });
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-toggle v-model="value">
+ <slot v-bind="{ value }"></slot>
+ </gl-toggle>
+</template>
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/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 79de1d78a6e..416537ef763 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -24,12 +24,16 @@
.settings {
// border-top for each item except the top one
- + .settings {
- border-top: 1px solid $border-color;
- }
+ border-top: 1px solid $border-color;
&:first-of-type {
margin-top: 10px;
+ border: 0;
+ }
+
+ + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid $border-color;
}
&.animating {
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/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 99411641874..f2f72bea5b4 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -85,7 +85,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
+ # TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204)
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
+ params.delete(:domain_blacklist_raw) if params[:domain_blacklist]
+ params.delete(:domain_whitelist_raw) if params[:domain_whitelist]
params.require(:application_setting).permit(
visible_application_setting_attributes
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/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index ec8077d18e3..bcd771dafcf 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -35,6 +35,12 @@ class Clusters::ClustersController < Clusters::BaseController
end
def new
+ return unless Feature.enabled?(:create_eks_clusters)
+
+ @gke_selected = params[:provider] == 'gke'
+ @eks_selected = params[:provider] == 'eks'
+
+ return redirect_to @authorize_url if @gke_selected && @authorize_url && !@valid_gcp_token
end
# Overridding ActionController::Metal#status is NOT a good idea
@@ -99,7 +105,7 @@ class Clusters::ClustersController < Clusters::BaseController
validate_gcp_token
user_cluster
- render :new, locals: { active_tab: 'gcp' }
+ render :new, locals: { active_tab: 'create' }
end
end
@@ -116,7 +122,7 @@ class Clusters::ClustersController < Clusters::BaseController
validate_gcp_token
gcp_cluster
- render :new, locals: { active_tab: 'user' }
+ render :new, locals: { active_tab: 'add' }
end
end
@@ -189,7 +195,8 @@ class Clusters::ClustersController < Clusters::BaseController
end
def generate_gcp_authorize_url
- state = generate_session_key_redirect(clusterable.new_path.to_s)
+ params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {}
+ state = generate_session_key_redirect(clusterable.new_path(params).to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
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/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/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/services_controller.rb b/app/controllers/projects/services_controller.rb
index e0df51590ae..c9f680a4696 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -18,10 +18,23 @@ class Projects::ServicesController < Projects::ApplicationController
def update
@service.attributes = service_params[:service]
- if @service.save(context: :manual_change)
- redirect_to(project_settings_integrations_path(@project), notice: success_message)
- else
- render 'edit'
+ saved = @service.save(context: :manual_change)
+
+ respond_to do |format|
+ format.html do
+ if saved
+ redirect_to project_settings_integrations_path(@project),
+ notice: success_message
+ else
+ render 'edit'
+ end
+ end
+
+ format.json do
+ status = saved ? :ok : :unprocessable_entity
+
+ render json: serialize_as_json, status: status
+ end
end
end
@@ -67,4 +80,10 @@ class Projects::ServicesController < Projects::ApplicationController
def ensure_service_enabled
render_404 unless service
end
+
+ def serialize_as_json
+ @service
+ .as_json(only: @service.json_fields)
+ .merge(errors: @service.errors.as_json)
+ end
end
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_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 84021d0da56..b1a6e988a1d 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -180,8 +180,12 @@ module ApplicationSettingsHelper
:default_projects_limit,
:default_snippet_visibility,
:disabled_oauth_sign_in_sources,
+ :domain_blacklist,
:domain_blacklist_enabled,
+ # TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204)
:domain_blacklist_raw,
+ :domain_whitelist,
+ # TODO Remove domain_whitelist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-ce/issues/67204)
:domain_whitelist_raw,
:outbound_local_requests_whitelist_raw,
:dsa_key_restriction,
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..a2cf081375e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -31,15 +31,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
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/issue.rb b/app/models/issue.rb
index 75d4fc8c1c5..7c5a139ab55 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -31,7 +31,7 @@ class Issue < ApplicationRecord
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 95daa48d4bc..901ebcf249f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -54,7 +54,7 @@ class MergeRequest < ApplicationRecord
belongs_to :head_pipeline, foreign_key: "head_pipeline_id", class_name: "Ci::Pipeline"
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 915978d37b8..7f46e5faf1a 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -37,7 +37,7 @@ class Milestone < ApplicationRecord
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
scope :of_projects, ->(ids) { where(project_id: ids) }
scope :of_groups, ->(ids) { where(group_id: ids) }
diff --git a/app/models/note.rb b/app/models/note.rb
index 0d024b0a25c..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
@@ -78,7 +76,7 @@ class Note < ApplicationRecord
# suggestions.delete_all calls
has_many :suggestions, -> { order(:relative_order) },
inverse_of: :note, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
- has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
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/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/service.rb b/app/models/service.rb
index 431c5881460..d866a51c42e 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -107,6 +107,13 @@ class Service < ApplicationRecord
[]
end
+ # Expose a list of fields in the JSON endpoint.
+ #
+ # This list is used in `Service#as_json(only: json_fields)`.
+ def json_fields
+ %w(active)
+ end
+
def test_data(project, user)
Gitlab::DataBuilder::Push.build_sample(project, user)
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 67d730e2fa3..9ca01715578 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,12 +23,6 @@ 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
@@ -131,7 +125,7 @@ class User < ApplicationRecord
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
- has_many :events, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :events, dependent: :delete_all, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :releases, dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index d1bf0344b66..49c64b31fc7 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -25,8 +25,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters])
end
- def new_path
- new_polymorphic_path([clusterable, :cluster])
+ def new_path(options = {})
+ new_polymorphic_path([clusterable, :cluster], options)
end
def create_user_clusters_path
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/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index f8bbe5216f1..cce400ad2a1 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -18,8 +18,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
end
override :new_path
- def new_path
- new_admin_cluster_path
+ def new_path(options = {})
+ new_admin_cluster_path(options)
end
override :cluster_status_cluster_path
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/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/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
new file mode 100644
index 00000000000..f707c6585ec
--- /dev/null
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
@@ -0,0 +1,8 @@
+- provider = local_assigns.fetch(:provider)
+- logo_path = local_assigns.fetch(:logo_path)
+- label = local_assigns.fetch(:label)
+
+= link_to clusterable.new_path(provider: provider), class: 'btn gl-button btn-outline flex-fill d-inline-flex flex-column mr-3 justify-content-center align-items-center' do
+ = image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13'
+ %span
+ = label
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
new file mode 100644
index 00000000000..24506205243
--- /dev/null
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
@@ -0,0 +1,11 @@
+- gke_label = s_('ClusterIntegration|Google GKE')
+- eks_label = s_('ClusterIntegration|Amazon EKS')
+- create_cluster_label = s_('ClusterIntegration|Create cluster on')
+.d-flex.flex-column
+ %h5
+ = create_cluster_label
+ .d-flex
+ = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
+ locals: { provider: 'gke', label: gke_label, logo_path: '' }
+ = render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
+ locals: { provider: 'eks', label: eks_label, logo_path: '' }
diff --git a/app/views/clusters/clusters/eks/_index.html.haml b/app/views/clusters/clusters/eks/_index.html.haml
new file mode 100644
index 00000000000..ca8e9ba527a
--- /dev/null
+++ b/app/views/clusters/clusters/eks/_index.html.haml
@@ -0,0 +1 @@
+.js-create-eks-cluster-form-container
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index 6a8af23e5e8..fb182d99ff0 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title _('Kubernetes')
- page_title _('Kubernetes Cluster')
-- active_tab = local_assigns.fetch(:active_tab, 'gcp')
+- create_eks_enabled = Feature.enabled?(:create_eks_clusters)
+- active_tab = local_assigns.fetch(:active_tab, 'create')
+- link_end = '<a/>'.html_safe
= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
@@ -11,26 +13,36 @@
.col-md-9.js-toggle-container
%ul.nav-links.nav-tabs.gitlab-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#create-gcp-cluster-pane', id: 'create-gcp-cluster-tab', class: active_when(active_tab == 'gcp'), data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' }
%span Create new Cluster on GKE
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#add-user-cluster-pane', id: 'add-user-cluster-tab', class: active_when(active_tab == 'user'), data: { toggle: 'tab' }, role: 'tab' }
+ %a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' }
%span Add existing cluster
.tab-content.gitlab-tab-content
- .tab-pane{ id: 'create-gcp-cluster-pane', class: active_when(active_tab == 'gcp'), role: 'tabpanel' }
- = render 'clusters/clusters/gcp/header'
- - if @valid_gcp_token
- = render 'clusters/clusters/gcp/form'
- - elsif @authorize_url
- .signin-with-google
- = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url)
- = _('or')
- = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer')
- - else
- - link = link_to(s_('ClusterIntegration|properly configured'), help_page_path("integration/google"), target: '_blank', rel: 'noopener noreferrer')
- = s_('Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_to_documentation: link }
+ - if create_eks_enabled
+ .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
+ - if @gke_selected && @valid_gcp_token
+ = render 'clusters/clusters/gcp/header'
+ = render 'clusters/clusters/gcp/form'
+ - elsif @eks_selected
+ = render 'clusters/clusters/eks/index'
+ - else
+ = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
+ - else
+ .tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
+ = render 'clusters/clusters/gcp/header'
+ - if @valid_gcp_token
+ = render 'clusters/clusters/gcp/form'
+ - elsif @authorize_url
+ .signin-with-google
+ - create_account_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral' }
+ = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px', alt: _('Sign in with Google')), @authorize_url)
+ = s_('or %{link_start}create a new Google account%{link_end}').html_safe % { link_start: create_account_link, link_end: link_end }
+ - else
+ - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
+ = s_('Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }
- .tab-pane{ id: 'add-user-cluster-pane', class: active_when(active_tab == 'user'), role: 'tabpanel' }
+ .tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
= render 'clusters/clusters/user/header'
= render 'clusters/clusters/user/form'
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/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index bc0a89bea62..4b82eb2c5ef 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -8,7 +8,7 @@
- else
- can_create_fork = current_user.can?(:create_fork)
= link_to new_project_fork_path(@project),
- class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}",
+ class: "btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}",
title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do
= sprite_icon('fork', { css_class: 'icon' })
%span= s_('ProjectOverview|Fork')
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.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 837707707a9..3b26b8df8a1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -41,7 +41,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present?
- = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
+ = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' }
- else
%span.no-value
= _('None')
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/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index b4266937a4e..441abd57334 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -17,14 +17,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
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/_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/46686-bump-kubeclient-version-qa.yml b/changelogs/unreleased/46686-bump-kubeclient-version-qa.yml
new file mode 100644
index 00000000000..b4adbe4dcea
--- /dev/null
+++ b/changelogs/unreleased/46686-bump-kubeclient-version-qa.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Kubeclient to 4.4.0
+merge_request: 32811
+author:
+type: other
diff --git a/changelogs/unreleased/60724-watch-button.yml b/changelogs/unreleased/60724-watch-button.yml
new file mode 100644
index 00000000000..f22f7708ed2
--- /dev/null
+++ b/changelogs/unreleased/60724-watch-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fix watch button styling and notifications buttons consistency
+merge_request: 32827
+author:
+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/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/api_settings.yml b/changelogs/unreleased/api_settings.yml
new file mode 100644
index 00000000000..58830a5ab97
--- /dev/null
+++ b/changelogs/unreleased/api_settings.yml
@@ -0,0 +1,5 @@
+---
+title: Improve application settings API
+merge_request: 31149
+author: Mathieu Parent
+type: fixed
diff --git a/changelogs/unreleased/fj-11777-lower-search-count-limits.yml b/changelogs/unreleased/fj-11777-lower-search-count-limits.yml
new file mode 100644
index 00000000000..c284bc49bfc
--- /dev/null
+++ b/changelogs/unreleased/fj-11777-lower-search-count-limits.yml
@@ -0,0 +1,5 @@
+---
+title: Lower search counters
+merge_request: 11777
+author:
+type: performance
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/gitaly-version-v1.62.0.yml b/changelogs/unreleased/gitaly-version-v1.62.0.yml
new file mode 100644
index 00000000000..6ae200bd7f4
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.62.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.62.0
+merge_request: 32608
+author:
+type: changed
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/issue-67127.yml b/changelogs/unreleased/issue-67127.yml
new file mode 100644
index 00000000000..70848dc6ff9
--- /dev/null
+++ b/changelogs/unreleased/issue-67127.yml
@@ -0,0 +1,5 @@
+---
+title: Expose 'protected' field for Tag API endpoint.
+merge_request: 32790
+author: Andrea Leone
+type: added
diff --git a/changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml b/changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml
new file mode 100644
index 00000000000..148e306c3b8
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-add-caret-icon-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Add caret icons to the monitoring dashboard
+merge_request: 32239
+author:
+type: changed
diff --git a/changelogs/unreleased/job-rules-e2e.yml b/changelogs/unreleased/job-rules-e2e.yml
new file mode 100644
index 00000000000..2c55dfcec49
--- /dev/null
+++ b/changelogs/unreleased/job-rules-e2e.yml
@@ -0,0 +1,5 @@
+---
+title: Passing job rules downstream and E2E specs for job:rules configuration
+merge_request: 32609
+author:
+type: fixed
diff --git a/changelogs/unreleased/kamil-improve-import-export.yml b/changelogs/unreleased/kamil-improve-import-export.yml
new file mode 100644
index 00000000000..9d485e0df2d
--- /dev/null
+++ b/changelogs/unreleased/kamil-improve-import-export.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce N+1 when doing project export
+merge_request: 32423
+author:
+type: performance
diff --git a/changelogs/unreleased/pl-project-service-json.yml b/changelogs/unreleased/pl-project-service-json.yml
new file mode 100644
index 00000000000..a273289d871
--- /dev/null
+++ b/changelogs/unreleased/pl-project-service-json.yml
@@ -0,0 +1,5 @@
+---
+title: Expose update project service endpoint JSON
+merge_request: 32759
+author:
+type: added
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/config/gitlab.yml.example b/config/gitlab.yml.example
index e3693f612e3..87159b695f9 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -321,6 +321,9 @@ production: &base
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
admin:
address: unix:/home/git/gitlab/tmp/sockets/private/pages-admin.socket # TCP connections are supported too (e.g. tcp://host:port)
+ # File that contains the shared secret key for verifying access for gitlab-pages.
+ # Default is '.gitlab_pages_shared_secret' relative to Rails.root (i.e. root of the GitLab app).
+ # secret_file: /home/git/gitlab/.gitlab_pages_shared_secret
## Mattermost
## For enabling Add to Mattermost button
@@ -339,7 +342,7 @@ production: &base
## Sidekiq
sidekiq:
- log_format: default # (json is also supported)
+ log_format: json # (default is the original format)
## Auxiliary jobs
# Periodically executed jobs, to self-heal GitLab, do external synchronizations, etc.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4160f488a7a..dbbb7ba1b60 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -292,6 +292,7 @@ Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pa
Settings.pages['admin'] ||= Settingslogic.new({})
Settings.pages.admin['certificate'] ||= ''
+Settings.pages['secret_file'] ||= Rails.root.join('.gitlab_pages_shared_secret')
#
# Geo
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/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/sidekiq_queues.yml b/config/sidekiq_queues.yml
index e89e9657314..f37cd518d48 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -116,3 +116,4 @@
- [incident_management, 2]
- [jira_connect, 1]
- [update_external_pull_requests, 3]
+ - [refresh_license_compliance_checks, 2]
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/20190806071559_remove_epic_issues_default_relative_position.rb b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
index f6db90f6637..3037f2ea106 100644
--- a/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
+++ b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
@@ -3,8 +3,23 @@
class RemoveEpicIssuesDefaultRelativePosition < ActiveRecord::Migration[5.2]
DOWNTIME = false
- def change
- change_column_null :epic_issues, :relative_position, true
- change_column_default :epic_issues, :relative_position, from: 1073741823, to: nil
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ # The column won't exist if someone installed EE, downgraded to CE
+ # before it was added in EE, then tries to upgrade CE.
+ if column_exists?(:epic_issues, :relative_position)
+ change_column_null :epic_issues, :relative_position, true
+ change_column_default :epic_issues, :relative_position, from: 1073741823, to: nil
+ else
+ add_column_with_default(:epic_issues, :relative_position, :integer, default: nil, allow_null: true)
+ end
+ end
+
+ def down
+ change_column_default :epic_issues, :relative_position, from: nil, to: 1073741823
+ change_column_null :epic_issues, :relative_position, false
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/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..14ce50b0619 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"
@@ -2390,6 +2390,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
@@ -3864,7 +3865,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/database_load_balancing.md b/doc/administration/database_load_balancing.md
index f643d853d10..6620989983f 100644
--- a/doc/administration/database_load_balancing.md
+++ b/doc/administration/database_load_balancing.md
@@ -148,9 +148,9 @@ The following options can be set:
If `record_type` is set to `SRV`, GitLab will continue to use a round-robin algorithm
and will ignore the `weight` and `priority` in the record. Since SRV records usually
return hostnames instead of IPs, GitLab will look for the IPs of returned hostnames
-in the additional section of the SRV response. If no IP is found for a hostname, Gitlab
-will query the configured `nameserver` for ANY record for each such hostname looking for A or AAAA records, eventually
-dropping this hostname from rotation if it can't resolve its IP.
+in the additional section of the SRV response. If no IP is found for a hostname, GitLab
+will query the configured `nameserver` for ANY record for each such hostname looking for A or AAAA
+records, eventually dropping this hostname from rotation if it can't resolve its IP.
The `interval` value specifies the _minimum_ time between checks. If the A
record has a TTL greater than this value, then service discovery will honor said
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..b58291b7478 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).
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 3ee8673b297..d2d7ebdd634 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -10,7 +10,7 @@ that can be:
- Mounted to the local disk
- Exposed as an NFS shared volume
-- Acessed via [gitaly] on its own machine.
+- Accessed via [gitaly] on its own machine.
In GitLab, this is configured in `/etc/gitlab/gitlab.rb` by the `git_data_dirs({})`
configuration hash. The storage layouts discussed here will apply to any shard
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/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/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/api/settings.md b/doc/api/settings.md
index a14b0d3632a..4ad4ebdacb6 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -210,7 +210,7 @@ are listed in the descriptions of the relevant settings.
| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). |
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |
-| `domain_blacklist` | array of strings | required by: `domain_blacklist_enabled` | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
+| `domain_blacklist` | array of strings | no | Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
| `domain_blacklist_enabled` | boolean | no | (**If enabled, requires:** `domain_blacklist`) Allows blocking sign-ups from emails from specific domains. |
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 1d874fea1f8..88f63d6b34b 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -46,7 +46,8 @@ Parameters:
},
"name": "v1.0.0",
"target": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": null
+ "message": null,
+ "protected": true
}
]
```
@@ -94,7 +95,8 @@ Example Response:
"committer_email": "contact@arthurverschaeve.be",
"committed_date": "2015-02-01T21:56:31.000+01:00"
},
- "release": null
+ "release": null,
+ "protected": false
}
```
@@ -138,7 +140,8 @@ Parameters:
},
"name": "v1.0.0",
"target": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": null
+ "message": null,
+ "protected": false
}
```
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 8f2e95dbb10..8aae0e85c89 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2086,7 +2086,9 @@ staging:
### `interruptible`
-`interruptible` is used to indicate that a job should be canceled if made redundant by a newer run of the same job. Defaults to `false` if there is an environment defined and `true` otherwise.
+> [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)
is enabled.
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/styleguide.md b/doc/development/documentation/styleguide.md
index 09dd31e2aee..39b5e191a7b 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -85,7 +85,7 @@ The more we reflexively add useful information to the docs, the more (and more s
If you have questions when considering, authoring, or editing docs, ask the Technical Writing team on Slack in `#docs` or in GitLab by mentioning the writer for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). Otherwise, forge ahead with your best effort. It does not need to be perfect; the team is happy to review and improve upon your content. Please review the [Documentation guidelines](index.md) before you begin your first documentation MR.
-Having a knowledge base is any form that is separate from the documentation would be against the docs-first methodology because the content would overlap with the documentation.
+Having a knowledge base in any form that is separate from the documentation would be against the docs-first methodology because the content would overlap with the documentation.
## Markdown
@@ -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/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 557d3132d71..336ef4ab278 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -13,7 +13,7 @@ _Note:_ The action itself will not update the state, only a mutation should upda
## File structure
-When using Vuex at GitLab, separate this concerns into different files to improve readability:
+When using Vuex at GitLab, separate these concerns into different files to improve readability:
```
└── store
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/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/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/aws/index.md b/doc/install/aws/index.md
index 358ba971049..ddf2b2fb738 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -613,6 +613,9 @@ To back up GitLab:
sudo gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Restoring GitLab from a backup
To restore GitLab, first review the [restore documentation](../../raketasks/backup_restore.md#restore),
@@ -631,6 +634,9 @@ released, you can update your GitLab instance:
sudo gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
1. Update the repositories and install GitLab:
```sh
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/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
index 50e491f29a2..4a96001f2de 100644
--- a/doc/migrate_ci_to_ce/README.md
+++ b/doc/migrate_ci_to_ce/README.md
@@ -77,6 +77,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r
If this fails you need to fix it before upgrading to 8.0. Also see
<https://about.gitlab.com/get-help/>
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### 2. Check source and target database types
Check what databases you use on your GitLab server and your CI server.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 06048ad618f..c230bb413f2 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -80,6 +80,9 @@ Use this command if you've installed GitLab with the Omnibus package:
sudo gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
Use this if you've installed GitLab from source:
```sh
@@ -92,6 +95,9 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-backup create
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
If you are using the [GitLab helm chart](https://gitlab.com/gitlab-org/charts/gitlab) on a
Kubernetes cluster, you can run the backup task using `backup-utility` script on
the GitLab task runner pod via `kubectl`. Refer to [backing up a GitLab installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/backup.md#backing-up-a-gitlab-installation) for more details:
@@ -202,6 +208,9 @@ To use the `copy` strategy instead of the default streaming strategy, specify
sudo gitlab-backup create STRATEGY=copy
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Backup filename
By default a backup file is created according to the specification in [the Backup timestamp](#backup-timestamp) section above. You can however override the `[TIMESTAMP]` part of the filename by setting the `BACKUP` environment variable. For example:
@@ -210,6 +219,9 @@ By default a backup file is created according to the specification in [the Backu
sudo gitlab-backup create BACKUP=dump
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
The resulting file will then be `dump_gitlab_backup.tar`. This is useful for systems that make use of rsync and incremental backups, and will result in considerably faster transfer speeds.
### Rsyncable
@@ -222,6 +234,9 @@ Note that the `--rsyncable` option in `gzip` is not guaranteed to be available o
sudo gitlab-backup create BACKUP=dump GZIP_RSYNCABLE=yes
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Excluding specific directories from the backup
You can choose what should be exempt from the backup up by adding the environment variable `SKIP`.
@@ -247,6 +262,9 @@ For Omnibus GitLab packages:
sudo gitlab-backup create SKIP=db,uploads
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
For installations from source:
```sh
@@ -452,6 +470,9 @@ sudo gitlab-backup create DIRECTORY=daily
sudo gitlab-backup create DIRECTORY=weekly
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
@@ -569,6 +590,9 @@ There, add the following line to schedule the backup for everyday at 2 AM:
0 2 * * * /opt/gitlab/bin/gitlab-backup create CRON=1
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
+
You may also want to set a limited lifetime for backups to prevent regular
backups using all your disk space.
@@ -729,6 +753,14 @@ restore:
sudo gitlab-backup restore BACKUP=1493107454_2018_04_25_10.6.4-ce
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:restore`.
+
+CAUTION: **Warning:**
+`gitlab-rake gitlab:backup:restore` does not set the right file system permissions on your Registry directory.
+This is a [known issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/62759). On GitLab 12.2 or newer, you can
+use `gitlab-backup restore` to avoid this issue.
+
Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above.
Reconfigure, restart and check GitLab:
@@ -763,6 +795,14 @@ For docker installations, the restore task can be run from host:
docker exec -it <name of container> gitlab-backup restore
```
+NOTE: **Note**
+For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:restore`.
+
+CAUTION: **Warning:**
+`gitlab-rake gitlab:backup:restore` does not set the right file system permissions on your Registry directory.
+This is a [known issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/62759). On GitLab 12.2 or newer, you can
+use `gitlab-backup restore` to avoid this issue.
+
The GitLab helm chart uses a different process, documented in
[restoring a GitLab helm chart installation](https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/backup-restore/restore.md).
@@ -978,7 +1018,7 @@ sudo chown -R registry:registry /var/opt/gitlab/gitlab-rails/shared/registry/doc
NOTE: **Note:**
If you have changed the default filesystem location for the registry, you will
-want to run the chown against your custom location instead of
+want to run the `chown` against your custom location instead of
`/var/opt/gitlab/gitlab-rails/shared/registry/docker`.
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
@@ -990,6 +1030,7 @@ While running the backup, you may receive a gzip error:
```sh
sudo /opt/gitlab/bin/gitlab-backup create
+...
Dumping ...
...
gzip: stdout: Input/output error
@@ -999,5 +1040,5 @@ Backup failed
If this happens, check the following:
-1. Confirm there is sufficent diskspace for the gzip operation.
-1. If NFS is being used, check if the mount option `timeo` is set. The default is `600`, and changing this to smaller values have resulted in this error.
+1. Confirm there is sufficient disk space for the gzip operation.
+1. If NFS is being used, check if the mount option `timeout` is set. The default is `600`, and changing this to smaller values have resulted in this error.
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/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/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index f6a1b6abdbf..b202dd7e9d2 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -13,7 +13,7 @@ NOTE: **Note:**
Support for MySQL was removed in GitLab 12.1. This procedure should be performed
**before** installing GitLab 12.1.
-[pgloader](https://pgloader.io/) 3.4.1+ is required.
+[pgloader](https://pgloader.io/) 3.4.1+ is required, confirm with `pgloader -V`.
You can install it directly from your distribution, for example in
Debian/Ubuntu:
@@ -125,6 +125,10 @@ new PostgreSQL one:
create no indexes, preserve index names, no foreign keys,
data only
+ SET MySQL PARAMETERS
+ net_read_timeout = '90',
+ net_write_timeout = '180'
+
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
;
@@ -222,6 +226,10 @@ new PostgreSQL one:
create no indexes, preserve index names, no foreign keys,
data only
+ SET MySQL PARAMETERS
+ net_read_timeout = '90',
+ net_write_timeout = '180'
+
ALTER SCHEMA 'gitlabhq_production' RENAME TO 'public'
;
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 89526c08e7e..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
@@ -146,7 +143,7 @@ Dependency Scanning can be [configured](#customizing-the-dependency-scanning-set
using environment variables.
| Environment variable | Description | Example usage |
-|-------------------------------- |-------------| |
+| --------------------------------------- | ----------- | ------------- |
| `DS_ANALYZER_IMAGES` | Comma separated list of custom images. The official default images are still enabled. Read more about [customizing analyzers](analyzers.md). | |
| `DS_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). | |
| `DS_ANALYZER_IMAGE_TAG` | Override the Docker tag of the official default images. Read more about [customizing analyzers](analyzers.md). | |
@@ -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/clusters/applications.md b/doc/user/clusters/applications.md
index 65b654d1553..e43b1ca6826 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -19,13 +19,11 @@ This namespace:
- Is created once.
- Has a non-configurable name.
-To see a list of available applications to install:
+To see a list of available applications to install. For a:
-1. For a:
- - [Project-level cluster](../project/clusters/index.md),
- navigate to your project's **Operations > Kubernetes**.
- - [Group-level cluster](../group/clusters/index.md),
- navigate to your group's **Kubernetes** page.
+- [Project-level cluster](../project/clusters/index.md), navigate to your project's
+ **Operations > Kubernetes**.
+- [Group-level cluster](../group/clusters/index.md), navigate to your group's **Kubernetes** page.
Install Helm first as it's used to install other applications.
@@ -61,8 +59,8 @@ can lead to confusion during deployments.
### Helm
-> - Available for project-level clusters since GitLab 10.2.
-> - Available for group-level clusters since GitLab 11.6.
+> - Introduced in GitLab 10.2 for project-level clusters.
+> - Introduced in GitLab 11.6 for group-level clusters.
[Helm](https://docs.helm.sh/) is a package manager for Kubernetes and is
required to install all the other applications. It is installed in its
@@ -71,8 +69,7 @@ environment.
### Cert-Manager
-> - Available for project-level clusters since GitLab 11.6.
-> - Available for group-level clusters since GitLab 11.6.
+> Introduced in GitLab 11.6 for project- and group-level clusters.
[Cert-Manager](https://docs.cert-manager.io/en/latest/) is a native
Kubernetes certificate management controller that helps with issuing
@@ -91,8 +88,8 @@ chart was used.
### GitLab Runner
-> - Available for project-level clusters since GitLab 10.6.
-> - Available for group-level clusters since GitLab 11.10.
+> - Introduced in GitLab 10.6 for project-level clusters.
+> - Introduced in GitLab 11.10 for group-level clusters.
[GitLab Runner](https://docs.gitlab.com/runner/) is the open source
project that is used to run your jobs and send the results back to
@@ -112,8 +109,8 @@ file.
### Ingress
-> - Available for project-level clusters since GitLab 10.2.
-> - Available for group-level clusters since GitLab 11.6.
+> - Introduced in GitLab 10.2 for project-level clusters.
+> - Introduced in GitLab 11.6 for group-level clusters.
[Ingress](https://kubernetes.github.io/ingress-nginx/) can provide load
balancing, SSL termination, and name-based virtual hosting. It acts as a
@@ -129,8 +126,8 @@ file.
### JupyterHub
-> - Available for project-level clusters since GitLab 11.0.
-> - Available for group-level clusters since GitLab 12.3.
+> - Introduced in GitLab 11.0 for project-level clusters.
+> - Introduced in GitLab 12.3 for group-level clusters.
[JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a
multi-user service for managing notebooks across a team. [Jupyter
@@ -163,7 +160,7 @@ file.
#### Jupyter Git Integration
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12 for project-level clusters.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28783) in GitLab 12.0 for project-level clusters.
When installing JupyterHub onto your Kubernetes cluster, [JupyterLab's Git extension](https://github.com/jupyterlab/jupyterlab-git)
is automatically provisioned and configured using the authenticated user's:
@@ -190,8 +187,8 @@ You can clone repositories from the files tab in Jupyter:
### Knative
-> - Available for project-level clusters since GitLab 11.5.
-> - Available for group-level and instance-level clusters since GitLab 12.3.
+> - Introduced in GitLab 11.5 for project-level clusters.
+> - Introduced in GitLab 12.3 for group- and instance-level clusters.
[Knative](https://cloud.google.com/knative) provides a platform to
create, deploy, and manage serverless workloads from a Kubernetes
@@ -214,8 +211,8 @@ chart is used to install this application.
### Prometheus
-> - Available for project-level clusters since GitLab 10.4.
-> - Available for group-level clusters since GitLab 11.11.
+> - Introduced in GitLab 10.4 for project-level clusters.
+> - Introduced in GitLab 11.11 for group-level clusters.
[Prometheus](https://prometheus.io/docs/introduction/overview/) is an
open-source monitoring and alerting system useful to supervise your
@@ -255,8 +252,7 @@ chart plus the values set by
## Uninstalling applications
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in
-> GitLab 11.11.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in GitLab 11.11.
The applications below can be uninstalled.
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/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/canary_deployments.md b/doc/user/project/canary_deployments.md
index 5068d2757be..d4c8bf0d309 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -2,7 +2,7 @@
> [Introduced][ee-1659] in [GitLab Premium][eep] 9.1.
-A popular [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration)
+A popular [Continuous Deployment](https://en.wikipedia.org/wiki/Continuous_deployment)
strategy, where a small portion of the fleet is updated to the new version of
your application.
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index cdb7f837158..dad53a600dc 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -6,14 +6,15 @@ your self-hosted GitLab instance.
## Overview
NOTE: **Note:**
-While these instructions will always work for users on GitLab.com, if you are an
-administrator of a self-hosted GitLab instance, you will need to enable the
-[GitHub integration][gh-import] in order for users to follow the preferred
-import method described on this page. If this is not enabled, users can alternatively import their
-GitHub repositories using a [personal access token](#using-a-github-token) from GitHub,
-but this method will not be able to associate all user activity (such as issues and pull requests)
-with matching GitLab users. As an administrator of a self-hosted GitLab instance, you can also use
-the [GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from
+These instructions work for users on GitLab.com, but if you are an
+administrator of a self-hosted GitLab instance or if you are importing from GitHub Enterprise,
+you must enable [GitHub integration][gh-import]. GitHub integration is the only method for
+importing from GitHub Enterprise. If you are using GitLab.com, you can alternatively import
+GitHub repositories using a [personal access token](#using-a-github-token),
+but this method is not recommended because it cannot associate all user activity
+(such as issues and pull requests) with matching GitLab users.
+If you are an administrator of a self-hosted GitLab instance, you can also use the
+[GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from
GitHub without the constraints of a Sidekiq worker.
The following aspects of a project are imported:
@@ -76,7 +77,7 @@ User-matching attempts occur in that order, and if a user is not identified eith
the user account that is performing the import.
NOTE: **Note:**
-If you are using a self-hosted GitLab instance, this process requires that you have configured the
+If you are using a self-hosted GitLab instance or if you are importing from GitHub Enterprise, this process requires that you have configured
[GitHub integration][gh-import].
1. From the top navigation bar, click **+** and select **New project**.
@@ -88,9 +89,13 @@ If you are using a self-hosted GitLab instance, this process requires that you h
### Using a GitHub token
NOTE: **Note:**
-For a proper author/assignee mapping for issues and pull requests, the [GitHub integration method (above)](#using-the-github-integration)
-should be used instead of the personal access token. If you are using GitLab.com or a self-hosted GitLab instance with the GitHub
-integration enabled, that should be the preferred method to import your repositories. Read more in the [How it works](#how-it-works) section.
+Using a personal access token to import projects is not recommended. If you are a GitLab.com user,
+you can use a personal access token to import your project from GitHub, but this method cannot
+associate all user activity (such as issues and pull requests) with matching GitLab users.
+If you are an administrator of a self-hosted GitLab instance or if you are importing from
+GitHub Enterprise, you cannot use a personal access token.
+The [GitHub integration method (above)](#using-the-github-integration) is recommended for all users.
+Read more in the [How it works](#how-it-works) section.
If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories:
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index f5bc6cbd988..211bbdc2bb9 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1327,3 +1327,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/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 ca9ddd91e0d..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.
@@ -196,7 +200,7 @@ If approvals are [set at the project level](#editing-approvals), the
default configuration (number of required approvals and approvers) can be
overridden for each merge request in that project.
-One possible scenario would be to to assign a group of approvers at the project
+One possible scenario would be to assign a group of approvers at the project
level and change them later when creating or editing the merge request.
First, you have to enable this option in the project's settings:
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/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/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/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 5bf1f484106..a00d7d6f775 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -102,7 +102,7 @@ The repository will push soon. To force a push, click the appropriate button.
> - [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab-ee/issues/10361) to
-GitLab.com users until September 22nd, 2019.
+GitLab.com users until March 22nd, 2020.
You can set up a repository to automatically have its branches, tags, and commits updated from an
upstream repository.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 88d411e22a9..3bf16dc41d2 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -119,6 +119,7 @@ module API
mount ::API::GroupVariables
mount ::API::ImportGithub
mount ::API::Internal::Base
+ mount ::API::Internal::Pages
mount ::API::Issues
mount ::API::JobArtifacts
mount ::API::Jobs
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index c9b3483acaf..312c8d5b548 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1312,6 +1312,10 @@ module API
options[:project].releases.find_by(tag: repo_tag.name)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ expose :protected do |repo_tag, options|
+ ::ProtectedTag.protected?(options[:project], repo_tag.name)
+ end
end
class Runner < Grape::Entity
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
new file mode 100644
index 00000000000..6ea048bde03
--- /dev/null
+++ b/lib/api/internal/pages.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module API
+ # Pages Internal API
+ module Internal
+ class Pages < Grape::API
+ before do
+ not_found! unless Feature.enabled?(:pages_internal_api)
+ authenticate_gitlab_pages_request!
+ end
+
+ helpers do
+ def authenticate_gitlab_pages_request!
+ unauthorized! unless Gitlab::Pages.verify_api_request(headers)
+ end
+ end
+
+ namespace 'internal' do
+ namespace 'pages' do
+ get "/" do
+ status :ok
+ end
+ end
+ end
+ 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/settings.rb b/lib/api/settings.rb
index dd27ebab83d..acf03051a5b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -50,10 +50,8 @@ module API
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
- given domain_blacklist_enabled: ->(val) { val } do
- requires :domain_blacklist, type: String, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
- end
- optional :domain_whitelist, type: String, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
@@ -74,7 +72,7 @@ module API
requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
end
optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
- optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project manifest],
+ optional :import_sources, type: Array[String], values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
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/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index 89623a809c9..43399c74457 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -6,7 +6,14 @@ module Gitlab
class Rules
include ::Gitlab::Utils::StrongMemoize
- Result = Struct.new(:when, :start_in)
+ Result = Struct.new(:when, :start_in) do
+ def build_attributes
+ {
+ when: self.when,
+ options: { start_in: start_in }.compact
+ }.compact
+ end
+ end
def initialize(rule_hashes, default_when = 'on_success')
@rule_list = Rule.fabricate_list(rule_hashes)
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
index ff0baf3348c..bf787fe95a6 100644
--- a/lib/gitlab/ci/build/rules/rule/clause.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -13,9 +13,7 @@ module Gitlab
UnknownClauseError = Class.new(StandardError)
def self.fabricate(type, value)
- type = type.to_s.camelize
-
- self.const_get(type).new(value) if self.const_defined?(type)
+ "#{self}::#{type.to_s.camelize}".safe_constantize&.new(value)
end
def initialize(spec)
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 3009c7e8329..f750886a8c5 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -122,7 +122,7 @@ module Gitlab
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :environment, :coverage, :retry,
+ :artifacts, :environment, :coverage, :retry, :rules,
:parallel, :needs, :interruptible
attributes :script, :tags, :allow_failure, :when, :dependencies,
@@ -145,6 +145,13 @@ module Gitlab
end
@entries.delete(:type)
+
+ # This is something of a hack, see issue for details:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/67150
+ if !only_defined? && has_rules?
+ @entries.delete(:only)
+ @entries.delete(:except)
+ end
end
inherit!(deps)
@@ -203,6 +210,7 @@ module Gitlab
cache: cache_value,
only: only_value,
except: except_value,
+ rules: has_rules? ? rules_value : nil,
variables: variables_defined? ? variables_value : {},
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
index 65cad0880f5..2fbc3d9e367 100644
--- a/lib/gitlab/ci/config/entry/rules.rb
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -26,6 +26,10 @@ module Gitlab
end
end
end
+
+ def value
+ @config
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 1066331062b..1f6b3853069 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -145,7 +145,7 @@ module Gitlab
def rules_attributes
strong_memoize(:rules_attributes) do
- @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {}
+ @using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
end
end
end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 501d91fa9ad..986605efdc3 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -42,6 +42,7 @@ module Gitlab
yaml_variables: yaml_variables(name),
needs_attributes: job[:needs]&.map { |need| { name: need } },
interruptible: job[:interruptible],
+ rules: job[:rules],
options: {
image: job[:image],
services: job[:services],
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/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 13883ca7f3d..28d48ce6dfe 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -8,6 +8,7 @@ module Gitlab
@included_attributes = config[:included_attributes] || {}
@excluded_attributes = config[:excluded_attributes] || {}
@methods = config[:methods] || {}
+ @preloads = config[:preloads] || {}
end
def find_root(model_key)
@@ -29,10 +30,26 @@ module Gitlab
only: @included_attributes[model_key],
except: @excluded_attributes[model_key],
methods: @methods[model_key],
- include: resolve_model_tree(model_tree)
+ include: resolve_model_tree(model_tree),
+ preload: resolve_preloads(model_key, model_tree)
}.compact
end
+ def resolve_preloads(model_key, model_tree)
+ model_tree
+ .map { |submodel_key, submodel_tree| resolve_preload(model_key, submodel_key, submodel_tree) }
+ .compact
+ .to_h
+ .deep_merge(@preloads[model_key].to_h)
+ .presence
+ end
+
+ def resolve_preload(parent_model_key, model_key, model_tree)
+ return if @methods[parent_model_key]&.include?(model_key)
+
+ [model_key, resolve_preloads(model_key, model_tree)]
+ end
+
def resolve_model_tree(model_tree)
return unless model_tree
diff --git a/lib/gitlab/import_export/fast_hash_serializer.rb b/lib/gitlab/import_export/fast_hash_serializer.rb
new file mode 100644
index 00000000000..a6ab4f3a3d9
--- /dev/null
+++ b/lib/gitlab/import_export/fast_hash_serializer.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+# ActiveModel::Serialization (https://github.com/rails/rails/blob/v5.0.7/activemodel/lib/active_model/serialization.rb#L184)
+# is simple in that it recursively calls `as_json` on each object to
+# serialize everything. However, for a model like a Project, this can
+# generate a query for every single association, which can add up to tens
+# of thousands of queries and lead to memory bloat.
+#
+# To improve this, we can do several things:
+
+# 1. Use the option tree in http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
+# to generate the necessary preload clauses.
+#
+# 2. We observe that a single project has many issues, merge requests,
+# etc. Instead of serializing everything at once, which could lead to
+# database timeouts and high memory usage, we take each top-level
+# association and serialize the data in batches.
+#
+# For example, we serialize the first 100 issues and preload all of
+# their associated events, notes, etc. before moving onto the next
+# batch. When we're done, we serialize merge requests in the same way.
+# We repeat this pattern for the remaining associations specified in
+# import_export.yml.
+module Gitlab
+ module ImportExport
+ class FastHashSerializer
+ attr_reader :subject, :tree
+
+ BATCH_SIZE = 100
+
+ def initialize(subject, tree, batch_size: BATCH_SIZE)
+ @subject = subject
+ @batch_size = batch_size
+ @tree = tree
+ end
+
+ # Serializes the subject into a Hash for the given option tree
+ # (e.g. Project#as_json)
+ def execute
+ simple_serialize.merge(serialize_includes)
+ end
+
+ private
+
+ def simple_serialize
+ subject.as_json(
+ tree.merge(include: nil, preloads: nil))
+ end
+
+ def serialize_includes
+ return {} unless includes
+
+ includes
+ .map(&method(:serialize_include_definition))
+ .compact
+ .to_h
+ end
+
+ # definition:
+ # { labels: { includes: ... } }
+ def serialize_include_definition(definition)
+ raise ArgumentError, 'definition needs to be Hash' unless definition.is_a?(Hash)
+ raise ArgumentError, 'definition needs to have exactly one Hash element' unless definition.one?
+
+ key = definition.first.first
+ options = definition.first.second
+
+ record = subject.public_send(key) # rubocop: disable GitlabSecurity/PublicSend
+ return unless record
+
+ serialized_record = serialize_record(key, record, options)
+ return unless serialized_record
+
+ # `#as_json` always returns keys as `strings`
+ [key.to_s, serialized_record]
+ end
+
+ def serialize_record(key, record, options)
+ unless record.respond_to?(:as_json)
+ raise "Invalid type of #{key} is #{record.class}"
+ end
+
+ # no has-many relation
+ unless record.is_a?(ActiveRecord::Relation)
+ return record.as_json(options)
+ end
+
+ # has-many relation
+ data = []
+
+ record.in_batches(of: @batch_size) do |batch| # rubocop:disable Cop/InBatches
+ batch = batch.preload(preloads[key]) if preloads&.key?(key)
+ data += batch.as_json(options)
+ end
+
+ data
+ end
+
+ def includes
+ tree[:include]
+ end
+
+ def preloads
+ tree[:preload]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 06c94beead8..511b702553e 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -231,6 +231,16 @@ methods:
ci_pipelines:
- :notes
+preloads:
+ statuses:
+ # TODO: We cannot preload tags, as they are not part of `GenericCommitStatus`
+ # tags: # needed by tag_list
+ project: # deprecated: needed by coverage_regex of Ci::Build
+ merge_requests:
+ source_project: # needed by source_branch_sha and diff_head_sha
+ target_project: # needed by target_branch_sha
+ assignees: # needed by assigne_id that is implemented by DeprecatedAssignee
+
# EE specific relationships and settings to include. All of this will be merged
# into the previous structures if EE is used.
ee:
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index f1b3db6b208..f75f69b2c75 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -41,7 +41,13 @@ module Gitlab
end
def serialize_project_tree
- @project.as_json(reader.project_tree)
+ if Feature.enabled?(:export_fast_serialize, default_enabled: true)
+ Gitlab::ImportExport::FastHashSerializer
+ .new(@project, reader.project_tree)
+ .execute
+ else
+ @project.as_json(reader.project_tree)
+ end
end
def reader
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/pages.rb b/lib/gitlab/pages.rb
index 16df0700b08..4899b1d3234 100644
--- a/lib/gitlab/pages.rb
+++ b/lib/gitlab/pages.rb
@@ -1,7 +1,22 @@
# frozen_string_literal: true
module Gitlab
- module Pages
+ class Pages
VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Pages-Api-Request'.freeze
+
+ include JwtAuthenticatable
+
+ class << self
+ def verify_api_request(request_headers)
+ decode_jwt_for_issuer('gitlab-pages', request_headers[INTERNAL_API_REQUEST_HEADER])
+ rescue JWT::DecodeError
+ false
+ end
+
+ def secret_path
+ Gitlab.config.pages.secret_file
+ 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/search_results.rb b/lib/gitlab/search_results.rb
index ce4c1611687..93e172299b9 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,7 +2,8 @@
module Gitlab
class SearchResults
- COUNT_LIMIT = 1001
+ COUNT_LIMIT = 101
+ COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+"
attr_reader :current_user, :query, :per_page
@@ -60,7 +61,7 @@ module Gitlab
def formatted_limited_count(count)
if count >= COUNT_LIMIT
- "#{COUNT_LIMIT - 1}+"
+ COUNT_LIMIT_MESSAGE
else
count.to_s
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/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/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 a77d70a5700..f2d3a39d593 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1146,6 +1146,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 ""
@@ -2530,6 +2533,9 @@ msgstr ""
msgid "ClusterIntegration|Alternatively"
msgstr ""
+msgid "ClusterIntegration|Amazon EKS"
+msgstr ""
+
msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later."
msgstr ""
@@ -2608,6 +2614,9 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Create cluster on"
+msgstr ""
+
msgid "ClusterIntegration|Did you know?"
msgstr ""
@@ -2659,6 +2668,9 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
+msgid "ClusterIntegration|Google GKE"
+msgstr ""
+
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
@@ -2989,9 +3001,6 @@ msgstr ""
msgid "ClusterIntegration|pricing"
msgstr ""
-msgid "ClusterIntegration|properly configured"
-msgstr ""
-
msgid "ClusterIntegration|sign up"
msgstr ""
@@ -5518,7 +5527,7 @@ msgstr ""
msgid "Google Takeout"
msgstr ""
-msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
+msgid "Google authentication is not %{link_start}property configured%{link_end}. Ask your GitLab administrator if you want to use this service."
msgstr ""
msgid "Got it"
@@ -10565,6 +10574,9 @@ msgstr ""
msgid "Sign in via 2FA code"
msgstr ""
+msgid "Sign in with Google"
+msgstr ""
+
msgid "Sign out"
msgstr ""
@@ -12299,6 +12311,9 @@ msgstr ""
msgid "Toggle backtrace"
msgstr ""
+msgid "Toggle collapse"
+msgstr ""
+
msgid "Toggle comments for this file"
msgstr ""
@@ -14180,7 +14195,7 @@ msgstr ""
msgid "nounSeries|%{item}, and %{lastItem}"
msgstr ""
-msgid "or"
+msgid "or %{link_start}create a new Google account%{link_end}"
msgstr ""
msgid "out of %d total test"
diff --git a/qa/qa.rb b/qa/qa.rb
index 8b38011486b..9bf28d396ba 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 52929ece9ed..e5e26b1864b 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -35,12 +35,17 @@ module QA
element :labels_block
element :edit_link_labels
element :dropdown_menu_labels
+ element :milestone_link
end
view 'app/views/shared/issuable/_close_reopen_button.html.haml' do
element :reopen_issue_button
end
+ def click_milestone_link
+ click_element(:milestone_link)
+ end
+
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment: nil, filter: :all_activities)
diff --git a/qa/qa/page/project/milestone/index.rb b/qa/qa/page/project/milestone/index.rb
index 6895c44f72f..8ad7689ce70 100644
--- a/qa/qa/page/project/milestone/index.rb
+++ b/qa/qa/page/project/milestone/index.rb
@@ -17,3 +17,5 @@ module QA
end
end
end
+
+QA::Page::Project::Milestone::Index.prepend_if_ee('QA::EE::Page::Project::Milestone::Index')
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/branch.rb b/qa/qa/resource/branch.rb
index a45dd030625..6dc47e36977 100644
--- a/qa/qa/resource/branch.rb
+++ b/qa/qa/resource/branch.rb
@@ -72,6 +72,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/issue.rb b/qa/qa/resource/issue.rb
index 16ab59352f3..8539eaeb337 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -3,7 +3,7 @@
module QA
module Resource
class Issue < Base
- attr_writer :description, :milestone
+ attr_writer :description, :milestone, :weight
attribute :project do
Project.fabricate! do |resource|
@@ -46,6 +46,7 @@ module QA
title: title
}.tap do |hash|
hash[:milestone_id] = @milestone.id if @milestone
+ hash[:weight] = @weight if @weight
end
end
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/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/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index e5501535875..afc059d7561 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -73,8 +73,8 @@ describe Admin::ClustersController do
end
describe 'GET #new' do
- def get_new
- get :new
+ def get_new(provider: 'gke')
+ get :new, params: { provider: provider }
end
describe 'functionality for new cluster' do
@@ -85,6 +85,7 @@ describe Admin::ClustersController do
end
before do
+ stub_feature_flags(create_eks_clusters: false)
allow(SecureRandom).to receive(:hex).and_return(key)
end
@@ -94,6 +95,20 @@ describe Admin::ClustersController do
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path)
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when selected provider is gke and no valid gcp token exists' do
+ it 'redirects to gcp authorize_url' do
+ get_new
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
end
context 'when omniauth has not configured' do
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/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 09677b42887..5a3ba51d4df 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -85,8 +85,8 @@ describe Groups::ClustersController do
end
describe 'GET new' do
- def go
- get :new, params: { group_id: group }
+ def go(provider: 'gke')
+ get :new, params: { group_id: group, provider: provider }
end
describe 'functionality for new cluster' do
@@ -97,6 +97,7 @@ describe Groups::ClustersController do
end
before do
+ stub_feature_flags(create_eks_clusters: false)
allow(SecureRandom).to receive(:hex).and_return(key)
end
@@ -106,6 +107,20 @@ describe Groups::ClustersController do
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_group_cluster_path(group))
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when selected provider is gke and no valid gcp token exists' do
+ it 'redirects to gcp authorize_url' do
+ go
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
end
context 'when omniauth has not configured' do
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 35cbab57037..8ac72df5d20 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -79,8 +79,12 @@ describe Projects::ClustersController do
end
describe 'GET new' do
- def go
- get :new, params: { namespace_id: project.namespace, project_id: project }
+ def go(provider: 'gke')
+ get :new, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ provider: provider
+ }
end
describe 'functionality for new cluster' do
@@ -91,6 +95,7 @@ describe Projects::ClustersController do
end
before do
+ stub_feature_flags(create_eks_clusters: false)
allow(SecureRandom).to receive(:hex).and_return(key)
end
@@ -100,6 +105,20 @@ describe Projects::ClustersController do
expect(assigns(:authorize_url)).to include(key)
expect(session[session_key_for_redirect_uri]).to eq(new_project_cluster_path(project))
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when selected provider is gke and no valid gcp token exists' do
+ it 'redirects to gcp authorize_url' do
+ go
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+ end
end
context 'when omniauth has not configured' do
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/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 180d997a8e8..0c074714bf3 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -19,9 +19,9 @@ describe Projects::ServicesController do
it 'renders 404' do
allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param }
+ put :test, params: project_params
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
@@ -29,11 +29,11 @@ describe Projects::ServicesController do
let(:service_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(json_response['message']).to eq "Validations failed."
+ expect(json_response['message']).to eq 'Validations failed.'
expect(json_response['service_response']).to include "Url can't be blank"
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
end
end
@@ -47,9 +47,9 @@ describe Projects::ServicesController do
it 'returns success' do
allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param }
+ put :test, params: project_params
- expect(response.status).to eq(200)
+ expect(response).to be_successful
end
end
@@ -57,11 +57,11 @@ describe Projects::ServicesController do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 200, body: '{}')
- expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original
+ expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(response.status).to eq(200)
+ expect(response).to be_successful
end
end
@@ -69,14 +69,23 @@ describe Projects::ServicesController do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 200, body: '{}')
- expect(Gitlab::HTTP).to receive(:get).with("/rest/api/2/serverInfo", any_args).and_call_original
+ expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(response.status).to eq(200)
+ expect(response).to be_successful
end
context 'when service is configured for the first time' do
+ let(:service_params) do
+ {
+ 'active' => '1',
+ 'push_events' => '1',
+ 'token' => 'token',
+ 'project_url' => 'http://test.com'
+ }
+ end
+
before do
allow_any_instance_of(ServiceHook).to receive(:execute).and_return(true)
end
@@ -84,7 +93,7 @@ describe Projects::ServicesController do
it 'persist the object' do
do_put
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
expect(json_response).to be_empty
expect(BuildkiteService.first).to be_present
end
@@ -92,18 +101,14 @@ describe Projects::ServicesController do
it 'creates the ServiceHook object' do
do_put
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
expect(json_response).to be_empty
expect(BuildkiteService.first.service_hook).to be_present
end
def do_put
- put :test, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: 'buildkite',
- service: { 'active' => '1', 'push_events' => '1', token: 'token', 'project_url' => 'http://test.com' }
- }
+ put :test, params: project_params(id: 'buildkite',
+ service: service_params)
end
end
end
@@ -113,9 +118,9 @@ describe Projects::ServicesController do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404)
- put :test, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params }
+ put :test, params: project_params(service: service_params)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to be_successful
expect(json_response).to eq(
'error' => true,
'message' => 'Test failed.',
@@ -127,39 +132,70 @@ describe Projects::ServicesController do
end
describe 'PUT #update' do
- context 'when param `active` is set to true' do
- it 'activates the service and redirects to integrations paths' do
- put :update,
- params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true } }
+ describe 'as HTML' do
+ let(:service_params) { { active: true } }
- expect(response).to redirect_to(project_settings_integrations_path(project))
- expect(flash[:notice]).to eq 'Jira activated.'
+ before do
+ put :update, params: project_params(service: service_params)
+ end
+
+ context 'when param `active` is set to true' do
+ it 'activates the service and redirects to integrations paths' do
+ expect(response).to redirect_to(project_settings_integrations_path(project))
+ expect(flash[:notice]).to eq 'Jira activated.'
+ end
+ end
+
+ context 'when param `active` is set to false' do
+ let(:service_params) { { active: false } }
+
+ it 'does not activate the service but saves the settings' do
+ expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
+ end
end
- end
- context 'when param `active` is set to false' do
- it 'does not activate the service but saves the settings' do
- put :update,
- params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: false } }
+ context 'when activating Jira service from a template' do
+ let(:service) do
+ create(:jira_service, project: project, template: true)
+ end
- expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
+ it 'activate Jira service from template' do
+ expect(flash[:notice]).to eq 'Jira activated.'
+ end
end
end
- context 'when activating Jira service from a template' do
- let(:template_service) { create(:jira_service, project: project, template: true) }
+ describe 'as JSON' do
+ before do
+ put :update, params: project_params(service: service_params, format: :json)
+ end
+
+ context 'when update succeeds' do
+ let(:service_params) { { url: 'http://example.com' } }
+
+ it 'returns JSON response with no errors' do
+ expect(response).to be_successful
+ expect(json_response).to include('active' => true, 'errors' => {})
+ end
+ end
- it 'activate Jira service from template' do
- put :update, params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true } }
+ context 'when update fails' do
+ let(:service_params) { { url: '' } }
- expect(flash[:notice]).to eq 'Jira activated.'
+ it 'returns JSON response with errors' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response).to include(
+ 'active' => true,
+ 'errors' => { 'url' => ['must be a valid URL', %{can't be blank}] }
+ )
+ end
end
end
end
- describe "GET #edit" do
+ describe 'GET #edit' do
before do
- get :edit, params: { namespace_id: project.namespace, project_id: project, id: 'jira' }
+ get :edit, params: project_params(id: 'jira')
end
context 'with approved services' do
@@ -168,4 +204,14 @@ describe Projects::ServicesController do
end
end
end
+
+ private
+
+ def project_params(opts = {})
+ opts.reverse_merge(
+ namespace_id: project.namespace,
+ project_id: project,
+ id: service.to_param
+ )
+ 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_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/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 820ce48e52c..a11237db508 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -18,6 +18,8 @@ describe 'Gcp Cluster', :js do
let(:project_id) { 'test-project-1234' }
before do
+ stub_feature_flags(create_eks_clusters: false)
+
allow_any_instance_of(Projects::ClustersController)
.to receive(:token_in_session).and_return('token')
allow_any_instance_of(Projects::ClustersController)
@@ -147,6 +149,7 @@ describe 'Gcp Cluster', :js do
context 'when user has not signed with Google' do
before do
+ stub_feature_flags(create_eks_clusters: false)
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index ce382c19fc1..d1cd19dff2d 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -51,6 +51,7 @@ describe 'Clusters', :js do
context 'when user has not signed in Google' do
before do
+ stub_feature_flags(create_eks_clusters: false)
visit project_clusters_path(project)
click_link 'Add Kubernetes cluster'
@@ -62,4 +63,27 @@ describe 'Clusters', :js do
expect(page).to have_link('Google account')
end
end
+
+ context 'when create_eks_clusters feature flag is enabled' do
+ before do
+ stub_feature_flags(create_eks_clusters: true)
+ end
+
+ context 'when user access create cluster page' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add Kubernetes cluster'
+ click_link 'Create new Cluster on GKE'
+ end
+
+ it 'user sees a link to create a GKE cluster' do
+ expect(page).to have_link('Google GKE')
+ end
+
+ it 'user sees a link to create an EKS cluster' do
+ expect(page).to have_link('Amazon EKS')
+ end
+ end
+ end
end
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/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json
index 5713ea1f526..bb0190955f0 100644
--- a/spec/fixtures/api/schemas/public_api/v4/tag.json
+++ b/spec/fixtures/api/schemas/public_api/v4/tag.json
@@ -16,7 +16,8 @@
{ "type": "null" },
{ "$ref": "release/tag_release.json" }
]
- }
+ },
+ "protected": { "type": "boolean" }
},
"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/vue_shared/components/gl_toggle_vuex_spec.js b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
new file mode 100644
index 00000000000..f076c45e56c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js
@@ -0,0 +1,115 @@
+import Vuex from 'vuex';
+import GlToggleVuex from '~/vue_shared/components/gl_toggle_vuex.vue';
+import { GlToggle } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('GlToggleVuex component', () => {
+ let wrapper;
+ let store;
+
+ const findButton = () => wrapper.find('button');
+
+ const createWrapper = (props = {}) => {
+ wrapper = mount(GlToggleVuex, {
+ localVue,
+ store,
+ propsData: {
+ stateProperty: 'toggleState',
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ toggleState: false,
+ },
+ actions: {
+ setToggleState: ({ commit }, { key, value }) => commit('setToggleState', { key, value }),
+ },
+ mutations: {
+ setToggleState: (state, { key, value }) => {
+ state[key] = value;
+ },
+ },
+ });
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders gl-toggle', () => {
+ expect(wrapper.find(GlToggle).exists()).toBe(true);
+ });
+
+ it('properly computes default value for setAction', () => {
+ expect(wrapper.props('setAction')).toBe('setToggleState');
+ });
+
+ describe('without a store module', () => {
+ it('calls action with new value when value changes', () => {
+ jest.spyOn(store, 'dispatch');
+
+ findButton().trigger('click');
+ expect(store.dispatch).toHaveBeenCalledWith('setToggleState', {
+ key: 'toggleState',
+ value: true,
+ });
+ });
+
+ it('updates store property when value changes', () => {
+ findButton().trigger('click');
+ expect(store.state.toggleState).toBe(true);
+ });
+ });
+
+ describe('with a store module', () => {
+ beforeEach(() => {
+ store = new Vuex.Store({
+ modules: {
+ someModule: {
+ namespaced: true,
+ state: {
+ toggleState: false,
+ },
+ actions: {
+ setToggleState: ({ commit }, { key, value }) =>
+ commit('setToggleState', { key, value }),
+ },
+ mutations: {
+ setToggleState: (state, { key, value }) => {
+ state[key] = value;
+ },
+ },
+ },
+ },
+ });
+
+ createWrapper({
+ storeModule: 'someModule',
+ });
+ });
+
+ it('calls action with new value when value changes', () => {
+ jest.spyOn(store, 'dispatch');
+
+ findButton().trigger('click');
+ expect(store.dispatch).toHaveBeenCalledWith('someModule/setToggleState', {
+ key: 'toggleState',
+ value: true,
+ });
+ });
+
+ it('updates store property when value changes', () => {
+ findButton().trigger('click');
+ expect(store.state.someModule.toggleState).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/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/monitoring/components/graph_group_spec.js b/spec/javascripts/monitoring/components/graph_group_spec.js
new file mode 100644
index 00000000000..068c4b5302c
--- /dev/null
+++ b/spec/javascripts/monitoring/components/graph_group_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import GraphGroup from '~/monitoring/components/graph_group.vue';
+
+describe('Graph group component', () => {
+ let graphGroup;
+
+ afterEach(() => {
+ graphGroup.destroy();
+ });
+
+ describe('When groups can be collapsed', () => {
+ beforeEach(() => {
+ graphGroup = shallowMount(GraphGroup, {
+ propsData: {
+ name: 'panel',
+ collapseGroup: true,
+ },
+ });
+ });
+
+ it('should show the angle-down caret icon when collapseGroup is true', () => {
+ expect(graphGroup.vm.caretIcon).toBe('angle-down');
+ });
+
+ it('should show the angle-right caret icon when collapseGroup is false', () => {
+ graphGroup.vm.collapse();
+
+ expect(graphGroup.vm.caretIcon).toBe('angle-right');
+ });
+ });
+
+ describe('When groups can not be collapsed', () => {
+ beforeEach(() => {
+ graphGroup = shallowMount(GraphGroup, {
+ propsData: {
+ name: 'panel',
+ collapseGroup: true,
+ showPanels: false,
+ },
+ });
+ });
+
+ it('should not contain a prometheus-graph-group container when showPanels is false', () => {
+ expect(graphGroup.vm.$el.querySelector('.prometheus-graph-group')).toBe(null);
+ });
+ });
+});
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/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 89431b80be3..023d7530b4b 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
context 'is matched' do
let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
- it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
+ it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) }
end
context 'is not matched' do
@@ -541,7 +541,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to be_included }
it 'correctly populates when:' do
- expect(seed_build.attributes).to include(when: 'delayed', start_in: '1 day')
+ expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' })
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index cf496b79a62..9d9a9ecda33 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -16,7 +16,10 @@ module Gitlab
let(:config) do
YAML.dump(
before_script: ['pwd'],
- rspec: { script: 'rspec' }
+ rspec: {
+ script: 'rspec',
+ interruptible: true
+ }
)
end
@@ -29,6 +32,7 @@ module Gitlab
before_script: ["pwd"],
script: ["rspec"]
},
+ interruptible: true,
allow_failure: false,
when: "on_success",
yaml_variables: []
@@ -36,6 +40,36 @@ module Gitlab
end
end
+ context 'with job rules' do
+ let(:config) do
+ YAML.dump(
+ rspec: {
+ script: 'rspec',
+ rules: [
+ { if: '$CI_COMMIT_REF_NAME == "master"' },
+ { changes: %w[README.md] }
+ ]
+ }
+ )
+ end
+
+ it 'returns valid build attributes' do
+ expect(subject).to eq({
+ stage: 'test',
+ stage_idx: 1,
+ name: 'rspec',
+ options: { script: ['rspec'] },
+ rules: [
+ { if: '$CI_COMMIT_REF_NAME == "master"' },
+ { changes: %w[README.md] }
+ ],
+ allow_failure: false,
+ when: 'on_success',
+ yaml_variables: []
+ })
+ end
+ end
+
describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
@@ -1252,7 +1286,7 @@ module Gitlab
end
end
- describe 'rules' do
+ context 'with when/rules conflict' do
subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
let(:config) do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index dafa4243145..e496ab4cd35 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -376,6 +376,7 @@ project:
- index_status
- feature_usage
- approval_rules
+- approval_merge_request_rules
- approvers
- approver_users
- pages_domains
diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
index 208b60844e3..3cbc1375d6e 100644
--- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -20,20 +20,41 @@ describe Gitlab::ImportExport::AttributesFinder do
except: [:iid],
include: [
{ merge_request_diff: {
- include: []
+ include: [],
+ preload: { source_project: nil }
} },
{ merge_request_test: { include: [] } }
],
- only: [:id]
+ only: [:id],
+ preload: {
+ merge_request_diff: { source_project: nil },
+ merge_request_test: nil
+ }
} },
{ commit_statuses: {
- include: [{ commit: { include: [] } }]
+ include: [{ commit: { include: [] } }],
+ preload: { commit: nil }
} },
{ project_members: {
include: [{ user: { include: [],
- only: [:email] } }]
+ only: [:email] } }],
+ preload: { user: nil }
} }
- ]
+ ],
+ preload: {
+ commit_statuses: {
+ commit: nil
+ },
+ issues: nil,
+ labels: nil,
+ merge_requests: {
+ merge_request_diff: { source_project: nil },
+ merge_request_test: nil
+ },
+ project_members: {
+ user: nil
+ }
+ }
}
end
@@ -50,7 +71,8 @@ describe Gitlab::ImportExport::AttributesFinder do
setup_yaml(tree: { project: [:issues] })
is_expected.to match(
- include: [{ issues: { include: [] } }]
+ include: [{ issues: { include: [] } }],
+ preload: { issues: nil }
)
end
@@ -58,7 +80,8 @@ describe Gitlab::ImportExport::AttributesFinder do
setup_yaml(tree: { project: [:project_feature] })
is_expected.to match(
- include: [{ project_feature: { include: [] } }]
+ include: [{ project_feature: { include: [] } }],
+ preload: { project_feature: nil }
)
end
@@ -67,7 +90,8 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ issues: { include: [] } },
- { snippets: { include: [] } }]
+ { snippets: { include: [] } }],
+ preload: { issues: nil, snippets: nil }
)
end
@@ -75,7 +99,9 @@ describe Gitlab::ImportExport::AttributesFinder do
setup_yaml(tree: { project: [issues: [:notes]] })
is_expected.to match(
- include: [{ issues: { include: [{ notes: { include: [] } }] } }]
+ include: [{ issues: { include: [{ notes: { include: [] } }],
+ preload: { notes: nil } } }],
+ preload: { issues: { notes: nil } }
)
end
@@ -85,7 +111,9 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ merge_requests:
{ include: [{ notes: { include: [] } },
- { merge_request_diff: { include: [] } }] } }]
+ { merge_request_diff: { include: [] } }],
+ preload: { merge_request_diff: nil, notes: nil } } }],
+ preload: { merge_requests: { merge_request_diff: nil, notes: nil } }
)
end
@@ -94,8 +122,11 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ merge_requests: {
- include: [{ notes: { include: [{ author: { include: [] } }] } }]
- } }]
+ include: [{ notes: { include: [{ author: { include: [] } }],
+ preload: { author: nil } } }],
+ preload: { notes: { author: nil } }
+ } }],
+ preload: { merge_requests: { notes: { author: nil } } }
)
end
@@ -105,7 +136,8 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ issues: { include: [],
- only: [:name, :description] } }]
+ only: [:name, :description] } }],
+ preload: { issues: nil }
)
end
@@ -115,7 +147,8 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ issues: { except: [:name],
- include: [] } }]
+ include: [] } }],
+ preload: { issues: nil }
)
end
@@ -127,7 +160,8 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ issues: { except: [:name],
include: [],
- only: [:description] } }]
+ only: [:description] } }],
+ preload: { issues: nil }
)
end
@@ -137,7 +171,8 @@ describe Gitlab::ImportExport::AttributesFinder do
is_expected.to match(
include: [{ issues: { include: [],
- methods: [:name] } }]
+ methods: [:name] } }],
+ preload: { issues: nil }
)
end
diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb
index e53db37def4..f09a29b84db 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::ImportExport::Config do
expect { subject }.not_to raise_error
expect(subject).to be_a(Hash)
expect(subject.keys).to contain_exactly(
- :tree, :excluded_attributes, :included_attributes, :methods)
+ :tree, :excluded_attributes, :included_attributes, :methods, :preloads)
end
end
end
@@ -55,6 +55,10 @@ describe Gitlab::ImportExport::Config do
events:
- :action
+ preloads:
+ statuses:
+ project:
+
ee:
tree:
project:
@@ -71,6 +75,9 @@ describe Gitlab::ImportExport::Config do
- :type_ee
events_ee:
- :action_ee
+ preloads:
+ statuses:
+ bridge_ee:
EOF
end
@@ -111,6 +118,11 @@ describe Gitlab::ImportExport::Config do
methods: {
labels: [:type],
events: [:action]
+ },
+ preloads: {
+ statuses: {
+ project: nil
+ }
}
}
)
@@ -150,6 +162,12 @@ describe Gitlab::ImportExport::Config do
labels: [:type, :type_ee],
events: [:action],
events_ee: [:action_ee]
+ },
+ preloads: {
+ statuses: {
+ project: nil,
+ bridge_ee: nil
+ }
}
}
)
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
new file mode 100644
index 00000000000..d23b27c9d8e
--- /dev/null
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -0,0 +1,272 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::FastHashSerializer do
+ subject { described_class.new(project, tree).execute }
+
+ let!(:project) { setup_project }
+ let(:user) { create(:user) }
+ let(:shared) { project.import_export_shared }
+ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
+ let(:tree) { reader.project_tree }
+
+ before do
+ project.add_maintainer(user)
+ allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD')
+ allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA')
+ end
+
+ it 'saves the correct hash' do
+ is_expected.to include({ 'description' => 'description', 'visibility_level' => 20 })
+ end
+
+ it 'has approvals_before_merge set' do
+ expect(subject['approvals_before_merge']).to eq(1)
+ end
+
+ it 'has milestones' do
+ expect(subject['milestones']).not_to be_empty
+ end
+
+ it 'has merge requests' do
+ expect(subject['merge_requests']).not_to be_empty
+ end
+
+ it 'has merge request\'s milestones' do
+ expect(subject['merge_requests'].first['milestone']).not_to be_empty
+ end
+
+ it 'has merge request\'s source branch SHA' do
+ expect(subject['merge_requests'].first['source_branch_sha']).to eq('ABCD')
+ end
+
+ it 'has merge request\'s target branch SHA' do
+ expect(subject['merge_requests'].first['target_branch_sha']).to eq('DCBA')
+ end
+
+ it 'has events' do
+ expect(subject['merge_requests'].first['milestone']['events']).not_to be_empty
+ end
+
+ it 'has snippets' do
+ expect(subject['snippets']).not_to be_empty
+ end
+
+ it 'has snippet notes' do
+ expect(subject['snippets'].first['notes']).not_to be_empty
+ end
+
+ it 'has releases' do
+ expect(subject['releases']).not_to be_empty
+ end
+
+ it 'has no author on releases' do
+ expect(subject['releases'].first['author']).to be_nil
+ end
+
+ it 'has the author ID on releases' do
+ expect(subject['releases'].first['author_id']).not_to be_nil
+ end
+
+ it 'has issues' do
+ expect(subject['issues']).not_to be_empty
+ end
+
+ it 'has issue comments' do
+ notes = subject['issues'].first['notes']
+
+ expect(notes).not_to be_empty
+ expect(notes.first['type']).to eq('DiscussionNote')
+ end
+
+ it 'has issue assignees' do
+ expect(subject['issues'].first['issue_assignees']).not_to be_empty
+ end
+
+ it 'has author on issue comments' do
+ expect(subject['issues'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has project members' do
+ expect(subject['project_members']).not_to be_empty
+ end
+
+ it 'has merge requests diffs' do
+ expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty
+ end
+
+ it 'has merge request diff files' do
+ expect(subject['merge_requests'].first['merge_request_diff']['merge_request_diff_files']).not_to be_empty
+ end
+
+ it 'has merge request diff commits' do
+ expect(subject['merge_requests'].first['merge_request_diff']['merge_request_diff_commits']).not_to be_empty
+ end
+
+ it 'has merge requests comments' do
+ expect(subject['merge_requests'].first['notes']).not_to be_empty
+ end
+
+ it 'has author on merge requests comments' do
+ expect(subject['merge_requests'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has pipeline stages' do
+ expect(subject.dig('ci_pipelines', 0, 'stages')).not_to be_empty
+ end
+
+ it 'has pipeline statuses' do
+ expect(subject.dig('ci_pipelines', 0, 'stages', 0, 'statuses')).not_to be_empty
+ end
+
+ it 'has pipeline builds' do
+ builds_count = subject
+ .dig('ci_pipelines', 0, 'stages', 0, 'statuses')
+ .count { |hash| hash['type'] == 'Ci::Build' }
+
+ expect(builds_count).to eq(1)
+ end
+
+ it 'has no when YML attributes but only the DB column' do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:ci_yaml_file)
+ .and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
+
+ expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
+
+ subject
+ end
+
+ it 'has pipeline commits' do
+ expect(subject['ci_pipelines']).not_to be_empty
+ end
+
+ it 'has ci pipeline notes' do
+ expect(subject['ci_pipelines'].first['notes']).not_to be_empty
+ end
+
+ it 'has labels with no associations' do
+ expect(subject['labels']).not_to be_empty
+ end
+
+ it 'has labels associated to records' do
+ expect(subject['issues'].first['label_links'].first['label']).not_to be_empty
+ end
+
+ it 'has project and group labels' do
+ label_types = subject['issues'].first['label_links'].map { |link| link['label']['type'] }
+
+ expect(label_types).to match_array(%w(ProjectLabel GroupLabel))
+ end
+
+ it 'has priorities associated to labels' do
+ priorities = subject['issues'].first['label_links'].flat_map { |link| link['label']['priorities'] }
+
+ expect(priorities).not_to be_empty
+ end
+
+ it 'has issue resource label events' do
+ expect(subject['issues'].first['resource_label_events']).not_to be_empty
+ end
+
+ it 'has merge request resource label events' do
+ expect(subject['merge_requests'].first['resource_label_events']).not_to be_empty
+ end
+
+ it 'saves the correct service type' do
+ expect(subject['services'].first['type']).to eq('CustomIssueTrackerService')
+ end
+
+ it 'saves the properties for a service' do
+ expect(subject['services'].first['properties']).to eq('one' => 'value')
+ end
+
+ it 'has project feature' do
+ project_feature = subject['project_feature']
+ expect(project_feature).not_to be_empty
+ expect(project_feature["issues_access_level"]).to eq(ProjectFeature::DISABLED)
+ expect(project_feature["wiki_access_level"]).to eq(ProjectFeature::ENABLED)
+ expect(project_feature["builds_access_level"]).to eq(ProjectFeature::PRIVATE)
+ end
+
+ it 'has custom attributes' do
+ expect(subject['custom_attributes'].count).to eq(2)
+ end
+
+ it 'has badges' do
+ expect(subject['project_badges'].count).to eq(2)
+ end
+
+ it 'does not complain about non UTF-8 characters in MR diff files' do
+ ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
+
+ expect(subject['merge_requests'].first['merge_request_diff']).not_to be_empty
+ end
+
+ context 'project attributes' do
+ it 'does not contain the runners token' do
+ expect(subject).not_to include("runners_token" => 'token')
+ end
+ end
+
+ it 'has a board and a list' do
+ expect(subject['boards'].first['lists']).not_to be_empty
+ end
+
+ def setup_project
+ issue = create(:issue, assignees: [user])
+ snippet = create(:project_snippet)
+ release = create(:release)
+ group = create(:group)
+
+ project = create(:project,
+ :public,
+ :repository,
+ :issues_disabled,
+ :wiki_enabled,
+ :builds_private,
+ description: 'description',
+ issues: [issue],
+ snippets: [snippet],
+ releases: [release],
+ group: group,
+ approvals_before_merge: 1
+ )
+ project_label = create(:label, project: project)
+ group_label = create(:group_label, group: group)
+ create(:label_link, label: project_label, target: issue)
+ create(:label_link, label: group_label, target: issue)
+ create(:label_priority, label: group_label, priority: 1)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
+
+ ci_build = create(:ci_build, project: project, when: nil)
+ ci_build.pipeline.update(project: project)
+ create(:commit_status, project: project, pipeline: ci_build.pipeline)
+
+ create(:milestone, project: project)
+ create(:discussion_note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_build.pipeline.sha)
+
+ create(:resource_label_event, label: project_label, issue: issue)
+ create(:resource_label_event, label: group_label, merge_request: merge_request)
+
+ create(:event, :created, target: milestone, project: project, author: user)
+ create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' })
+
+ create(:project_custom_attribute, project: project)
+ create(:project_custom_attribute, project: project)
+
+ create(:project_badge, project: project)
+ create(:project_badge, project: project)
+
+ board = create(:board, project: project, name: 'TestBoard')
+ create(:list, board: board, position: 0, label: project_label)
+
+ project
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index fefbed93316..ff46e062a5d 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -23,12 +23,65 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(project_tree_saver.save).to be true
end
+ context ':export_fast_serialize feature flag checks' do
+ before do
+ expect(Gitlab::ImportExport::Reader).to receive(:new).with(shared: shared).and_return(reader)
+ expect(reader).to receive(:project_tree).and_return(project_tree)
+ end
+
+ let(:serializer) { instance_double('Gitlab::ImportExport::FastHashSerializer') }
+ let(:reader) { instance_double('Gitlab::ImportExport::Reader') }
+ let(:project_tree) do
+ {
+ include: [{ issues: { include: [] } }],
+ preload: { issues: nil }
+ }
+ end
+
+ context 'when :export_fast_serialize feature is enabled' do
+ before do
+ stub_feature_flags(export_fast_serialize: true)
+ end
+
+ it 'uses FastHashSerializer' do
+ expect(Gitlab::ImportExport::FastHashSerializer)
+ .to receive(:new)
+ .with(project, project_tree)
+ .and_return(serializer)
+
+ expect(serializer).to receive(:execute)
+
+ project_tree_saver.save
+ end
+ end
+
+ context 'when :export_fast_serialize feature is disabled' do
+ before do
+ stub_feature_flags(export_fast_serialize: false)
+ end
+
+ it 'is serialized via built-in `as_json`' do
+ expect(project).to receive(:as_json).with(project_tree)
+
+ project_tree_saver.save
+ end
+ end
+ end
+
+ # It is mostly duplicated in
+ # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb`
+ # except:
+ # context 'with description override' do
+ # context 'group members' do
+ # ^ These are specific for the ProjectTreeSaver
context 'JSON' do
let(:saved_project_json) do
project_tree_saver.save
project_json(project_tree_saver.full_path)
end
+ # It is not duplicated in
+ # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb`
context 'with description override' do
let(:params) { { description: 'Foo Bar' } }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared, params: params) }
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/pages_spec.rb b/spec/lib/gitlab/pages_spec.rb
new file mode 100644
index 00000000000..affa2ebab2a
--- /dev/null
+++ b/spec/lib/gitlab/pages_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Pages do
+ let(:pages_shared_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
+
+ before do
+ allow(described_class).to receive(:secret).and_return(pages_shared_secret)
+ end
+
+ describe '.verify_api_request' do
+ let(:payload) { { 'iss' => 'gitlab-pages' } }
+
+ it 'returns false if fails to validate the JWT' do
+ encoded_token = JWT.encode(payload, 'wrongsecret', 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers)).to eq(false)
+ end
+
+ it 'returns the decoded JWT' do
+ encoded_token = JWT.encode(payload, described_class.secret, 'HS256')
+ headers = { described_class::INTERNAL_API_REQUEST_HEADER => encoded_token }
+
+ expect(described_class.verify_api_request(headers)).to eq([{ "iss" => "gitlab-pages" }, { "alg" => "HS256" }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 0dbfcf96124..e0b9581c75c 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -4,6 +4,8 @@
require 'spec_helper'
describe Gitlab::ProjectSearchResults do
+ include SearchHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { 'hello world' }
@@ -31,10 +33,10 @@ describe Gitlab::ProjectSearchResults do
where(:scope, :count_method, :expected) do
'blobs' | :blobs_count | '1234'
- 'notes' | :limited_notes_count | '1000+'
+ 'notes' | :limited_notes_count | max_limited_count
'wiki_blobs' | :wiki_blobs_count | '1234'
'commits' | :commits_count | '1234'
- 'projects' | :limited_projects_count | '1000+'
+ 'projects' | :limited_projects_count | max_limited_count
'unknown' | nil | nil
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/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 5621c686b8a..26cba53502d 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::SearchResults do
include ProjectForksHelper
+ include SearchHelpers
let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
@@ -35,11 +36,11 @@ describe Gitlab::SearchResults do
using RSpec::Parameterized::TableSyntax
where(:scope, :count_method, :expected) do
- 'projects' | :limited_projects_count | '1000+'
- 'issues' | :limited_issues_count | '1000+'
- 'merge_requests' | :limited_merge_requests_count | '1000+'
- 'milestones' | :limited_milestones_count | '1000+'
- 'users' | :limited_users_count | '1000+'
+ 'projects' | :limited_projects_count | max_limited_count
+ 'issues' | :limited_issues_count | max_limited_count
+ 'merge_requests' | :limited_merge_requests_count | max_limited_count
+ 'milestones' | :limited_milestones_count | max_limited_count
+ 'users' | :limited_users_count | max_limited_count
'unknown' | nil | nil
end
@@ -56,9 +57,9 @@ describe Gitlab::SearchResults do
where(:count, :expected) do
23 | '23'
- 1000 | '1000'
- 1001 | '1000+'
- 1234 | '1000+'
+ 100 | '100'
+ 101 | max_limited_count
+ 1234 | max_limited_count
end
with_them do
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/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index 89d290aaa81..d3353b76c15 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::SnippetSearchResults do
+ include SearchHelpers
+
let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
let(:results) { described_class.new(Snippet.all, 'foo') }
@@ -25,7 +27,7 @@ describe Gitlab::SnippetSearchResults do
where(:scope, :count_method, :expected) do
'snippet_titles' | :snippet_titles_count | '1234'
'snippet_blobs' | :snippet_blobs_count | '1234'
- 'projects' | :limited_projects_count | '1000+'
+ 'projects' | :limited_projects_count | max_limited_count
'unknown' | nil | nil
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/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/user_spec.rb b/spec/models/user_spec.rb
index b8c323904b8..6722a3c627d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -32,7 +32,7 @@ describe User do
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
- it { is_expected.to have_many(:events).dependent(:destroy) }
+ it { is_expected.to have_many(:events).dependent(:delete_all) }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
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/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
new file mode 100644
index 00000000000..0b3c5be9c45
--- /dev/null
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Internal::Pages do
+ describe "GET /internal/pages" do
+ let(:pages_shared_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
+
+ before do
+ allow(Gitlab::Pages).to receive(:secret).and_return(pages_shared_secret)
+ end
+
+ def query_host(host, headers = {})
+ get api("/internal/pages"), headers: headers, params: { host: host }
+ end
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(pages_internal_api: false)
+ end
+
+ it 'responds with 404 Not Found' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'feature flag enabled' do
+ context 'not authenticated' do
+ it 'responds with 401 Unauthorized' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context 'authenticated' do
+ def query_host(host)
+ jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
+ headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
+
+ super(host, headers)
+ end
+
+ it 'responds with 200 OK' do
+ query_host('pages.gitlab.io')
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ 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/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 048d04cdefd..d98b9be726a 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -252,5 +252,43 @@ describe API::Settings, 'Settings' do
expect(json_response['asset_proxy_whitelist']).to eq(['example.com', '*.example.com', 'localhost'])
end
end
+
+ context 'domain_blacklist settings' do
+ it 'rejects domain_blacklist_enabled when domain_blacklist is empty' do
+ put api('/application/settings', admin),
+ params: {
+ domain_blacklist_enabled: true,
+ domain_blacklist: []
+ }
+
+ expect(response).to have_gitlab_http_status(400)
+ message = json_response["message"]
+ expect(message["domain_blacklist"]).to eq(["Domain blacklist cannot be empty if Blacklist is enabled."])
+ end
+
+ it 'allows array for domain_blacklist' do
+ put api('/application/settings', admin),
+ params: {
+ domain_blacklist_enabled: true,
+ domain_blacklist: ['domain1.com', 'domain2.com']
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['domain_blacklist_enabled']).to be(true)
+ expect(json_response['domain_blacklist']).to eq(['domain1.com', 'domain2.com'])
+ end
+
+ it 'allows a string for domain_blacklist' do
+ put api('/application/settings', admin),
+ params: {
+ domain_blacklist_enabled: true,
+ domain_blacklist: 'domain3.com, *.domain4.com'
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['domain_blacklist_enabled']).to be(true)
+ expect(json_response['domain_blacklist']).to eq(['domain3.com', '*.domain4.com'])
+ end
+ end
end
end
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/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index d8880819d9f..fe86982af91 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -760,33 +760,32 @@ describe Ci::CreatePipelineService do
end
context 'when builds with auto-retries are configured' do
+ let(:pipeline) { execute_service }
+ let(:rspec_job) { pipeline.builds.find_by(name: 'rspec') }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump({
+ rspec: { script: 'rspec', retry: retry_value }
+ }))
+ end
+
context 'as an integer' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
- stub_ci_pipeline_yaml_file(config)
- end
+ let(:retry_value) { 2 }
it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
-
expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
- expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always']
+ expect(rspec_job.retries_max).to eq 2
+ expect(rspec_job.retry_when).to eq ['always']
end
end
context 'as hash' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } })
- stub_ci_pipeline_yaml_file(config)
- end
+ let(:retry_value) { { max: 2, when: 'runner_system_failure' } }
it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
-
expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
- expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure']
+ expect(rspec_job.retries_max).to eq 2
+ expect(rspec_job.retry_when).to eq ['runner_system_failure']
end
end
end
@@ -1174,7 +1173,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to be_persisted
expect(pipeline).to be_merge_request_event
expect(pipeline.merge_request).to eq(merge_request)
- expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test])
+ expect(pipeline.builds.order(:stage_id).pluck(:name)).to eq(%w[test])
end
it 'persists the specified source sha' do
@@ -1439,7 +1438,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to be_persisted
expect(pipeline).to be_web
expect(pipeline.merge_request).to be_nil
- expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[build pages])
+ expect(pipeline.builds.order(:stage_id).pluck(:name)).to eq(%w[build pages])
end
end
end
@@ -1479,7 +1478,7 @@ describe Ci::CreatePipelineService do
it 'creates a pipeline with build_a and test_a' do
expect(pipeline).to be_persisted
- expect(pipeline.builds.map(&:name)).to contain_exactly("build_a", "test_a")
+ expect(pipeline.builds.pluck(:name)).to contain_exactly("build_a", "test_a")
end
end
@@ -1514,7 +1513,303 @@ describe Ci::CreatePipelineService do
it 'does create a pipeline only with deploy' do
expect(pipeline).to be_persisted
- expect(pipeline.builds.map(&:name)).to contain_exactly("deploy")
+ expect(pipeline.builds.pluck(:name)).to contain_exactly("deploy")
+ end
+ end
+ end
+
+ context 'when rules are used' do
+ let(:ref_name) { 'refs/heads/master' }
+ let(:pipeline) { execute_service }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+ let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') }
+ let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') }
+ let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') }
+
+ shared_examples 'rules jobs are excluded' do
+ it 'only persists the job without rules' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_nil
+ expect(delayed_job).to be_nil
+ end
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ end
+
+ context 'with simple if: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ master-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "nonexistant-branch"
+ when: never
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: manual
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ when: delayed
+ start_in: 1 hour
+
+ never-job:
+ script: "echo Goodbye, World!"
+ rules:
+ - if: $CI_COMMIT_REF_NAME
+ when: never
+ EOY
+ end
+
+ context 'with matches' do
+ it 'creates a pipeline with the vanilla and manual jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'delayed-job', 'master-job')
+ end
+
+ it 'assigns job:when values to the builds' do
+ expect(pipeline.builds.pluck(:when)).to contain_exactly('on_success', 'delayed', 'manual')
+ end
+
+ it 'assigns start_in for delayed jobs' do
+ expect(delayed_job.options[:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'with no matches' do
+ let(:ref_name) { 'refs/heads/feature' }
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+ end
+
+ context 'with complex if: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+ rules:
+ - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME
+ when: manual
+ EOY
+ end
+
+ it 'matches the first rule' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job')
+ expect(regular_job.when).to eq('manual')
+ end
+ end
+
+ context 'with changes:' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - changes:
+ - README.md
+ when: manual
+ - changes:
+ - app.rb
+ when: on_success
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - changes:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+ EOY
+ end
+
+ context 'and matches' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[README.md])
+ end
+
+ it 'creates two jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names)
+ .to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+ end
+
+ context 'and matches the second rule' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[app.rb])
+ end
+
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ end
+ end
+
+ context 'and does not match' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[useless_script.rb])
+ end
+
+ it_behaves_like 'rules jobs are excluded'
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+ end
+
+ context 'with mixed if: and changes: rules' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - changes:
+ - README.md
+ when: manual
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: on_success
+
+ delayed-job:
+ script: "echo See you later, World!"
+ rules:
+ - changes:
+ - README.md
+ when: delayed
+ start_in: 4 hours
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: delayed
+ start_in: 1 hour
+ EOY
+ end
+
+ context 'and changes: matches before if' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[README.md])
+ end
+
+ it 'creates two jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names)
+ .to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for all jobs' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('manual')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('4 hours')
+ end
+ end
+
+ context 'and if: matches after changes' do
+ it 'includes both jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job')
+ end
+
+ it 'sets when: for the created rules job based on the second clause' do
+ expect(regular_job.when).to eq('on_success')
+ expect(rules_job.when).to eq('on_success')
+ expect(delayed_job.when).to eq('delayed')
+ expect(delayed_job.options[:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'and does not match' do
+ let(:ref_name) { 'refs/heads/wip' }
+
+ it_behaves_like 'rules jobs are excluded'
+
+ it 'sets when: for the created job' do
+ expect(regular_job.when).to eq('on_success')
+ end
+ end
+ end
+
+ context 'with mixed if: and changes: clauses' do
+ let(:config) do
+ <<-EOY
+ regular-job:
+ script: 'echo Hello, World!'
+
+ rules-job:
+ script: "echo hello world, $CI_COMMIT_REF_NAME"
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ changes: [README.md]
+ when: on_success
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ changes: [app.rb]
+ when: manual
+ EOY
+ end
+
+ context 'with if matches and changes matches' do
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[app.rb])
+ end
+
+ it 'persists all jobs' do
+ expect(pipeline).to be_persisted
+ expect(regular_job).to be_persisted
+ expect(rules_job).to be_persisted
+ expect(rules_job.when).to eq('manual')
+ end
+ end
+
+ context 'with if matches and no change matches' do
+ it_behaves_like 'rules jobs are excluded'
+ end
+
+ context 'with change matches and no if matches' do
+ let(:ref_name) { 'refs/heads/feature' }
+
+ before do
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:modified_paths).and_return(%w[README.md])
+ end
+
+ it_behaves_like 'rules jobs are excluded'
+ end
+
+ context 'and no matches' do
+ let(:ref_name) { 'refs/heads/feature' }
+
+ it_behaves_like 'rules jobs are excluded'
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/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index 2cf3f4b83c4..d1d25fbabcd 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -19,4 +19,8 @@ module SearchHelpers
click_link scope
end
end
+
+ def max_limited_count
+ Gitlab::SearchResults::COUNT_LIMIT_MESSAGE
+ end
end
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index ed2a3243f0d..116bc8d0b9c 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -13,6 +13,10 @@ tree:
group_members:
- :user
+preloads:
+ merge_request_diff:
+ source_project:
+
included_attributes:
merge_requests:
- :id
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 76d82649c5f..f2f31e1b7f2 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
shared_examples_for 'multiple issue boards' do
- dropdown_selector = '.js-boards-selector .dropdown-menu'
-
context 'authorized user' do
before do
parent.add_maintainer(user)
@@ -20,18 +18,14 @@ shared_examples_for 'multiple issue boards' do
end
it 'shows a list of boards' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
expect(page).to have_content(board.name)
expect(page).to have_content(board2.name)
end
end
it 'switches current board' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_link board2.name
end
@@ -43,9 +37,7 @@ shared_examples_for 'multiple issue boards' do
end
it 'creates new board without detailed configuration' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_button 'Create new board'
end
@@ -57,28 +49,23 @@ shared_examples_for 'multiple issue boards' do
end
it 'deletes board' do
- click_button board.name
-
- wait_for_requests
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_button 'Delete board'
end
expect(page).to have_content('Are you sure you want to delete this board?')
click_button 'Delete'
- click_button board2.name
- page.within(dropdown_selector) do
+ wait_for_requests
+
+ in_boards_switcher_dropdown do
expect(page).not_to have_content(board.name)
expect(page).to have_content(board2.name)
end
end
it 'adds a list to the none default board' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_link board2.name
end
@@ -100,9 +87,7 @@ shared_examples_for 'multiple issue boards' do
expect(page).to have_selector('.board', count: 3)
- click_button board2.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
click_link board.name
end
@@ -114,9 +99,9 @@ shared_examples_for 'multiple issue boards' do
it 'maintains sidebar state over board switch' do
assert_boards_nav_active
- find('.boards-switcher').click
- wait_for_requests
- click_link board2.name
+ in_boards_switcher_dropdown do
+ click_link board2.name
+ end
assert_boards_nav_active
end
@@ -129,15 +114,24 @@ shared_examples_for 'multiple issue boards' do
end
it 'does not show action links' do
- click_button board.name
-
- page.within(dropdown_selector) do
+ in_boards_switcher_dropdown do
expect(page).not_to have_content('Create new board')
expect(page).not_to have_content('Delete board')
end
end
end
+ def in_boards_switcher_dropdown
+ find('.boards-switcher').click
+
+ wait_for_requests
+
+ dropdown_selector = '.js-boards-selector .dropdown-menu'
+ page.within(dropdown_selector) do
+ yield
+ end
+ end
+
def assert_boards_nav_active
expect(find('.nav-sidebar .active .active')).to have_selector('a', text: 'Boards')
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