summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouglas Barbosa Alexandre <dbalexandre@gmail.com>2019-09-09 18:22:40 -0300
committerDouglas Barbosa Alexandre <dbalexandre@gmail.com>2019-09-09 18:22:40 -0300
commit58423fa8757726b4840d3b8db334212ed7c705a4 (patch)
tree7696ba5f1d40c6b9ecad50e6da4b45275cf36d4f
parented1192c511057230dfd90f8b873b7813a48ecd6b (diff)
parent814d12b8c72b0e8a4f9025ffb1373c11b36a061a (diff)
downloadgitlab-ce-58423fa8757726b4840d3b8db334212ed7c705a4.tar.gz
Merge remote-tracking branch 'origin/master' into camilstaps/gitlab-ce-new-66023-public-private-fork-counts
-rw-r--r--.gitignore1
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml3
-rw-r--r--.gitlab/issue_templates/Feature proposal.md12
-rw-r--r--.gitlab/merge_request_templates/Documentation.md1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock12
-rw-r--r--app/assets/javascripts/admin/statistics_panel/components/app.vue45
-rw-r--r--app/assets/javascripts/admin/statistics_panel/constants.js14
-rw-r--r--app/assets/javascripts/admin/statistics_panel/index.js22
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/actions.js28
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/getters.js17
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/index.js16
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/mutations.js16
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/state.js5
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js8
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue1
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue1
-rw-r--r--app/assets/javascripts/pages/admin/index.js7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue3
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_component_mixin.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_toggle_vuex.vue49
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/controllers/admin/dashboard_controller.rb3
-rw-r--r--app/controllers/clusters/base_controller.rb4
-rw-r--r--app/controllers/clusters/clusters_controller.rb13
-rw-r--r--app/controllers/projects/services_controller.rb27
-rw-r--r--app/controllers/registrations_controller.rb1
-rw-r--r--app/finders/issuable_finder.rb23
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/user_callouts_helper.rb6
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/ci/pipeline_enums.rb3
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/external_pull_request.rb96
-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.rb2
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/service.rb7
-rw-r--r--app/models/user.rb2
-rw-r--r--app/policies/clusters/instance_policy.rb1
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/instance_clusterable_presenter.rb4
-rw-r--r--app/serializers/merge_request_noteable_entity.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb5
-rw-r--r--app/services/external_pull_requests/create_pipeline_service.rb29
-rw-r--r--app/services/git/base_hooks_service.rb28
-rw-r--r--app/validators/certificate_key_validator.rb4
-rw-r--r--app/validators/named_ecdsa_key_validator.rb34
-rw-r--r--app/views/admin/dashboard/index.html.haml36
-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/help/_shortcuts.html.haml350
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml6
-rw-r--r--app/views/projects/buttons/_fork.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml17
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml4
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/update_external_pull_requests_worker.rb25
-rw-r--r--changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml5
-rw-r--r--changelogs/unreleased/46686-bump-kubeclient-version-qa.yml5
-rw-r--r--changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml5
-rw-r--r--changelogs/unreleased/60724-watch-button.yml5
-rw-r--r--changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml5
-rw-r--r--changelogs/unreleased/ac-accelerate-wiki-attachments.yml5
-rw-r--r--changelogs/unreleased/api_settings.yml5
-rw-r--r--changelogs/unreleased/ecdsa_pages_certificates.yml5
-rw-r--r--changelogs/unreleased/fj-11777-lower-search-count-limits.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.62.0.yml5
-rw-r--r--changelogs/unreleased/id-autosave-for-new-mr.yml5
-rw-r--r--changelogs/unreleased/instance-group-level-knative.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/keyboard-shortcuts-2.yml5
-rw-r--r--changelogs/unreleased/pl-project-service-json.yml5
-rw-r--r--changelogs/unreleased/quote-branch-names-in-instructions.yml5
-rw-r--r--changelogs/unreleased/sh-add-margin-member-list.yml5
-rw-r--r--changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml5
-rw-r--r--changelogs/unreleased/update-rouge.yml5
-rw-r--r--changelogs/unreleased/use_default_external_auth_label_empty.yml6
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb21
-rw-r--r--db/migrate/20190829131130_create_external_pull_requests.rb25
-rw-r--r--db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb15
-rw-r--r--db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb17
-rw-r--r--db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb17
-rw-r--r--db/schema.rb19
-rw-r--r--doc/administration/database_load_balancing.md6
-rw-r--r--doc/administration/integration/plantuml.md17
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md6
-rw-r--r--doc/api/api_resources.md1
-rw-r--r--doc/api/merge_request_approvals.md71
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/statistics.md35
-rw-r--r--doc/api/v3_to_v4.md4
-rw-r--r--doc/ci/docker/using_docker_build.md7
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md14
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/development/background_migrations.md8
-rw-r--r--doc/development/dangerbot.md2
-rw-r--r--doc/development/documentation/styleguide.md2
-rw-r--r--doc/development/fe_guide/development_process.md18
-rw-r--r--doc/development/testing_guide/end_to_end/page_objects.md12
-rw-r--r--doc/integration/omniauth.md30
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/update/mysql_to_postgresql.md10
-rw-r--r--doc/user/admin_area/diff_limits.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md19
-rw-r--r--doc/user/application_security/sast/index.md19
-rw-r--r--[-rwxr-xr-x]doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.pngbin61667 -> 61667 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.pngbin52247 -> 52247 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.pngbin48767 -> 48767 bytes
-rw-r--r--doc/user/clusters/applications.md45
-rw-r--r--doc/user/project/description_templates.md2
-rw-r--r--doc/user/project/import/github.md29
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff_v12_2.pngbin0 -> 136144 bytes
-rw-r--r--doc/user/project/merge_requests/index.md22
-rw-r--r--doc/user/project/web_ide/index.md2
-rw-r--r--doc/workflow/repository_mirroring.md2
-rw-r--r--doc/workflow/shortcuts.md191
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/entities.rb49
-rw-r--r--lib/api/internal/pages.rb27
-rw-r--r--lib/api/settings.rb8
-rw-r--r--lib/api/statistics.rb18
-rw-r--r--lib/api/validations/types/workhorse_file.rb20
-rw-r--r--lib/api/wikis.rb22
-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/chain/build.rb1
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb1
-rw-r--r--lib/gitlab/danger/helper.rb7
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb65
-rw-r--r--lib/gitlab/import_export/config.rb81
-rw-r--r--lib/gitlab/import_export/fast_hash_serializer.rb108
-rw-r--r--lib/gitlab/import_export/import_export.yml178
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb117
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb85
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb27
-rw-r--r--lib/gitlab/import_export/reader.rb30
-rw-r--r--lib/gitlab/middleware/multipart.rb18
-rw-r--r--lib/gitlab/pages.rb17
-rw-r--r--lib/gitlab/push_options.rb20
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb2
-rw-r--r--lib/gitlab/search_results.rb5
-rw-r--r--lib/tasks/gitlab_danger.rake2
-rw-r--r--locale/gitlab.pot182
-rw-r--r--package.json5
-rw-r--r--qa/README.md4
-rw-r--r--qa/qa/page/main/menu.rb4
-rw-r--r--qa/qa/page/merge_request/new.rb2
-rw-r--r--qa/qa/resource/group.rb6
-rw-r--r--qa/qa/resource/merge_request.rb19
-rw-r--r--qa/qa/resource/project.rb9
-rw-r--r--qa/qa/resource/user.rb1
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb19
-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/services_controller_spec.rb130
-rw-r--r--spec/controllers/registrations_controller_spec.rb20
-rw-r--r--spec/factories/external_pull_requests.rb17
-rw-r--r--spec/factories/pages_domains.rb83
-rw-r--r--spec/features/admin/clusters/applications_spec.rb21
-rw-r--r--spec/features/admin/dashboard_spec.rb2
-rw-r--r--spec/features/clusters/installing_applications_shared_examples.rb228
-rw-r--r--spec/features/groups/clusters/applications_spec.rb23
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb11
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/projects/clusters/applications_spec.rb229
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb3
-rw-r--r--spec/features/projects/clusters_spec.rb24
-rw-r--r--spec/finders/issues_finder_spec.rb50
-rw-r--r--spec/finders/merge_requests_finder_spec.rb20
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_noteable.json4
-rw-r--r--spec/fixtures/api/schemas/statistics.json29
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js73
-rw-r--r--spec/frontend/admin/statistics_panel/mock_data.js15
-rw-r--r--spec/frontend/admin/statistics_panel/store/actions_spec.js115
-rw-r--r--spec/frontend/admin/statistics_panel/store/getters_spec.js48
-rw-r--r--spec/frontend/admin/statistics_panel/store/mutations_spec.js41
-rw-r--r--spec/frontend/clusters/components/applications_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gl_toggle_vuex_spec.js115
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js26
-rw-r--r--spec/javascripts/boards/mock_data.js2
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb34
-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/danger/helper_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attributes_finder_spec.rb230
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb284
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb272
-rw-r--r--spec/lib/gitlab/import_export/model_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb18
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb53
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb105
-rw-r--r--spec/lib/gitlab/import_export/relation_rename_service_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml14
-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/search_results_spec.rb17
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb20
-rw-r--r--spec/models/external_pull_request_spec.rb220
-rw-r--r--spec/models/pages_domain_spec.rb18
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/internal/pages_spec.rb54
-rw-r--r--spec/requests/api/settings_spec.rb38
-rw-r--r--spec/requests/api/statistics_spec.rb91
-rw-r--r--spec/requests/api/wikis_spec.rb13
-rw-r--r--spec/requests/projects/uploads_spec.rb38
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb5
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb489
-rw-r--r--spec/services/external_pull_requests/create_pipeline_service_spec.rb72
-rw-r--r--spec/services/git/branch_push_service_spec.rb14
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb13
-rw-r--r--spec/support/helpers/search_helpers.rb4
-rw-r--r--spec/support/helpers/workhorse_helpers.rb31
-rw-r--r--spec/support/import_export/import_export.yml25
-rw-r--r--spec/validators/named_ecdsa_key_validator_spec.rb54
-rw-r--r--spec/workers/update_external_pull_requests_worker_spec.rb54
-rw-r--r--yarn.lock21
247 files changed, 5487 insertions, 1531 deletions
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/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/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index ba9624aeeab..e502614b5ca 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -15,6 +15,7 @@
## Author's checklist
- [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
+- [ ] If applicable, update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html).
- [ ] Link docs to and from the higher-level index page, plus other related docs where helpful.
- [ ] Apply the ~Documentation label.
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/Gemfile b/Gemfile
index fdb30aeb187..ac848cce5e8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -106,7 +106,7 @@ gem 'fog-aws', '~> 3.5'
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
# Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0'
-gem 'fog-google', '~> 1.8'
+gem 'fog-google', '~> 1.9'
gem 'fog-local', '~> 0.6'
gem 'fog-openstack', '~> 1.0'
gem 'fog-rackspace', '~> 0.1.1'
@@ -135,7 +135,7 @@ gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 2.0.10'
gem 'asciidoctor-include-ext', '~> 0.3.1', require: false
gem 'asciidoctor-plantuml', '0.0.9'
-gem 'rouge', '~> 3.7'
+gem 'rouge', '~> 3.10'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.4'
@@ -234,7 +234,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 d787b5c0569..48053e5740e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -284,7 +284,7 @@ GEM
excon (~> 0.58)
formatador (~> 0.2)
mime-types
- fog-google (1.8.2)
+ fog-google (1.9.1)
fog-core (<= 2.1.0)
fog-json (~> 1.2)
fog-xml (~> 0.1.0)
@@ -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)
@@ -799,7 +799,7 @@ GEM
retriable (3.1.2)
rinku (2.0.0)
rotp (2.1.2)
- rouge (3.7.0)
+ rouge (3.10.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -1114,7 +1114,7 @@ DEPENDENCIES
fog-aliyun (~> 0.3)
fog-aws (~> 3.5)
fog-core (= 2.1.0)
- fog-google (~> 1.8)
+ fog-google (~> 1.9)
fog-local (~> 0.6)
fog-openstack (~> 1.0)
fog-rackspace (~> 0.1.1)
@@ -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)
@@ -1229,7 +1229,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 3.7)
+ rouge (~> 3.10)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.8.0)
diff --git a/app/assets/javascripts/admin/statistics_panel/components/app.vue b/app/assets/javascripts/admin/statistics_panel/components/app.vue
new file mode 100644
index 00000000000..29077d926cf
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/components/app.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import statisticsLabels from '../constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ data() {
+ return {
+ statisticsLabels,
+ };
+ },
+ computed: {
+ ...mapState(['isLoading', 'statistics']),
+ ...mapGetters(['getStatistics']),
+ },
+ mounted() {
+ this.fetchStatistics();
+ },
+ methods: {
+ ...mapActions(['fetchStatistics']),
+ },
+};
+</script>
+
+<template>
+ <div class="info-well">
+ <div class="well-segment admin-well admin-well-statistics">
+ <h4>{{ __('Statistics') }}</h4>
+ <gl-loading-icon v-if="isLoading" size="md" class="my-3" />
+ <template v-else>
+ <p
+ v-for="statistic in getStatistics(statisticsLabels)"
+ :key="statistic.key"
+ class="js-stats"
+ >
+ {{ statistic.label }}
+ <span class="light float-right">{{ statistic.value }}</span>
+ </p>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/statistics_panel/constants.js b/app/assets/javascripts/admin/statistics_panel/constants.js
new file mode 100644
index 00000000000..2dce19a3894
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/constants.js
@@ -0,0 +1,14 @@
+import { s__ } from '~/locale';
+
+const statisticsLabels = {
+ forks: s__('AdminStatistics|Forks'),
+ issues: s__('AdminStatistics|Issues'),
+ mergeRequests: s__('AdminStatistics|Merge Requests'),
+ notes: s__('AdminStatistics|Notes'),
+ snippets: s__('AdminStatistics|Snippets'),
+ sshKeys: s__('AdminStatistics|SSH Keys'),
+ milestones: s__('AdminStatistics|Milestones'),
+ activeUsers: s__('AdminStatistics|Active Users'),
+};
+
+export default statisticsLabels;
diff --git a/app/assets/javascripts/admin/statistics_panel/index.js b/app/assets/javascripts/admin/statistics_panel/index.js
new file mode 100644
index 00000000000..39112e3ddc0
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import StatisticsPanelApp from './components/app.vue';
+import createStore from './store';
+
+export default function(el) {
+ if (!el) {
+ return false;
+ }
+
+ const store = createStore();
+
+ return new Vue({
+ el,
+ store,
+ components: {
+ StatisticsPanelApp,
+ },
+ render(h) {
+ return h(StatisticsPanelApp);
+ },
+ });
+}
diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js
new file mode 100644
index 00000000000..537025f524c
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js
@@ -0,0 +1,28 @@
+import Api from '~/api';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
+
+export const requestStatistics = ({ commit }) => commit(types.REQUEST_STATISTICS);
+
+export const fetchStatistics = ({ dispatch }) => {
+ dispatch('requestStatistics');
+
+ Api.adminStatistics()
+ .then(({ data }) => {
+ dispatch('receiveStatisticsSuccess', convertObjectPropsToCamelCase(data, { deep: true }));
+ })
+ .catch(error => dispatch('receiveStatisticsError', error));
+};
+
+export const receiveStatisticsSuccess = ({ commit }, statistics) =>
+ commit(types.RECEIVE_STATISTICS_SUCCESS, statistics);
+
+export const receiveStatisticsError = ({ commit }, error) => {
+ commit(types.RECEIVE_STATISTICS_ERROR, error);
+ createFlash(s__('AdminDashboard|Error loading the statistics. Please try again'));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js
new file mode 100644
index 00000000000..24437bc76bf
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js
@@ -0,0 +1,17 @@
+/**
+ * Merges the statisticsLabels with the state's data
+ * and returns an array of the following form:
+ * [{ key: "forks", label: "Forks", value: 50 }]
+ */
+export const getStatistics = state => labels =>
+ Object.keys(labels).map(key => {
+ const result = {
+ key,
+ label: labels[key],
+ value: state.statistics && state.statistics[key] ? state.statistics[key] : null,
+ };
+ return result;
+ });
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/index.js b/app/assets/javascripts/admin/statistics_panel/store/index.js
new file mode 100644
index 00000000000..ece9e6419dd
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/admin/statistics_panel/store/mutation_types.js b/app/assets/javascripts/admin/statistics_panel/store/mutation_types.js
new file mode 100644
index 00000000000..4e0ca4ed3cd
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const REQUEST_STATISTICS = 'REQUEST_STATISTICS';
+export const RECEIVE_STATISTICS_SUCCESS = 'RECEIVE_STATISTICS_SUCCESS';
+export const RECEIVE_STATISTICS_ERROR = 'RECEIVE_STATISTICS_ERROR';
diff --git a/app/assets/javascripts/admin/statistics_panel/store/mutations.js b/app/assets/javascripts/admin/statistics_panel/store/mutations.js
new file mode 100644
index 00000000000..d0fac5cfbab
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/mutations.js
@@ -0,0 +1,16 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_STATISTICS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_STATISTICS_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.error = null;
+ state.statistics = data;
+ },
+ [types.RECEIVE_STATISTICS_ERROR](state, error) {
+ state.isLoading = false;
+ state.error = error;
+ },
+};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/state.js b/app/assets/javascripts/admin/statistics_panel/store/state.js
new file mode 100644
index 00000000000..f2f2dc0a4d2
--- /dev/null
+++ b/app/assets/javascripts/admin/statistics_panel/store/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ error: null,
+ isLoading: false,
+ statistics: null,
+});
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 136ffdf8b9d..1d97ad5ec11 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',
+ adminStatisticsPath: 'api/:version/application/statistics',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -376,6 +377,11 @@ const Api = {
return axios.get(url);
},
+ adminStatistics() {
+ const url = Api.buildUrl(this.adminStatisticsPath);
+ return axios.get(url);
+ },
+
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 7e3515b1f4b..66cb9fd7672 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -46,7 +46,6 @@ export default class Shortcuts {
$(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) {
$(this).remove();
- $('.hidden-shortcut').show();
e.preventDefault();
});
}
@@ -104,7 +103,6 @@ export default class Shortcuts {
return results;
}
- $('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
});
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index c8eb96a625c..f7b327b2af1 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -6,7 +6,7 @@ import { CopyAsGFM } from '../markdown/copy_as_gfm';
import { getSelectedFragment } from '~/lib/utils/common_utils';
export default class ShortcutsIssuable extends Shortcuts {
- constructor(isMergeRequest) {
+ constructor() {
super();
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
@@ -14,12 +14,6 @@ export default class ShortcutsIssuable extends Shortcuts {
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
-
- if (isMergeRequest) {
- this.enabledHelp.push('.hidden-shortcut.merge_requests');
- } else {
- this.enabledHelp.push('.hidden-shortcut.issues');
- }
}
static replyWithSelectedText() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index bef1553703b..b46b4132ba8 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -23,7 +23,5 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
-
- this.enabledHelp.push('.hidden-shortcut.project');
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
index a88c280fa3b..3e791e4673a 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
@@ -11,7 +11,5 @@ export default class ShortcutsNetwork extends ShortcutsNavigation {
Mousetrap.bind(['down', 'j'], graph.scrollDown);
Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
-
- this.enabledHelp.push('.hidden-shortcut.network');
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 208c91a1f08..8b7e6a56d25 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -6,8 +6,6 @@ export default class ShortcutsWiki extends ShortcutsNavigation {
constructor() {
super();
Mousetrap.bind('e', ShortcutsWiki.editWiki);
-
- this.enabledHelp.push('.hidden-shortcut.wiki');
}
static editWiki() {
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index f9284266b72..f9a08f151c5 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -2,9 +2,9 @@
import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
-import ListIssue from '../models/issue';
import boardsStore from '../stores/boards_store';
export default {
@@ -54,6 +54,9 @@ export default {
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
+ const { weightFeatureAvailable } = boardsStore;
+ const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {};
+
const issue = new ListIssue({
title: this.title,
labels,
@@ -61,6 +64,7 @@ export default {
assignees,
milestone,
project_id: this.selectedProject.id,
+ weight,
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 7296426549a..ebb2f5b23e4 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -245,6 +245,7 @@ export default {
<div
v-if="!loading"
ref="content"
+ data-qa-selector="boards_dropdown_content"
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index b6da572b201..27959898fb7 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -458,7 +458,6 @@ export default {
</div>
</application-row>
<application-row
- v-if="isProjectCluster"
id="knative"
:logo-url="knativeLogo"
:title="applications.knative.title"
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index 8a32556f06c..74f2eead755 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,3 +1,8 @@
import initAdmin from './admin';
+import initAdminStatisticsPanel from '../../admin/statistics_panel/index';
-document.addEventListener('DOMContentLoaded', initAdmin());
+document.addEventListener('DOMContentLoaded', () => {
+ const statisticsPanelContainer = document.getElementById('js-admin-statistics-container');
+ initAdmin();
+ initAdminStatisticsPanel(statisticsPanelContainer);
+});
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ba0dea626dc..27c1b639889 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -20,6 +20,9 @@ export default {
<stage-column-component
v-for="(stage, index) in graph"
:key="stage.name"
+ :class="{
+ 'append-right-48': shouldAddRightMargin(index),
+ }"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
index 66e9476dadf..f383a4b3368 100644
--- a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js
@@ -40,5 +40,15 @@ export default {
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
+ /**
+ * CSS class is applied:
+ * - if pipeline graph contains only one stage column component
+ *
+ * @param {number} index
+ * @returns {boolean}
+ */
+ shouldAddRightMargin(index) {
+ return !(index === this.graph.length - 1);
+ },
},
};
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/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index e9218dcec67..b95978b6966 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -395,6 +395,7 @@ img.emoji {
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
.prepend-left-32 { margin-left: 32px; }
+.prepend-left-64 { margin-left: 64px; }
.append-right-4 { margin-right: 4px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
@@ -402,6 +403,8 @@ img.emoji {
.append-right-15 { margin-right: 15px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
+.append-right-32 { margin-right: 32px; }
+.append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-4 { margin-bottom: $gl-padding-4; }
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index fd9a75bc5b6..9c924559135 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -2,6 +2,12 @@
max-width: 98%;
}
+.modal-1040 {
+ @include media-breakpoint-up(xl) {
+ max-width: 1040px;
+ }
+}
+
.modal-header {
background-color: $modal-body-bg;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 5f4db37c317..d4bd5b1b7dc 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -476,10 +476,6 @@
display: inline-block;
vertical-align: top;
- &:not(:last-child) {
- margin-right: 44px;
- }
-
&.left-margin {
&:not(:first-child) {
margin-left: 44px;
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/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 23cc9ee247a..64b959e2431 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -3,8 +3,7 @@
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
- COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
- MergeRequest, Note, Snippet, Key, Milestone].freeze
+ COUNTED_ITEMS = [Project, User, Group].freeze
# rubocop: disable CodeReuse/ActiveRecord
def index
diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb
index ef42f7c4074..188805c6106 100644
--- a/app/controllers/clusters/base_controller.rb
+++ b/app/controllers/clusters/base_controller.rb
@@ -31,6 +31,10 @@ class Clusters::BaseController < ApplicationController
access_denied! unless can?(current_user, :create_cluster, clusterable)
end
+ def authorize_read_prometheus!
+ access_denied! unless can?(current_user, :read_prometheus, clusterable)
+ end
+
def clusterable
raise NotImplementedError
end
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/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/registrations_controller.rb b/app/controllers/registrations_controller.rb
index e773ec09924..fb631f09f10 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -20,6 +20,7 @@ class RegistrationsController < Devise::RegistrationsController
super do |new_user|
persist_accepted_terms_if_required(new_user)
+ yield new_user if block_given?
end
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b735f9ff3b8..8ed6ff56e2b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -193,15 +193,30 @@ class IssuableFinder
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects(min_access_level)
- elsif group
- find_group_projects
else
- Project.public_or_visible_to_user(current_user, min_access_level)
+ projects_public_or_visible_to_user
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
end
+ def projects_public_or_visible_to_user
+ projects =
+ if group
+ if params[:projects]
+ find_group_projects.id_in(params[:projects])
+ else
+ find_group_projects
+ end
+ elsif params[:projects]
+ Project.id_in(params[:projects])
+ else
+ Project
+ end
+
+ projects.public_or_visible_to_user(current_user, min_access_level)
+ end
+
def find_group_projects
return Project.none unless group
@@ -209,7 +224,7 @@ class IssuableFinder
Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
else
group.projects
- end.public_or_visible_to_user(current_user, min_access_level)
+ end
end
def search
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/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index d5e459311f7..f10fadfdf49 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
module UserCalloutsHelper
- GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
- GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
- SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'.freeze
+ GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'
+ SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d620959b538..d2271c1335c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -23,6 +23,7 @@ module Ci
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
belongs_to :merge_request, class_name: 'MergeRequest'
+ belongs_to :external_pull_request
has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
@@ -64,6 +65,11 @@ module Ci
validates :merge_request, presence: { if: :merge_request_event? }
validates :merge_request, absence: { unless: :merge_request_event? }
validates :tag, inclusion: { in: [false], if: :merge_request_event? }
+
+ validates :external_pull_request, presence: { if: :external_pull_request_event? }
+ validates :external_pull_request, absence: { unless: :external_pull_request_event? }
+ validates :tag, inclusion: { in: [false], if: :external_pull_request_event? }
+
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
@@ -683,6 +689,10 @@ module Ci
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
variables.concat(merge_request.predefined_variables)
end
+
+ if external_pull_request_event? && external_pull_request
+ variables.concat(external_pull_request.predefined_variables)
+ end
end
end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 571c4271475..0c2bd0aa8eb 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -23,7 +23,8 @@ module Ci
api: 5,
external: 6,
chat: 8,
- merge_request_event: 10
+ merge_request_event: 10,
+ external_pull_request_event: 11
}
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index ef1af1fc8bc..a976093ac0c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -10,7 +10,6 @@ module Clusters
self.table_name = 'clusters'
PROJECT_ONLY_APPLICATIONS = {
- Applications::Knative.application_name => Applications::Knative
}.freeze
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
@@ -18,7 +17,8 @@ module Clusters
Applications::CertManager.application_name => Applications::CertManager,
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
- Applications::Jupyter.application_name => Applications::Jupyter
+ Applications::Jupyter.application_name => Applications::Jupyter,
+ Applications::Knative.application_name => Applications::Knative
}.merge(PROJECT_ONLY_APPLICATIONS).freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
new file mode 100644
index 00000000000..65ae8d95500
--- /dev/null
+++ b/app/models/external_pull_request.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+# This model stores pull requests coming from external providers, such as
+# GitHub, when GitLab project is set as CI/CD only and remote mirror.
+#
+# When setting up a remote mirror with GitHub we subscribe to push and
+# pull_request webhook events. When a pull request is opened on GitHub,
+# a webhook is sent out, we create or update the status of the pull
+# request locally.
+#
+# When the mirror is updated and changes are pushed to branches we check
+# if there are open pull requests for the source and target branch.
+# If so, we create pipelines for external pull requests.
+class ExternalPullRequest < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+ include ShaAttribute
+
+ belongs_to :project
+
+ sha_attribute :source_sha
+ sha_attribute :target_sha
+
+ validates :source_branch, presence: true
+ validates :target_branch, presence: true
+ validates :source_sha, presence: true
+ validates :target_sha, presence: true
+ validates :source_repository, presence: true
+ validates :target_repository, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ open: 1,
+ closed: 2
+ }
+
+ # We currently don't support pull requests from fork, so
+ # we are going to return an error to the webhook
+ validate :not_from_fork
+
+ scope :by_source_branch, ->(branch) { where(source_branch: branch) }
+ scope :by_source_repository, -> (repository) { where(source_repository: repository) }
+
+ def self.create_or_update_from_params(params)
+ find_params = params.slice(:project_id, :source_branch, :target_branch)
+
+ safe_find_or_initialize_and_update(find: find_params, update: params) do |pull_request|
+ yield(pull_request) if block_given?
+ end
+ end
+
+ def actual_branch_head?
+ actual_source_branch_sha == source_sha
+ end
+
+ def from_fork?
+ source_repository != target_repository
+ end
+
+ def source_ref
+ Gitlab::Git::BRANCH_REF_PREFIX + source_branch
+ end
+
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME', value: target_branch)
+ end
+ end
+
+ private
+
+ def actual_source_branch_sha
+ project.commit(source_ref)&.sha
+ end
+
+ def not_from_fork
+ if from_fork?
+ errors.add(:base, 'Pull requests from fork are not supported')
+ end
+ end
+
+ def self.safe_find_or_initialize_and_update(find:, update:)
+ safe_ensure_unique(retries: 1) do
+ model = find_or_initialize_by(find)
+
+ if model.update(update)
+ yield(model) if block_given?
+ end
+
+ model
+ end
+ end
+end
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..5bd3a7f969a 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -78,7 +78,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/pages_domain.rb b/app/models/pages_domain.rb
index 12ce717efd7..a2a471074a9 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -17,7 +17,7 @@ class PagesDomain < ApplicationRecord
validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? }
validates :key, presence: { message: 'must be present if HTTPS-only is enabled' },
if: :certificate_should_be_present?
- validates :key, certificate_key: true, if: ->(domain) { domain.key.present? }
+ validates :key, certificate_key: true, named_ecdsa_key: true, if: ->(domain) { domain.key.present? }
validates :verification_code, presence: true, allow_blank: false
validate :validate_pages_domain
@@ -247,7 +247,7 @@ class PagesDomain < ApplicationRecord
def pkey
return unless key
- @pkey ||= OpenSSL::PKey::RSA.new(key)
+ @pkey ||= OpenSSL::PKey.read(key)
rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
nil
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 17b52d0578e..d948410e397 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -291,6 +291,8 @@ class Project < ApplicationRecord
has_many :remote_mirrors, inverse_of: :project
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
+ has_many :external_pull_requests, inverse_of: :project
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
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/user.rb b/app/models/user.rb
index 67d730e2fa3..5f109feb96a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -131,7 +131,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/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb
index bd7ff413afe..c8e6c973bf5 100644
--- a/app/policies/clusters/instance_policy.rb
+++ b/app/policies/clusters/instance_policy.rb
@@ -8,6 +8,7 @@ module Clusters
enable :create_cluster
enable :update_cluster
enable :admin_cluster
+ enable :read_prometheus
end
end
end
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/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/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb
index e22be6880bb..9504fdd8eac 100644
--- a/app/serializers/merge_request_noteable_entity.rb
+++ b/app/serializers/merge_request_noteable_entity.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class MergeRequestNoteableEntity < Grape::Entity
+class MergeRequestNoteableEntity < IssuableEntity
include RequestAwareEntity
# Currently this attr is exposed to be used in app/assets/javascripts/notes/stores/getters.js
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 8f8582afb43..4a7f62de9e1 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -18,7 +18,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
- def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block)
+ # rubocop: disable Metrics/ParameterLists
+ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
@@ -32,6 +33,7 @@ module Ci
trigger_request: trigger_request,
schedule: schedule,
merge_request: merge_request,
+ external_pull_request: external_pull_request,
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
@@ -62,6 +64,7 @@ module Ci
pipeline
end
+ # rubocop: enable Metrics/ParameterLists
def execute!(*args, &block)
execute(*args, &block).tap do |pipeline|
diff --git a/app/services/external_pull_requests/create_pipeline_service.rb b/app/services/external_pull_requests/create_pipeline_service.rb
new file mode 100644
index 00000000000..36411465ff1
--- /dev/null
+++ b/app/services/external_pull_requests/create_pipeline_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# This service is responsible for creating a pipeline for a given
+# ExternalPullRequest coming from other providers such as GitHub.
+
+module ExternalPullRequests
+ class CreatePipelineService < BaseService
+ def execute(pull_request)
+ return unless pull_request.open? && pull_request.actual_branch_head?
+
+ create_pipeline_for(pull_request)
+ end
+
+ private
+
+ def create_pipeline_for(pull_request)
+ Ci::CreatePipelineService.new(project, current_user, create_params(pull_request))
+ .execute(:external_pull_request_event, external_pull_request: pull_request)
+ end
+
+ def create_params(pull_request)
+ {
+ ref: pull_request.source_ref,
+ source_sha: pull_request.source_sha,
+ target_sha: pull_request.target_sha
+ }
+ end
+ end
+end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 47c308c8280..35a4d2015fa 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -57,7 +57,9 @@ module Git
Ci::CreatePipelineService
.new(project, current_user, pipeline_params)
- .execute(:push, pipeline_options)
+ .execute!(:push, pipeline_options)
+ rescue Ci::CreatePipelineService::CreateError => ex
+ log_pipeline_errors(ex)
end
def execute_project_hooks
@@ -125,5 +127,29 @@ module Git
project.mark_stuck_remote_mirrors_as_failed!
project.update_remote_mirrors
end
+
+ def log_pipeline_errors(exception)
+ data = {
+ class: self.class.name,
+ correlation_id: Labkit::Correlation::CorrelationId.current_id.to_s,
+ project_id: project.id,
+ project_path: project.full_path,
+ message: "Error creating pipeline",
+ errors: exception.to_s,
+ pipeline_params: pipeline_params
+ }
+
+ logger.warn(data)
+ end
+
+ def logger
+ if Sidekiq.server?
+ Sidekiq.logger
+ else
+ # This service runs in Sidekiq, so this shouldn't ever be
+ # called, but this is included just in case.
+ Gitlab::ProjectServiceLogger
+ end
+ end
end
end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
index 5b2bbffc066..b9d54d9636e 100644
--- a/app/validators/certificate_key_validator.rb
+++ b/app/validators/certificate_key_validator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# UrlValidator
+# CertificateKeyValidator
#
# Custom validator for private keys.
#
@@ -20,7 +20,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator
def valid_private_key_pem?(value)
return false unless value
- pkey = OpenSSL::PKey::RSA.new(value)
+ pkey = OpenSSL::PKey.read(value)
pkey.private?
rescue OpenSSL::PKey::PKeyError
false
diff --git a/app/validators/named_ecdsa_key_validator.rb b/app/validators/named_ecdsa_key_validator.rb
new file mode 100644
index 00000000000..42ee02b6ad4
--- /dev/null
+++ b/app/validators/named_ecdsa_key_validator.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# NamedEcdsaKeyValidator
+#
+# Custom validator for ecdsa private keys.
+# Golang currently doesn't support explicit curves for ECDSA certificates
+# This validator checks if curve is set by name, not by parameters
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, named_ecdsa_key: true
+# end
+#
+class NamedEcdsaKeyValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if explicit_ec?(value)
+ record.errors.add(attribute, "ECDSA keys with explicit curves are not supported")
+ end
+ end
+
+ private
+
+ UNNAMED_CURVE = "UNDEF"
+
+ def explicit_ec?(value)
+ return false unless value
+
+ pkey = OpenSSL::PKey.read(value)
+ return false unless pkey.is_a?(OpenSSL::PKey::EC)
+
+ pkey.group.curve_name == UNNAMED_CURVE
+ rescue OpenSSL::PKey::PKeyError
+ false
+ end
+end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 8aca61efe7b..8fad42436ca 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -35,41 +35,7 @@
= link_to 'New group', new_admin_group_path, class: "btn btn-success"
.row
.col-md-4
- .info-well
- .well-segment.admin-well.admin-well-statistics
- %h4 Statistics
- %p
- Forks
- %span.light.float-right
- = approximate_fork_count_with_delimiters(@counts)
- %p
- Issues
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Issue)
- %p
- Merge Requests
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, MergeRequest)
- %p
- Notes
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Note)
- %p
- Snippets
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Snippet)
- %p
- SSH Keys
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Key)
- %p
- Milestones
- %span.light.float-right
- = approximate_count_with_delimiters(@counts, Milestone)
- %p
- Active Users
- %span.light.float-right
- = number_with_delimiter(User.active.count)
+ #js-admin-statistics-container
.col-md-4
.info-well
.well-segment.admin-well.admin-well-features
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/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index a996c86a256..f1ba804f920 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,5 +1,5 @@
#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog.modal-lg
+ .modal-dialog.modal-lg.modal-1040
.modal-content
.modal-header
%h4.modal-title
@@ -11,104 +11,100 @@
.modal-body
.row
.col-lg-4
- %table.shortcut-mappings
+ %table.shortcut-mappings.text-2
%tbody
%tr
%th
%th= _('Global Shortcuts')
%tr
%td.shortcut
- %kbd s
- %td= _('Focus Search')
+ %kbd ?
+ %td= _('Toggle this dialog')
%tr
%td.shortcut
- %kbd f
- %td= _('Focus Filter')
+ %kbd shift p
+ %td= _('Go to your projects')
%tr
%td.shortcut
- %kbd p
- %kbd b
- %td= _('Toggle the Performance Bar')
+ %kbd shift g
+ %td= _('Go to your groups')
%tr
%td.shortcut
- %kbd ?
- %td= _('Show/hide this dialog')
+ %kbd shift a
+ %td= _('Go to the activity feed')
%tr
%td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; shift p
- - else
- %kbd ctrl shift p
- %td= _('Toggle Markdown preview')
+ %kbd shift l
+ %td= _('Go to the milestone list')
%tr
%td.shortcut
- %kbd
- %i.fa.fa-arrow-up
- %td= _('Edit last comment (when focused on an empty textarea)')
+ %kbd shift s
+ %td= _('Go to your snippets')
%tr
%td.shortcut
- %kbd shift t
- %td
- = _('Go to todos')
+ %kbd s
+ %td= _('Start search')
%tr
%td.shortcut
- %kbd shift a
- %td
- = _('Go to the activity feed')
+ %kbd shift i
+ %td= _('Go to your issues')
%tr
%td.shortcut
- %kbd shift p
- %td
- = _('Go to projects')
+ %kbd shift m
+ %td= _('Go to your merge requests')
%tr
%td.shortcut
- %kbd shift i
- %td
- = _('Go to issues')
+ %kbd shift t
+ %td= _('Go to your To-Do list')
%tr
%td.shortcut
- %kbd shift m
- %td
- = _('Go to merge requests')
+ %kbd p
+ %kbd b
+ %td= _('Toggle the Performance Bar')
+ %tbody
%tr
- %td.shortcut
- %kbd shift g
- %td
- = _('Go to groups')
+ %th
+ %th= _('Web IDE')
%tr
%td.shortcut
- %kbd shift l
- %td
- = _('Go to milestones')
+ - if browser.platform.mac?
+ %kbd &#8984; p
+ - else
+ %kbd ctrl p
+ %td= _('Go to file')
%tr
%td.shortcut
- %kbd shift s
- %td
- = _('Go to snippets')
+ - if browser.platform.mac?
+ %kbd &#8984; enter
+ - else
+ %kbd ctrl enter
+ %td= _('Commit (when editing commit message)')
%tbody
%tr
%th
- %th= _('Finding Project File')
+ %th= _('Wiki pages')
%tr
%td.shortcut
- %kbd
- %i.fa.fa-arrow-up
- %td= _('Move selection up')
+ %kbd e
+ %td= _('Edit wiki page')
+ %tbody
%tr
- %td.shortcut
- %kbd
- %i.fa.fa-arrow-down
- %td= _('Move selection down')
+ %th
+ %th= _('Editing')
%tr
%td.shortcut
- %kbd enter
- %td= _('Open Selection')
+ - if browser.platform.mac?
+ %kbd &#8984; shift p
+ - else
+ %kbd ctrl shift p
+ %td= _('Toggle Markdown preview')
%tr
%td.shortcut
- %kbd esc
- %td= _('Go back')
+ %kbd
+ %i.fa.fa-arrow-up
+ %td= _('Edit your most recent comment in a thread (from an empty textarea)')
.col-lg-4
- %table.shortcut-mappings
+ %table.shortcut-mappings.text-2
%tbody
%tr
%th
@@ -117,105 +113,94 @@
%td.shortcut
%kbd g
%kbd p
- %td
- = _('Go to the project\'s overview page')
+ %td= _('Go to the project\'s overview page')
%tr
%td.shortcut
%kbd g
%kbd v
- %td
- = _('Go to the project\'s activity feed')
+ %td= _('Go to the project\'s activity feed')
%tr
%td.shortcut
%kbd g
- %kbd f
- %td
- = _('Go to files')
+ %kbd r
+ %td= _('Go to releases')
%tr
%td.shortcut
%kbd g
- %kbd c
- %td
- = _('Go to commits')
+ %kbd f
+ %td= _('Go to files')
+ %tr
+ %td.shortcut
+ %kbd t
+ %td= _('Go to find file')
%tr
%td.shortcut
%kbd g
- %kbd j
- %td
- = _('Go to jobs')
+ %kbd c
+ %td= _('Go to commits')
%tr
%td.shortcut
%kbd g
%kbd n
- %td
- = _('Go to network graph')
+ %td= _('Go to repository graph')
%tr
%td.shortcut
%kbd g
%kbd d
- %td
- = _('Go to repository charts')
+ %td= _('Go to repository charts')
%tr
%td.shortcut
%kbd g
%kbd i
- %td
- = _('Go to issues')
+ %td= _('Go to issues')
+ %tr
+ %td.shortcut
+ %kbd i
+ %td= _('New issue')
%tr
%td.shortcut
%kbd g
%kbd b
- %td
- = _('Go to issue boards')
+ %td= _('Go to issue boards')
%tr
%td.shortcut
%kbd g
%kbd m
- %td
- = _('Go to merge requests')
+ %td= _('Go to merge requests')
%tr
%td.shortcut
%kbd g
- %kbd e
- %td
- = _('Go to environments')
+ %kbd j
+ %td= _('Go to jobs')
%tr
%td.shortcut
%kbd g
%kbd l
- %td
- = _('Go to metrics')
+ %td= _('Go to metrics')
+ %tr
+ %td.shortcut
+ %kbd g
+ %kbd e
+ %td= _('Go to environments')
%tr
%td.shortcut
%kbd g
%kbd k
- %td
- = _('Go to kubernetes')
+ %td= _('Go to kubernetes')
%tr
%td.shortcut
%kbd g
%kbd s
- %td
- = _('Go to snippets')
+ %td= _('Go to snippets')
%tr
%td.shortcut
%kbd g
%kbd w
- %td
- = _('Go to wiki')
- %tr
- %td.shortcut
- %kbd t
- %td= _('Go to finding file')
- %tr
- %td.shortcut
- %kbd i
- %td= _('New issue')
-
+ %td= _('Go to wiki')
%tbody
%tr
%th
- %th= _('Project Files browsing')
+ %th= _('Project Files')
%tr
%td.shortcut
%kbd
@@ -230,38 +215,87 @@
%td.shortcut
%kbd enter
%td= _('Open Selection')
- %tbody
%tr
- %th
- %th= _('Project File')
+ %td.shortcut
+ %kbd esc
+ %td= _('Go back (while searching for files')
%tr
%td.shortcut
%kbd y
- %td= _('Go to file permalink')
+ %td= _('Go to file permalink (while viewing a file)')
+ .col-lg-4
+ %table.shortcut-mappings.text-2
%tbody
%tr
%th
- %th= _('Web IDE')
+ %th= _('Issues / Merge Requests')
+ %tr
+ %td.shortcut
+ %kbd a
+ %td= _('Change assignee')
+ %tr
+ %td.shortcut
+ %kbd m
+ %td= _('Change milestone')
+ %tr
+ %td.shortcut
+ %kbd r
+ %td= _('Comment/Reply (quoting selected text)')
+ %tr
+ %td.shortcut
+ %kbd e
+ %td= _('Edit description')
+ %tr
+ %td.shortcut
+ %kbd l
+ %td= _('Change label')
+ %tr
+ %td.shortcut
+ %kbd ]
+ \/
+ %kbd j
+ %td= _('Next file in diff (MRs only)')
+ %tr
+ %td.shortcut
+ %kbd [
+ \/
+ %kbd k
+ %td= _('Previous file in diff (MRs only)')
%tr
%td.shortcut
- if browser.platform.mac?
%kbd &#8984; p
- else
%kbd ctrl p
- %td= _('Go to file')
+ %td= _('Go to file (MRs only)')
%tr
%td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; enter
- - else
- %kbd ctrl enter
- %td= _('Commit (when editing commit message)')
- .col-lg-4
- %table.shortcut-mappings
- %tbody.hidden-shortcut.network{ style: 'display:none' }
+ %kbd n
+ %td= _('Next unresolved discussion (MRs only)')
+ %tr
+ %td.shortcut
+ %kbd p
+ %td= _('Previous unresolved discussion (MRs only)')
+ %tbody
%tr
%th
- %th= _('Network Graph')
+ %th= _('Epics (Ultimate / Gold license only)')
+ %tr
+ %td.shortcut
+ %kbd r
+ %td= _('Comment/Reply (quoting selected text)')
+ %tr
+ %td.shortcut
+ %kbd e
+ %td= _('Edit epic description')
+ %tr
+ %td.shortcut
+ %kbd l
+ %td= _('Change label')
+ %tbody
+ %tr
+ %th
+ %th= _('Repository Graph')
%tr
%td.shortcut
%kbd
@@ -295,92 +329,12 @@
%kbd
shift
%i.fa.fa-arrow-up
- \/
- %kbd
- shift k
+ \/ k
%td= _('Scroll to top')
%tr
%td.shortcut
%kbd
shift
%i.fa.fa-arrow-down
- \/
- %kbd
- shift j
+ \/ j
%td= _('Scroll to bottom')
- %tbody.hidden-shortcut.issues{ style: 'display:none' }
- %tr
- %th
- %th= _('Issues')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
- %tr
- %td.shortcut
- %kbd r
- %td= _('Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit issue')
- %tr
- %td.shortcut
- %kbd l
- %td= _('Change Label')
- %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
- %tr
- %th
- %th= _('Merge Requests')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
- %tr
- %td.shortcut
- %kbd r
- %td= _('Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit merge request')
- %tr
- %td.shortcut
- %kbd l
- %td= _('Change Label')
- %tr
- %td.shortcut
- %kbd ]
- \/
- %kbd j
- %td= _('Move to next file')
- %tr
- %td.shortcut
- %kbd [
- \/
- %kbd k
- %td= _('Move to previous file')
- %tr
- %td.shortcut
- %kbd n
- %td= _('Move to next unresolved discussion')
- %tr
- %td.shortcut
- %kbd p
- %td= _('Move to previous unresolved discussion')
- %tbody.hidden-shortcut.wiki{ style: 'display:none' }
- %tr
- %th
- %th= _('Wiki pages')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit wiki page')
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index c1f4b3adfec..7cc7d1783c4 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -48,14 +48,14 @@
- if group_sidebar_link?(:issues)
= nav_link(path: group_issues_sub_menu_items) do
- = link_to issues_group_path(@group) do
+ = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' } do
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
= _('Issues')
%span.badge.badge-pill.count= number_with_delimiter(issues_count)
- %ul.sidebar-sub-level-items
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
@@ -70,7 +70,7 @@
- if group_sidebar_link?(:boards)
= nav_link(path: ['boards#index', 'boards#show']) do
- = link_to group_boards_path(@group), title: boards_link_text do
+ = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
%span
= boards_link_text
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/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 15499c89ffb..928b54ea28f 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -13,12 +13,13 @@
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
- git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch}
- git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
+ -# All repo/branch refs have been quoted to allow support for special characters (such as #my-branch)
+ git fetch "#{h default_url_to_repo(@merge_request.source_project)}" "#{h @merge_request.source_branch}"
+ git checkout -b "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}" FETCH_HEAD
- else
:preserve
git fetch origin
- git checkout -b #{h @merge_request.source_branch} origin/#{h @merge_request.source_branch}
+ git checkout -b "#{h @merge_request.source_branch}" "origin/#{h @merge_request.source_branch}"
%p
%strong Step 2.
Review the changes locally
@@ -31,20 +32,20 @@
- if @merge_request.for_fork?
:preserve
git fetch origin
- git checkout origin/#{h @merge_request.target_branch}
- git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch}
+ git checkout "origin/#{h @merge_request.target_branch}"
+ git merge --no-ff "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}"
- else
:preserve
git fetch origin
- git checkout origin/#{h @merge_request.target_branch}
- git merge --no-ff #{h @merge_request.source_branch}
+ git checkout "origin/#{h @merge_request.target_branch}"
+ git merge --no-ff "#{h @merge_request.source_branch}"
%p
%strong Step 4.
Push the result of the merge to GitLab
= clipboard_button(target: "pre#merge-info-4", title: "Copy commands to clipboard")
%pre.dark#merge-info-4
:preserve
- git push origin #{h @merge_request.target_branch}
+ git push origin "#{h @merge_request.target_branch}"
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
index feca109dade..5f3d49adff7 100644
--- a/app/views/shared/members/_sort_dropdown.html.haml
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -1,4 +1,4 @@
-= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold pr-2'
+= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold px-2'
.dropdown.inline.qa-user-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
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/workers/all_queues.yml b/app/workers/all_queues.yml
index 991a177018e..a33afd436b0 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -160,6 +160,7 @@
- repository_import
- repository_remove_remote
- system_hook_push
+- update_external_pull_requests
- update_merge_requests
- update_project_statistics
- upload_checksum
diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb
new file mode 100644
index 00000000000..c5acfa82ada
--- /dev/null
+++ b/app/workers/update_external_pull_requests_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class UpdateExternalPullRequestsWorker
+ include ApplicationWorker
+
+ def perform(project_id, user_id, ref)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ user = User.find_by_id(user_id)
+ return unless user
+
+ branch = Gitlab::Git.branch_name(ref)
+ return unless branch
+
+ external_pull_requests = project.external_pull_requests
+ .by_source_repository(project.import_source)
+ .by_source_branch(branch)
+
+ external_pull_requests.find_each do |pull_request|
+ ExternalPullRequests::CreatePipelineService.new(project, user)
+ .execute(pull_request)
+ end
+ end
+end
diff --git a/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml b/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml
new file mode 100644
index 00000000000..9f34a912dc5
--- /dev/null
+++ b/changelogs/unreleased/31735-only-show-copy_metadata-when-usable.yml
@@ -0,0 +1,5 @@
+---
+title: Only show /copy_metadata quick action when usable
+merge_request: 31735
+author: Lee Tickett
+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/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml b/changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml
new file mode 100644
index 00000000000..9862137c80c
--- /dev/null
+++ b/changelogs/unreleased/51123-error-500-viewing-admin-page-due-to-statement-timeout-on-counting-t.yml
@@ -0,0 +1,5 @@
+---
+title: 'Admin dashboard: Fetch and render statistics async'
+merge_request: 32449
+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/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml b/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml
new file mode 100644
index 00000000000..54bec43815c
--- /dev/null
+++ b/changelogs/unreleased/66616-follow-up-documentation-for-merge-trains-cancel-when-running.yml
@@ -0,0 +1,5 @@
+---
+title: Update merge train documentation
+merge_request: 32218
+author:
+type: changed
diff --git a/changelogs/unreleased/ac-accelerate-wiki-attachments.yml b/changelogs/unreleased/ac-accelerate-wiki-attachments.yml
new file mode 100644
index 00000000000..347a570488e
--- /dev/null
+++ b/changelogs/unreleased/ac-accelerate-wiki-attachments.yml
@@ -0,0 +1,5 @@
+---
+title: Preprocess wiki attachments with GitLab-Workhorse
+merge_request: 32663
+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/ecdsa_pages_certificates.yml b/changelogs/unreleased/ecdsa_pages_certificates.yml
new file mode 100644
index 00000000000..059cb434b62
--- /dev/null
+++ b/changelogs/unreleased/ecdsa_pages_certificates.yml
@@ -0,0 +1,5 @@
+---
+title: Allow ECDSA certificates for pages domains
+merge_request: 32393
+author:
+type: added
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/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/id-autosave-for-new-mr.yml b/changelogs/unreleased/id-autosave-for-new-mr.yml
new file mode 100644
index 00000000000..8f269094715
--- /dev/null
+++ b/changelogs/unreleased/id-autosave-for-new-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix sharing localStorage with all MRs
+merge_request: 32699
+author:
+type: fixed
diff --git a/changelogs/unreleased/instance-group-level-knative.yml b/changelogs/unreleased/instance-group-level-knative.yml
new file mode 100644
index 00000000000..5108334a3ea
--- /dev/null
+++ b/changelogs/unreleased/instance-group-level-knative.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Knative to be installed on group and instance level clusters
+merge_request: 32128
+author:
+type: added
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/keyboard-shortcuts-2.yml b/changelogs/unreleased/keyboard-shortcuts-2.yml
new file mode 100644
index 00000000000..a6a2266b20a
--- /dev/null
+++ b/changelogs/unreleased/keyboard-shortcuts-2.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up keyboard shortcuts help modal, removing and adding as needed
+merge_request: 31642
+author:
+type: other
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/quote-branch-names-in-instructions.yml b/changelogs/unreleased/quote-branch-names-in-instructions.yml
new file mode 100644
index 00000000000..fe5964c8918
--- /dev/null
+++ b/changelogs/unreleased/quote-branch-names-in-instructions.yml
@@ -0,0 +1,5 @@
+---
+title: Quote branch names in how to merge instructions
+merge_request: 32639
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/sh-add-margin-member-list.yml b/changelogs/unreleased/sh-add-margin-member-list.yml
new file mode 100644
index 00000000000..35d35aad8bf
--- /dev/null
+++ b/changelogs/unreleased/sh-add-margin-member-list.yml
@@ -0,0 +1,5 @@
+---
+title: Add padding to left of "Sort by" in members dropdown
+merge_request: 32602
+author:
+type: other
diff --git a/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml b/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml
new file mode 100644
index 00000000000..b334355cab6
--- /dev/null
+++ b/changelogs/unreleased/sh-add-sidekiq-logging-for-bad-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Log errors for failed pipeline creation in PostReceive
+merge_request: 32633
+author:
+type: other
diff --git a/changelogs/unreleased/update-rouge.yml b/changelogs/unreleased/update-rouge.yml
new file mode 100644
index 00000000000..6f44de02d76
--- /dev/null
+++ b/changelogs/unreleased/update-rouge.yml
@@ -0,0 +1,5 @@
+---
+title: Update rouge to v3.10.0
+merge_request: 32745
+author:
+type: other
diff --git a/changelogs/unreleased/use_default_external_auth_label_empty.yml b/changelogs/unreleased/use_default_external_auth_label_empty.yml
new file mode 100644
index 00000000000..9c1039b0875
--- /dev/null
+++ b/changelogs/unreleased/use_default_external_auth_label_empty.yml
@@ -0,0 +1,6 @@
+---
+title: Prevent empty external authorization classification labels from overriding
+ the default label
+merge_request: 32517
+author: Will Chandler
+type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index e3693f612e3..aa7c2d343a8 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
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/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7edec576f9a..e89e9657314 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -115,3 +115,4 @@
- [export_csv, 1]
- [incident_management, 2]
- [jira_connect, 1]
+ - [update_external_pull_requests, 3]
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/20190829131130_create_external_pull_requests.rb b/db/migrate/20190829131130_create_external_pull_requests.rb
new file mode 100644
index 00000000000..0c3168807ec
--- /dev/null
+++ b/db/migrate/20190829131130_create_external_pull_requests.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CreateExternalPullRequests < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX = 'index_external_pull_requests_on_project_and_branches'
+
+ def change
+ create_table :external_pull_requests do |t|
+ t.timestamps_with_timezone null: false
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }, index: false
+ t.integer :pull_request_iid, null: false
+ t.integer :status, null: false, limit: 2
+ t.string :source_branch, null: false, limit: 255
+ t.string :target_branch, null: false, limit: 255
+ t.string :source_repository, null: false, limit: 255
+ t.string :target_repository, null: false, limit: 255
+ t.binary :source_sha, null: false
+ t.binary :target_sha, null: false
+
+ t.index [:project_id, :source_branch, :target_branch], unique: true, name: INDEX
+ end
+ end
+end
diff --git a/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb b/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb
new file mode 100644
index 00000000000..5abf56742b1
--- /dev/null
+++ b/db/migrate/20190830075508_add_external_pull_request_id_to_ci_pipelines.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddExternalPullRequestIdToCiPipelines < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :ci_pipelines, :external_pull_request_id, :bigint
+ end
+
+ def down
+ remove_column :ci_pipelines, :external_pull_request_id
+ end
+end
diff --git a/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb b/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb
new file mode 100644
index 00000000000..d2f5ad7a420
--- /dev/null
+++ b/db/migrate/20190830080123_add_index_to_ci_pipelines_external_pull_request.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_pipelines, :external_pull_request_id, where: 'external_pull_request_id IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :ci_pipelines, :external_pull_request_id
+ end
+end
diff --git a/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb b/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb
new file mode 100644
index 00000000000..b38fda83047
--- /dev/null
+++ b/db/migrate/20190830080626_add_foreign_key_to_ci_pipelines_external_pull_request.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddForeignKeyToCiPipelinesExternalPullRequest < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :ci_pipelines, :external_pull_requests, column: :external_pull_request_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :ci_pipelines, :external_pull_requests
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 61f7787f192..6ddfb8bcb39 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -754,7 +754,9 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.integer "merge_request_id"
t.binary "source_sha"
t.binary "target_sha"
+ t.bigint "external_pull_request_id"
t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id"
+ t.index ["external_pull_request_id"], name: "index_ci_pipelines_on_external_pull_request_id", where: "(external_pull_request_id IS NOT NULL)"
t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", where: "(merge_request_id IS NOT NULL)"
t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id"
t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)"
@@ -1323,6 +1325,21 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
end
+ create_table "external_pull_requests", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "project_id", null: false
+ t.integer "pull_request_iid", null: false
+ t.integer "status", limit: 2, null: false
+ t.string "source_branch", limit: 255, null: false
+ t.string "target_branch", limit: 255, null: false
+ t.string "source_repository", limit: 255, null: false
+ t.string "target_repository", limit: 255, null: false
+ t.binary "source_sha", null: false
+ t.binary "target_sha", null: false
+ t.index ["project_id", "source_branch", "target_branch"], name: "index_external_pull_requests_on_project_and_branches", unique: true
+ end
+
create_table "feature_gates", id: :serial, force: :cascade do |t|
t.string "feature_key", null: false
t.string "key", null: false
@@ -3785,6 +3802,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
+ add_foreign_key "ci_pipelines", "external_pull_requests", name: "fk_190998ef09", on_delete: :nullify
add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
@@ -3849,6 +3867,7 @@ ActiveRecord::Schema.define(version: 2019_09_05_223900) do
add_foreign_key "events", "namespaces", column: "group_id", 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
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
add_foreign_key "fork_network_members", "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/integration/plantuml.md b/doc/administration/integration/plantuml.md
index df6c554decb..318711fd281 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -54,6 +54,21 @@ http://localhost:8080/plantuml
you can change these defaults by editing the `/etc/tomcat7/server.xml` file.
+### Making local PlantUML accessible using custom GitLab setup
+
+The PlantUML server runs locally on your server, so it is not accessible
+externally. As such, it is necessary to catch external PlantUML calls and
+redirect them to the local server.
+
+The idea is to redirect each call to `https://gitlab.example.com/-/plantuml/`
+to the local PlantUML server `http://localhost:8080/plantuml`.
+
+To enable the redirection, add the following line in `/etc/gitlab/gitlab.rb`:
+
+```ruby
+nginx['custom_gitlab_server_config'] = "location /-/plantuml { \n proxy_cache off; \n proxy_pass http://127.0.0.1:8080; \n}\n"
+```
+
## GitLab
You need to enable PlantUML integration from Settings under Admin Area. To do
@@ -62,7 +77,7 @@ that, login with an Admin account and do following:
- In GitLab, go to **Admin Area > Settings > Integrations**.
- Expand the **PlantUML** section.
- Check **Enable PlantUML** checkbox.
-- Set the PlantUML instance as **PlantUML URL**.
+- Set the PlantUML instance as `https://gitlab.example.com/-/plantuml/`.
## Creating Diagrams
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 6dbfd5404d0..5c348702ba2 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -7,9 +7,9 @@ installations from source you'll have to configure it yourself.
To enable the GitLab Prometheus metrics:
1. Log into GitLab as an administrator, and go to the Admin area.
-1. Click on the gear, then click on Settings.
-1. Find the `Metrics - Prometheus` section, and click `Enable Prometheus Metrics`
-1. [Restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart) for the changes to take effect
+1. Navigate to GitLab's **Settings > Metrics and profiling**.
+1. Find the **Metrics - Prometheus** section, and click **Enable Prometheus Metrics**.
+1. [Restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart) for the changes to take effect.
## Collecting the metrics
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 9af5430f1c8..e2ddc2cbc18 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -126,6 +126,7 @@ The following API resources are available outside of project and group contexts
| [Runners](runners.md) | `/runners` (also available for projects) |
| [Search](search.md) | `/search` (also available for groups and projects) |
| [Settings](settings.md) | `/application/settings` |
+| [Statistics](statistics.md) | `/application/statistics` |
| [Sidekiq metrics](sidekiq_metrics.md) | `/sidekiq` |
| [Suggestions](suggestions.md) | `/suggestions` |
| [System hooks](system_hooks.md) | `/hooks` |
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index b73fe38f53e..d19f11ba1d4 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -525,6 +525,77 @@ PUT /projects/:id/merge_requests/:merge_request_iid/approvers
}
```
+### Get the approval state of merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/13712) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.3.
+
+You can request information about a merge request's approval state by using the following endpoint:
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/approval_state
+```
+
+The `approval_rules_overwritten` will be `true` if the merge request level rules
+are created for the merge request. If there's none, it'll be `false`.
+
+This includes additional information about the users who have already approved
+(`approved_by`) and whether a rule is already approved (`approved`).
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
+
+```json
+{
+ "approval_rules_overwritten": true,
+ "rules": [
+ {
+ "id": 1,
+ "name": "Ruby",
+ "rule_type": "regular",
+ "eligible_approvers": [
+ {
+ "id": 4,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "approvals_required": 2,
+ "users": [
+ {
+ "id": 4,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "groups": [],
+ "contains_hidden_groups": false,
+ "approved_by": [
+ {
+ "id": 4,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "source_rule": null,
+ "approved": true
+ }
+ ]
+}
+```
+
### Get merge request level rules
>**Note:** This API endpoint is only available on 12.3 Starter and above.
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/statistics.md b/doc/api/statistics.md
new file mode 100644
index 00000000000..5078b2f26d4
--- /dev/null
+++ b/doc/api/statistics.md
@@ -0,0 +1,35 @@
+# Application statistics API
+
+## Get current application statistics
+
+List the current statistics of the GitLab instance. You have to be an
+administrator in order to perform this action.
+
+NOTE: **Note:**
+These statistics are approximate.
+
+```
+GET /application/statistics
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/statistics
+```
+
+Example response:
+
+```json
+{
+ "forks": "10",
+ "issues": "76",
+ "merge_requests": "27",
+ "notes": "954",
+ "snippets": "50",
+ "ssh_keys": "10",
+ "milestones": "40",
+ "users": "50",
+ "groups": "10",
+ "projects": "20",
+ "active_users": "50"
+}
+```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 5f875528a6c..b6059c71b27 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -23,8 +23,8 @@ Below are the changes made between V3 and V4.
- Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
- - `/licences`
- - `/licences/:key`
+ - `/licenses`
+ - `/licenses/:key`
- `/gitignores`
- `/gitlab_ci_ymls`
- `/dockerfiles`
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index a48da557e09..fc0125fcc18 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -721,6 +721,13 @@ deploy:
- master
```
+NOTE: **Note:**
+This example explicitly calls `docker pull`. If you prefer to implicitly pull the
+built image using `image:`, and use either the [Docker](https://docs.gitlab.com/runner/executors/docker.html)
+or [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html) executor,
+make sure that [`pull_policy`](https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work)
+is set to `always`.
+
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
[2fa]: ../../user/profile/account/two_factor_authentication.md
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
index 126e12e460f..d68fba82f4b 100644
--- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
@@ -61,6 +61,20 @@ CAUTION: **Warning:**
Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](../index.md#configuring-pipelines-for-merge-requests),
otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state.
+## Automatic pipeline cancelation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12996) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+
+GitLab CI can detect the presence of redundant pipelines,
+and will cancel them automatically in order to conserve CI resources.
+
+When a user merges a merge request immediately within an ongoing merge
+train, the train will be reconstructed, as it will recreate the expected
+post-merge commit and pipeline. In this case, the merge train may already
+have pipelines running against the previous expected post-merge commit.
+These pipelines are considered redundant and will be automatically
+canceled.
+
## Troubleshooting
### Pipelines for merged results not created even with new change pushed to merge request
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/background_migrations.md b/doc/development/background_migrations.md
index a456bbc781f..606ee431c3e 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -302,18 +302,18 @@ for more details.
## Best practices
-1. Make sure to know how much data you're dealing with
+1. Make sure to know how much data you're dealing with.
1. Make sure that background migration jobs are idempotent.
1. Make sure that tests you write are not false positives.
1. Make sure that if the data being migrated is critical and cannot be lost, the
clean-up migration also checks the final state of the data before completing.
-1. Make sure to know how much time it'll take to run all scheduled migrations
+1. Make sure to know how much time it'll take to run all scheduled migrations.
1. When migrating many columns, make sure it won't generate too many
dead tuples in the process (you may need to directly query the number of dead tuples
- and adjust the scheduling according to this piece of data)
+ and adjust the scheduling according to this piece of data).
1. Make sure to discuss the numbers with a database specialist, the migration may add
more pressure on DB than you expect (measure on staging,
- or ask someone to measure on production)
+ or ask someone to measure on production).
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
diff --git a/doc/development/dangerbot.md b/doc/development/dangerbot.md
index 5fc5886e3a2..6bf59209d21 100644
--- a/doc/development/dangerbot.md
+++ b/doc/development/dangerbot.md
@@ -77,7 +77,7 @@ complex logic related to that task.
Danger code is just Ruby code. It should adhere to our coding standards, and
needs tests, like any other piece of Ruby in our codebase. However, we aren't
-able to test a `Dangerfile` directly! So, to maximise test coverage, try to
+able to test a `Dangerfile` directly! So, to maximize test coverage, try to
minimize the number of lines of code in `danger/`. A non-trivial `Dangerfile`
should mostly call plugin code with arguments derived from the methods provided
by Danger. The plugin code itself should have unit tests.
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 09dd31e2aee..4a50e90a26c 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
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index ae0e2361840..9224a2548ab 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -58,6 +58,24 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations.
```
+### Merge Request Review
+
+With the purpose of being [respectful of others' time](https://about.gitlab.com/handbook/values/#be-respectful-of-others-time) please follow these guidelines when asking for a review:
+
+- Make sure your Merge Request:
+ - milestone is set
+ - at least the labels suggested by danger-bot are set
+ - has a clear description
+ - includes before/after screenshots if there is a UI change
+ - pipeline is green
+ - includes tests
+ - includes a changelog entry (when necessary)
+- Before assigning to a maintainer, assign to a reviewer.
+- If you assigned a merge request, or pinged someone directly, keep in mind that we work in different timezones and asynchronously, so be patient. Unless the merge request is urgent (like fixing a broken master), please don't DM or reassign the merge request before waiting for a 24-hour window.
+- If you have a question regarding your merge request/issue, make it on the merge request/issue. When we DM each other, we no longer have a SSOT and [no one else is able to contribute](https://about.gitlab.com/handbook/values/#public-by-default).
+- When you have a big WIP merge request with many changes, you're adivsed to get the review started before adding/removing significant code. Make sure it is assigned well before the release cut-off, as the reviewer(s)/maintainer(s) would always prioritize reviewing finished MRs before WIP ones.
+- Make sure to remove the WIP title before the last round of review.
+
### Share your work early
1. Before writing code, ensure your vision of the architecture is aligned with
diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md
index 850ea6b60ac..8820b54fa87 100644
--- a/doc/development/testing_guide/end_to_end/page_objects.md
+++ b/doc/development/testing_guide/end_to_end/page_objects.md
@@ -167,6 +167,18 @@ There are two supported methods of defining elements within a view.
Any existing `.qa-selector` class should be considered deprecated
and we should prefer the `data-qa-selector` method of definition.
+### Exceptions
+
+In some cases it might not be possible or worthwhile to add a selector.
+
+Some UI components use external libraries, including some maintained by third parties.
+Even if a library is maintained by GitLab, the selector sanity test only runs
+on code within the GitLab project, so it's not possible to specify the path for
+the view for code in a library.
+
+In such rare cases it's reasonable to use CSS selectors in page object methods,
+with a comment explaining why an `element` can't be added.
+
## Running the test locally
During development, you can run the `qa:selectors` test by running
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index ef319f7f0ce..f1456146032 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -298,3 +298,33 @@ gitlab_rails['omniauth_allow_bypass_two_factor'] = ['twitter', 'google_oauth2']
omniauth:
allow_bypass_two_factor: ['twitter', 'google_oauth2']
```
+
+## Automatically sign in with provider
+
+You can add the `auto_sign_in_with_provider` setting to your
+GitLab configuration to automatically redirect login requests
+to your OmniAuth provider for authentication, thus removing the need to click a button
+before actually signing in.
+
+For example, when using the Azure integration, you would set the following
+to enable auto sign in.
+
+For Omnibus package:
+
+```ruby
+gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'azure_oauth2'
+```
+
+For installations from source:
+
+```yaml
+omniauth:
+ auto_sign_in_with_provider: azure_oauth2
+```
+
+Please keep in mind that every sign in attempt will be redirected to the OmniAuth provider,
+so you will not be able to sign in using local credentials. Make sure that at least one
+of the OmniAuth users has admin permissions.
+
+You may also bypass the auto signin feature by browsing to
+`https://gitlab.example.com/users/sign_in?auto_sign_in=false`.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 59b775d13bd..06048ad618f 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -190,7 +190,7 @@ it` may occur, and will cause the backup process to fail. To combat this, 8.17
introduces a new backup strategy called `copy`. The strategy copies data files
to a temporary location before calling `tar` and `gzip`, avoiding the error.
-A side-effect is that the backup process with take up to an additional 1X disk
+A side-effect is that the backup process will take up to an additional 1X disk
space. The process does its best to clean up the temporary files at each stage
so the problem doesn't compound, but it could be a considerable change for large
installations. This is why the `copy` strategy is not the default in 8.17.
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/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md
index 9fe4b50a991..5117b5f476f 100644
--- a/doc/user/admin_area/diff_limits.md
+++ b/doc/user/admin_area/diff_limits.md
@@ -6,6 +6,8 @@ type: reference
You can set a maximum size for display of diff files (patches).
+For details about diff files, [View changes between files](../project/merge_requests/index.md#view-changes-between-file-versions).
+
## Maximum diff patch size
Diff files which exceed this value will be presented as 'too large' and won't
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index fa2df667031..89526c08e7e 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -46,6 +46,10 @@ To run a Dependency Scanning job, you need GitLab Runner with the
executor running in privileged mode. If you're using the shared Runners on GitLab.com,
this is enabled by default.
+CAUTION: **Caution:**
+If you use your own Runners, make sure that the Docker version you have installed
+is **not** `19.03.00`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
+
## Supported languages and package managers
The following languages and dependency managers are supported.
@@ -343,14 +347,11 @@ You can search the [gemnasium-db](https://gitlab.com/gitlab-org/security-product
to find a vulnerability in the Gemnasium database.
You can also [submit new vulnerabilities](https://gitlab.com/gitlab-org/security-products/gemnasium-db/blob/master/CONTRIBUTING.md).
-<!-- ## Troubleshooting
+## Troubleshooting
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+### Error response from daemon: error processing tar file: docker-tar: relocation error
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+This error occurs when the Docker version used to run the SAST job is `19.03.00`.
+You are advised to update to Docker `19.03.01` or greater. Older versions are not
+affected. Read more in
+[this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/13830#note_211354992 "Current SAST container fails").
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 15a21bb82e0..956d3ef7c8c 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -51,6 +51,10 @@ To run a SAST job, you need GitLab Runner with the
executor running in privileged mode. If you're using the shared Runners on GitLab.com,
this is enabled by default.
+CAUTION: **Caution:**
+If you use your own Runners, make sure that the Docker version you have installed
+is **not** `19.03.00`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
+
## Supported languages and frameworks
The following table shows which languages, package managers and frameworks are supported and which tools are used.
@@ -350,14 +354,11 @@ Once a vulnerability is found, you can interact with it. Read more on how to
For more information about the vulnerabilities database update, check the
[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
-<!-- ## Troubleshooting
+## Troubleshooting
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+### Error response from daemon: error processing tar file: docker-tar: relocation error
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+This error occurs when the Docker version used to run the SAST job is `19.03.00`.
+You are advised to update to Docker `19.03.01` or greater. Older versions are not
+affected. Read more in
+[this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/13830#note_211354992 "Current SAST container fails").
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
index 1fe76a9e08f..1fe76a9e08f 100755..100644
--- a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
+++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png
index 09979ba99b3..09979ba99b3 100755..100644
--- a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png
+++ b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_3.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
index 51e80bdb50d..51e80bdb50d 100755..100644
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
Binary files differ
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 83ddcf61664..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,7 +126,8 @@ file.
### JupyterHub
-> Available for project-level clusters since GitLab 11.0.
+> - 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
@@ -138,8 +136,9 @@ web-based interactive programming environment used for data analysis,
visualization, and machine learning.
Authentication will be enabled only for [project
-members](../project/members/index.md) with [Developer or
-higher](../permissions.md) access to the project.
+members](../project/members/index.md) for project-level clusters and group
+members for group-level clusters with [Developer or
+higher](../permissions.md) access to the associated project or group.
We use a [custom Jupyter
image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile)
@@ -161,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:
@@ -188,7 +187,8 @@ You can clone repositories from the files tab in Jupyter:
### Knative
-> Available for project-level clusters since GitLab 11.5.
+> - 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
@@ -211,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
@@ -252,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/project/description_templates.md b/doc/user/project/description_templates.md
index f53dc056010..a79f368499c 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -87,7 +87,7 @@ pre-filled with the text you entered in the template(s).
We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples.
> **Tip:**
-It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions.
+It is possible to use [quick actions](quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions to perform the relevant actions.
Here is an example for a Bug report template:
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/merge_requests/img/merge_request_diff_v12_2.png b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png
new file mode 100644
index 00000000000..e56fbb9750f
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_diff_v12_2.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 9f31f38460a..aa58e971cc3 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -164,6 +164,26 @@ you hide threads that are no longer relevant.
[Read more about resolving threads in merge requests reviews.](../../discussions/index.md)
+## View changes between file versions
+
+The **Changes** tab of a merge request shows the changes to files between branches or
+commits. This view of changes to a file is also known as a **diff**. By default, the diff view
+compares the file in the merge request branch and the file in the target branch.
+
+The diff view includes the following:
+
+- The file's name and path.
+- The number of lines added and deleted.
+- Buttons for the following options:
+ - Toggle comments for this file; useful for inline reviews.
+ - Edit the file in the merge request's branch.
+ - Show full file, in case you want to look at the changes in context with the rest of the file.
+ - View file at the current commit.
+ - Preview the changes with [Review Apps](../../../ci/review_apps/index.md).
+- The changed lines, with the specific changes highlighted.
+
+![Example screenshot of a source code diff](img/merge_request_diff_v12_2.png)
+
## Commenting on any file line in merge requests
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13950) in GitLab 11.5.
@@ -378,6 +398,8 @@ You can also use this push option in addition to the
### Add or remove labels using git push options
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31831) in GitLab 12.3.
+
You can add or remove labels from merge requests using push options.
For example, to add two labels to an existing merge request, use the
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 9bf400e7dff..ecd8f74194e 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -194,7 +194,7 @@ terminal:
Once the terminal has started, the console will be displayed and we could access
the project repository files.
-**Important**. The terminal job is branch dependant. This means that the
+**Important**. The terminal job is branch dependent. This means that the
configuration file used to trigger and configure the terminal will be the one in
the selected branch of the Web IDE.
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/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 5d08bf5e77d..2ec733182f8 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -1,109 +1,134 @@
+---
+type: reference
+---
+
# GitLab keyboard shortcuts
-You can see GitLab's keyboard shortcuts by using <kbd>shift</kbd> + <kbd>?</kbd>
+GitLab has many useful keyboard shortcuts to make it easier to access different features.
+You can see the quick reference sheet within GitLab itself with <kbd>Shift</kbd> + <kbd>?</kbd>.
-## Global Shortcuts
+The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must
+be in specific pages for the other shortcuts to be available, as explained in each
+section below.
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>n</kbd> | Main navigation |
-| <kbd>s</kbd> | Focus search |
-| <kbd>f</kbd> | Focus filter |
-| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
-| <kbd>?</kbd> | Show/hide this dialog |
-| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
-| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
+## Global Shortcuts
-## Project Files Browsing
+These shortcuts are available in most areas of GitLab
+
+| Keyboard Shortcut | Description |
+| ------------------------------- | ----------- |
+| <kbd>?</kbd> | Show/hide shortcut reference sheet. |
+| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to your Projects page. |
+| <kbd>Shift</kbd> + <kbd>g</kbd> | Go to your Groups page. |
+| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to your Activity page. |
+| <kbd>Shift</kbd> + <kbd>l</kbd> | Go to your Milestones page. |
+| <kbd>Shift</kbd> + <kbd>s</kbd> | Go to your Snippets page. |
+| <kbd>s</kbd> | Put cursor in the issues/merge requests search. |
+| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. |
+| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.|
+| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
+| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. |
+
+Additionally, the following shortcuts are available when editing text in text fields,
+for example comments, replies, or issue and merge request descriptions:
+
+| Keyboard Shortcut | Description |
+| ---------------------------------------------------------------------- | ----------- |
+| <kbd>↑</kbd> | Edit your last comment. You must be in a blank text field below a thread, and you must already have at least one comment in the thread. |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle Markdown preview, when editing text in a text field that has **Write** and **Preview** tabs at the top. |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>↑</kbd> | Move selection up |
-| <kbd>↓</kbd> | Move selection down |
-| <kbd>enter</kbd> | Open selection |
+## Project
-## Finding Project File
+These shortcuts are available from any page within a project. You must type them
+relatively quickly to work, and they will take you to another page in the project.
+
+| Keyboard Shortcut | Description |
+| --------------------------- | ----------- |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project home page (**Project > Details**). |
+| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project activity feed (**Project > Activity**). |
+| <kbd>g</kbd> + <kbd>r</kbd> | Go to the project releases list (**Project > Releases**). |
+| <kbd>g</kbd> + <kbd>f</kbd> | Go to the [project files](#project-files) list (**Repository > Files**). |
+| <kbd>t</kbd> | Go to the project file search page. (**Repository > Files**, click **Find Files**). |
+| <kbd>g</kbd> + <kbd>c</kbd> | Go to the project commits list (**Repository > Commits**). |
+| <kbd>g</kbd> + <kbd>n</kbd> | Go to the [repository graph](#repository-graph) page (**Repository > Graph**). |
+| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts (**Repository > Charts**). |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to the project issues list (**Issues > List**). |
+| <kbd>i</kbd> | Go to the New Issue page (**Issues**, click **New Issue** ). |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to the project issue boards list (**Issues > Boards**). |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to the project merge requests list (**Merge Requests**). |
+| <kbd>g</kbd> + <kbd>j</kbd> | Go to the CI/CD jobs list (**CI/CD > Jobs**). |
+| <kbd>g</kbd> + <kbd>l</kbd> | Go to the project metrics (**Operations > Metrics**). |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project environments (**Operations > Environments**). |
+| <kbd>g</kbd> + <kbd>k</kbd> | Go to the project Kubernetes cluster integration page (**Operations > Kubernetes**). Note that you must have at least [`maintainer` permissions](../user/permissions.md) to access this page. |
+| <kbd>g</kbd> + <kbd>s</kbd> | Go to the project snippets list (**Snippets**). |
+| <kbd>g</kbd> + <kbd>w</kbd> | Go to the project wiki (**Wiki**), if enabled. |
+
+### Issues and Merge Requests
+
+These shortcuts are available when viewing issues and merge requests.
+
+| Keyboard Shortcut | Description |
+| ---------------------------- | ----------- |
+| <kbd>e</kbd> | Edit description. |
+| <kbd>a</kbd> | Change assignee. |
+| <kbd>m</kbd> | Change milestone. |
+| <kbd>l</kbd> | Change label. |
+| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
+| <kbd>n</kbd> | Move to next unresolved discussion (Merge requests only). |
+| <kbd>p</kbd> | Move to previous unresolved discussion (Merge requests only). |
+| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (Merge requests only). |
+| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (Merge requests only). |
+
+### Project Files
+
+These shortcuts are available when browsing the files in a project (navigate to
+**Repository** > **Files**):
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>↑</kbd> | Move selection up |
-| <kbd>↓</kbd> | Move selection down |
-| <kbd>enter</kbd> | Open selection |
-| <kbd>esc</kbd> | Go back |
+| <kbd>↑</kbd> | Move selection up. |
+| <kbd>↓</kbd> | Move selection down. |
+| <kbd>enter</kbd> | Open selection. |
+| <kbd>esc</kbd> | Go back to file list screen (only while searching for files, **Repository > Files** then click on **Find File**). |
+| <kbd>y</kbd> | Go to file permalink (only while viewing a file). |
-## Global Dashboard
+### Web IDE
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to the activity feed |
-| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to projects |
-| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to issues |
-| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to merge requests |
-| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to todos |
+These shortcuts are available when editing a file with the [Web IDE](../user/project/web_ide/index.md):
-## Project
+| Keyboard Shortcut | Description |
+| ------------------------------------------------------- | ----------- |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message). |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
-| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
-| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
-| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
-| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
-| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
-| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
-| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
-| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
-| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
-| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
-| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
-| <kbd>t</kbd> | Go to finding file |
-| <kbd>i</kbd> | New issue |
-
-## Network Graph
+### Repository Graph
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left |
-| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right |
-| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up |
-| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down |
-| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top |
-| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
+These shortcuts are available when viewing the project [repository graph](../user/project/repository/index.md#repository-graph)
+page (navigate to **Repository > Graph**):
-## Issues and Merge Requests
+| Keyboard Shortcut | Description |
+| ------------------------------------------------------------------ | ----------- |
+| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left. |
+| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right. |
+| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up. |
+| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down. |
+| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top. |
+| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom. |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>a</kbd> | Change assignee |
-| <kbd>m</kbd> | Change milestone |
-| <kbd>r</kbd> | Reply (quoting selected text) |
-| <kbd>e</kbd> | Edit issue/merge request |
-| <kbd>l</kbd> | Change label |
-| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file |
-| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file |
-| <kbd>n</kbd> | Move to next unresolved discussion |
-| <kbd>p</kbd> | Move to previous unresolved discussion |
+### Wiki pages
-## Epics **(ULTIMATE)**
+This shortcut is available when viewing a [wiki page](../user/project/wiki/index.md):
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>r</kbd> | Reply (quoting selected text) |
-| <kbd>e</kbd> | Edit description |
-| <kbd>l</kbd> | Change label |
-
-## Wiki pages
+| <kbd>e</kbd> | Edit wiki page. |
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>e</kbd> | Edit wiki page|
+## Epics **(ULTIMATE)**
-## Web IDE
+These shortcuts are available when viewing [Epics](../user/group/epics/index.md):
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
-| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>p</kbd> | Go to file |
-| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message) |
+| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
+| <kbd>e</kbd> | Edit description. |
+| <kbd>l</kbd> | Change label. |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index aa6a67d817a..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
@@ -161,6 +162,7 @@ module API
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
+ mount ::API::Statistics
mount ::API::Submodules
mount ::API::Subscriptions
mount ::API::Suggestions
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f7cd6d35854..c9b3483acaf 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1169,6 +1169,55 @@ module API
expose :message, :starts_at, :ends_at, :color, :font
end
+ class ApplicationStatistics < Grape::Entity
+ include ActionView::Helpers::NumberHelper
+ include CountHelper
+
+ expose :forks do |counts|
+ approximate_fork_count_with_delimiters(counts)
+ end
+
+ expose :issues do |counts|
+ approximate_count_with_delimiters(counts, ::Issue)
+ end
+
+ expose :merge_requests do |counts|
+ approximate_count_with_delimiters(counts, ::MergeRequest)
+ end
+
+ expose :notes do |counts|
+ approximate_count_with_delimiters(counts, ::Note)
+ end
+
+ expose :snippets do |counts|
+ approximate_count_with_delimiters(counts, ::Snippet)
+ end
+
+ expose :ssh_keys do |counts|
+ approximate_count_with_delimiters(counts, ::Key)
+ end
+
+ expose :milestones do |counts|
+ approximate_count_with_delimiters(counts, ::Milestone)
+ end
+
+ expose :users do |counts|
+ approximate_count_with_delimiters(counts, ::User)
+ end
+
+ expose :projects do |counts|
+ approximate_count_with_delimiters(counts, ::Project)
+ end
+
+ expose :groups do |counts|
+ approximate_count_with_delimiters(counts, ::Group)
+ end
+
+ expose :active_users do |_|
+ number_with_delimiter(::User.active.count)
+ end
+ end
+
class ApplicationSetting < Grape::Entity
def self.exposed_attributes
attributes = ::ApplicationSettingsHelper.visible_attributes
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/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/statistics.rb b/lib/api/statistics.rb
new file mode 100644
index 00000000000..d2dce34dfa5
--- /dev/null
+++ b/lib/api/statistics.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ class Statistics < Grape::API
+ before { authenticated_as_admin! }
+
+ COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
+ MergeRequest, Note, Snippet, Key, Milestone].freeze
+
+ desc 'Get the current application statistics' do
+ success Entities::ApplicationStatistics
+ end
+ get "application/statistics" do
+ counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
+ present counts, with: Entities::ApplicationStatistics
+ end
+ end
+end
diff --git a/lib/api/validations/types/workhorse_file.rb b/lib/api/validations/types/workhorse_file.rb
new file mode 100644
index 00000000000..18d111f6556
--- /dev/null
+++ b/lib/api/validations/types/workhorse_file.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Validations
+ module Types
+ class WorkhorseFile < Virtus::Attribute
+ def coerce(input)
+ # Processing of multipart file objects
+ # is already taken care of by Gitlab::Middleware::Multipart.
+ # Nothing to do here.
+ input
+ end
+
+ def value_coerced?(value)
+ value.is_a?(::UploadedFile)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 5724adb2c40..c5a5488950d 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -4,11 +4,21 @@ module API
class Wikis < Grape::API
helpers do
def commit_params(attrs)
- {
- file_name: attrs[:file][:filename],
- file_content: attrs[:file][:tempfile].read,
- branch_name: attrs[:branch]
- }
+ # In order to avoid service disruption this can work with an old workhorse without the acceleration
+ # the first branch of this if must be removed when we drop support for non accelerated uploads
+ if attrs[:file].is_a?(Hash)
+ {
+ file_name: attrs[:file][:filename],
+ file_content: attrs[:file][:tempfile].read,
+ branch_name: attrs[:branch]
+ }
+ else
+ {
+ file_name: attrs[:file].original_filename,
+ file_content: attrs[:file].read,
+ branch_name: attrs[:branch]
+ }
+ end
end
params :common_wiki_page_params do
@@ -106,7 +116,7 @@ module API
success Entities::WikiAttachment
end
params do
- requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
+ requires :file, types: [::API::Validations::Types::SafeFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
post ":id/wikis/attachments" do
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/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 164a4634d84..899df81ea5c 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -19,6 +19,7 @@ module Gitlab
user: @command.current_user,
pipeline_schedule: @command.schedule,
merge_request: @command.merge_request,
+ external_pull_request: @command.external_pull_request,
variables_attributes: Array(@command.variables_attributes)
)
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index afad391e8e0..58f89a6be5e 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -7,7 +7,7 @@ module Gitlab
Command = Struct.new(
:source, :project, :current_user,
:origin_ref, :checkout_sha, :after_sha, :before_sha, :source_sha, :target_sha,
- :trigger_request, :schedule, :merge_request,
+ :trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update
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/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 702c73e8e4d..e2911b4e6c8 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -39,7 +39,12 @@ module Gitlab
end
def gitlab_helper
- gitlab if respond_to?(:gitlab)
+ # Unfortunately the following does not work:
+ # - respond_to?(:gitlab)
+ # - respond_to?(:gitlab, true)
+ gitlab
+ rescue NoMethodError
+ nil
end
def release_automation?
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 42cd94add79..28d48ce6dfe 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -3,49 +3,62 @@
module Gitlab
module ImportExport
class AttributesFinder
- def initialize(included_attributes:, excluded_attributes:, methods:)
- @included_attributes = included_attributes || {}
- @excluded_attributes = excluded_attributes || {}
- @methods = methods || {}
+ def initialize(config:)
+ @tree = config[:tree] || {}
+ @included_attributes = config[:included_attributes] || {}
+ @excluded_attributes = config[:excluded_attributes] || {}
+ @methods = config[:methods] || {}
+ @preloads = config[:preloads] || {}
end
- def find(model_object)
- parsed_hash = find_attributes_only(model_object)
- parsed_hash.empty? ? model_object : { model_object => parsed_hash }
+ def find_root(model_key)
+ find(model_key, @tree[model_key])
end
- def parse(model_object)
- parsed_hash = find_attributes_only(model_object)
- yield parsed_hash unless parsed_hash.empty?
+ def find_relations_tree(model_key)
+ @tree[model_key]
end
- def find_included(value)
- key = key_from_hash(value)
- @included_attributes[key].nil? ? {} : { only: @included_attributes[key] }
+ def find_excluded_keys(klass_name)
+ @excluded_attributes[klass_name.to_sym]&.map(&:to_s) || []
end
- def find_excluded(value)
- key = key_from_hash(value)
- @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] }
+ private
+
+ def find(model_key, model_tree)
+ {
+ only: @included_attributes[model_key],
+ except: @excluded_attributes[model_key],
+ methods: @methods[model_key],
+ include: resolve_model_tree(model_tree),
+ preload: resolve_preloads(model_key, model_tree)
+ }.compact
end
- def find_method(value)
- key = key_from_hash(value)
- @methods[key].nil? ? {} : { methods: @methods[key] }
+ 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 find_excluded_keys(klass_name)
- @excluded_attributes[klass_name.to_sym]&.map(&:to_s) || []
+ 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
- private
+ def resolve_model_tree(model_tree)
+ return unless model_tree
- def find_attributes_only(value)
- find_included(value).merge(find_excluded(value)).merge(find_method(value))
+ model_tree
+ .map(&method(:resolve_model))
end
- def key_from_hash(value)
- value.is_a?(Hash) ? value.first.first : value
+ def resolve_model(model_key, model_tree)
+ { model_key => find(model_key, model_tree) }
end
end
end
diff --git a/lib/gitlab/import_export/config.rb b/lib/gitlab/import_export/config.rb
index f6cd4eb5e0c..6f4919ead4e 100644
--- a/lib/gitlab/import_export/config.rb
+++ b/lib/gitlab/import_export/config.rb
@@ -3,70 +3,49 @@
module Gitlab
module ImportExport
class Config
+ def initialize
+ @hash = parse_yaml
+ @hash.deep_symbolize_keys!
+ @ee_hash = @hash.delete(:ee) || {}
+
+ @hash[:tree] = normalize_tree(@hash[:tree])
+ @ee_hash[:tree] = normalize_tree(@ee_hash[:tree] || {})
+ end
+
# Returns a Hash of the YAML file, including EE specific data if EE is
# used.
def to_h
- hash = parse_yaml
- ee_hash = hash['ee']
-
- if merge? && ee_hash
- ee_hash.each do |key, value|
- if key == 'project_tree'
- merge_project_tree(value, hash[key])
- else
- merge_attributes_list(value, hash[key])
- end
- end
+ if merge_ee?
+ deep_merge(@hash, @ee_hash)
+ else
+ @hash
end
-
- # We don't want to expose this section after this point, as it is no
- # longer needed.
- hash.delete('ee')
-
- hash
end
- # Merges a project relationships tree into the target tree.
- #
- # @param [Array<Hash|Symbol>] source_values
- # @param [Array<Hash|Symbol>] target_values
- def merge_project_tree(source_values, target_values)
- source_values.each do |value|
- if value.is_a?(Hash)
- # Examples:
- #
- # { 'project_tree' => [{ 'labels' => [...] }] }
- # { 'notes' => [:author, { 'events' => [:push_event_payload] }] }
- value.each do |key, val|
- target = target_values
- .find { |h| h.is_a?(Hash) && h[key] }
+ private
- if target
- merge_project_tree(val, target[key])
- else
- target_values << { key => val.dup }
- end
- end
- else
- # Example: :priorities, :author, etc
- target_values << value
- end
+ def deep_merge(hash_a, hash_b)
+ hash_a.deep_merge(hash_b) do |_, this_val, other_val|
+ this_val.to_a + other_val.to_a
end
end
- # Merges a Hash containing a flat list of attributes, such as the entries
- # in a `excluded_attributes` section.
- #
- # @param [Hash] source_values
- # @param [Hash] target_values
- def merge_attributes_list(source_values, target_values)
- source_values.each do |key, values|
- target_values[key] ||= []
- target_values[key].concat(values)
+ def normalize_tree(item)
+ case item
+ when Array
+ item.reduce({}) do |hash, subitem|
+ hash.merge!(normalize_tree(subitem))
+ end
+ when Hash
+ item.transform_values(&method(:normalize_tree))
+ when Symbol
+ { item => {} }
+ else
+ raise ArgumentError, "#{item} needs to be Array, Hash, Symbol or NilClass"
end
end
- def merge?
+ def merge_ee?
Gitlab.ee?
end
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 bd0f3e70749..511b702553e 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -3,87 +3,92 @@
# This list _must_ only contain relationships that are available to both CE and
# EE. EE specific relationships must be defined in the `ee` section further
# down below.
-project_tree:
- - labels:
- - :priorities
- - milestones:
- - events:
- - :push_event_payload
- - issues:
- - events:
- - :push_event_payload
- - :timelogs
- - notes:
- - :author
- - events:
- - :push_event_payload
- - label_links:
- - label:
- - :priorities
- - milestone:
- - events:
- - :push_event_payload
- - resource_label_events:
- - label:
- - :priorities
- - :issue_assignees
- - snippets:
- - :award_emoji
- - notes:
- - :author
- - releases:
- - :links
- - project_members:
- - :user
- - merge_requests:
- - :metrics
- - notes:
- - :author
+tree:
+ project:
+ - labels:
+ - :priorities
+ - milestones:
- events:
- :push_event_payload
- - :suggestions
- - merge_request_diff:
- - :merge_request_diff_commits
- - :merge_request_diff_files
- - events:
- - :push_event_payload
- - :timelogs
- - label_links:
- - label:
- - :priorities
- - milestone:
+ - issues:
- events:
- :push_event_payload
- - resource_label_events:
- - label:
- - :priorities
- - ci_pipelines:
- - notes:
- - :author
+ - :timelogs
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - label_links:
+ - label:
+ - :priorities
+ - milestone:
+ - events:
+ - :push_event_payload
+ - resource_label_events:
+ - label:
+ - :priorities
+ - :issue_assignees
+ - snippets:
+ - :award_emoji
+ - notes:
+ - :author
+ - releases:
+ - :links
+ - project_members:
+ - :user
+ - merge_requests:
+ - :metrics
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - :suggestions
+ - merge_request_diff:
+ - :merge_request_diff_commits
+ - :merge_request_diff_files
- events:
- :push_event_payload
- - stages:
- - :statuses
- - :auto_devops
- - :triggers
- - :pipeline_schedules
- - :services
- - protected_branches:
- - :merge_access_levels
- - :push_access_levels
- - protected_tags:
- - :create_access_levels
- - :project_feature
- - :custom_attributes
- - :prometheus_metrics
- - :project_badges
- - :ci_cd_settings
- - :error_tracking_setting
- - :metrics_setting
- - boards:
- - lists:
- - label:
- - :priorities
+ - :timelogs
+ - label_links:
+ - label:
+ - :priorities
+ - milestone:
+ - events:
+ - :push_event_payload
+ - resource_label_events:
+ - label:
+ - :priorities
+ - ci_pipelines:
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - stages:
+ - :statuses
+ - :external_pull_request
+ - :external_pull_requests
+ - :auto_devops
+ - :triggers
+ - :pipeline_schedules
+ - :services
+ - protected_branches:
+ - :merge_access_levels
+ - :push_access_levels
+ - protected_tags:
+ - :create_access_levels
+ - :project_feature
+ - :custom_attributes
+ - :prometheus_metrics
+ - :project_badges
+ - :ci_cd_settings
+ - :error_tracking_setting
+ - :metrics_setting
+ - boards:
+ - lists:
+ - label:
+ - :priorities
+ group_members:
+ - :user
# Only include the following attributes for the models specified.
included_attributes:
@@ -223,12 +228,25 @@ methods:
- :type
lists:
- :list_type
+ 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:
- project_tree:
- - protected_branches:
- - :unprotect_access_levels
- - protected_environments:
- - :deploy_access_levels
+ tree:
+ project:
+ protected_branches:
+ - :unprotect_access_levels
+ protected_environments:
+ - :deploy_access_levels
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
deleted file mode 100644
index a92e3862361..00000000000
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- # Generates a hash that conforms with http://apidock.com/rails/Hash/to_json
- # and its peculiar options.
- class JsonHashBuilder
- def self.build(model_objects, attributes_finder)
- new(model_objects, attributes_finder).build
- end
-
- def initialize(model_objects, attributes_finder)
- @model_objects = model_objects
- @attributes_finder = attributes_finder
- end
-
- def build
- process_model_objects(@model_objects)
- end
-
- private
-
- # Called when the model is actually a hash containing other relations (more models)
- # Returns the config in the right format for calling +to_json+
- #
- # +model_object_hash+ - A model relationship such as:
- # {:merge_requests=>[:merge_request_diff, :notes]}
- def process_model_objects(model_object_hash)
- json_config_hash = {}
- current_key = model_object_hash.first.first
-
- model_object_hash.values.flatten.each do |model_object|
- @attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash }
- handle_model_object(current_key, model_object, json_config_hash)
- end
-
- json_config_hash
- end
-
- # Creates or adds to an existing hash an individual model or list
- #
- # +current_key+ main model that will be a key in the hash
- # +model_object+ model or list of models to include in the hash
- # +json_config_hash+ the original hash containing the root model
- def handle_model_object(current_key, model_object, json_config_hash)
- model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object
-
- if json_config_hash[current_key]
- add_model_value(current_key, model_or_sub_model, json_config_hash)
- else
- create_model_value(current_key, model_or_sub_model, json_config_hash)
- end
- end
-
- # Constructs a new hash that will hold the configuration for that particular object
- # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
- #
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- # +json_config_hash+ the original hash containing the root model
- def create_model_value(current_key, value, json_config_hash)
- json_config_hash[current_key] = parse_hash(value) || { include: value }
- end
-
- # Calls attributes finder to parse the hash and add any attributes to it
- #
- # +value+ existing model to be included in the hash
- # +parsed_hash+ the original hash
- def parse_hash(value)
- return if already_contains_methods?(value)
-
- @attributes_finder.parse(value) do |hash|
- { include: hash_or_merge(value, hash) }
- end
- end
-
- def already_contains_methods?(value)
- value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
- end
-
- # Adds new model configuration to an existing hash with key +current_key+
- # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
- #
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- # +json_config_hash+ the original hash containing the root model
- def add_model_value(current_key, value, json_config_hash)
- @attributes_finder.parse(value) do |hash|
- value = { value => hash } unless value.is_a?(Hash)
- end
-
- add_to_array(current_key, json_config_hash, value)
- end
-
- # Adds new model configuration to an existing hash with key +current_key+
- # it creates a new array if it was previously a single value
- #
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- # +json_config_hash+ the original hash containing the root model
- def add_to_array(current_key, json_config_hash, value)
- old_values = json_config_hash[current_key][:include]
-
- json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
- end
-
- # Construct a new hash or merge with an existing one a model configuration
- # This is to fulfil +to_json+ requirements.
- #
- # +hash+ hash containing configuration generated mainly from +@attributes_finder+
- # +value+ existing model to be included in the hash
- def hash_or_merge(value, hash)
- value.is_a?(Hash) ? value.merge(hash) : { value => hash }
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 91fe4e5d074..2dd18616cd6 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -58,11 +58,13 @@ module Gitlab
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project.
def create_relations
- default_relation_list.each do |relation|
- if relation.is_a?(Hash)
- create_sub_relations(relation, @tree_hash)
- elsif @tree_hash[relation.to_s].present?
- save_relation_hash(@tree_hash[relation.to_s], relation)
+ project_relations_without_project_members.each do |relation_key, relation_definition|
+ relation_key_s = relation_key.to_s
+
+ if relation_definition.present?
+ create_sub_relations(relation_key_s, relation_definition, @tree_hash)
+ elsif @tree_hash[relation_key_s].present?
+ save_relation_hash(relation_key_s, @tree_hash[relation_key_s])
end
end
@@ -71,7 +73,7 @@ module Gitlab
@saved
end
- def save_relation_hash(relation_hash_batch, relation_key)
+ def save_relation_hash(relation_key, relation_hash_batch)
relation_hash = create_relation(relation_key, relation_hash_batch)
remove_group_models(relation_hash) if relation_hash.is_a?(Array)
@@ -91,10 +93,13 @@ module Gitlab
end
end
- def default_relation_list
- reader.tree.reject do |model|
- model.is_a?(Hash) && model[:project_members]
- end
+ def project_relations_without_project_members
+ # We remove `project_members` as they are deserialized separately
+ project_relations.except(:project_members)
+ end
+
+ def project_relations
+ reader.attributes_finder.find_relations_tree(:project)
end
def restore_project
@@ -107,7 +112,7 @@ module Gitlab
def project_params
@project_params ||= begin
- attrs = json_params.merge(override_params).merge(visibility_level)
+ attrs = json_params.merge(override_params).merge(visibility_level, external_label)
# Cleaning all imported and overridden params
Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
@@ -135,6 +140,13 @@ module Gitlab
{ 'visibility_level' => level }
end
+ def external_label
+ label = override_params['external_authorization_classification_label'].presence ||
+ json_params['external_authorization_classification_label'].presence
+
+ { 'external_authorization_classification_label' => label }
+ end
+
# Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+
@@ -143,8 +155,7 @@ module Gitlab
# issue, finds any subrelations such as notes, creates them and assign them back to the hash
#
# Recursively calls this method if the sub-relation is a hash containing more sub-relations
- def create_sub_relations(relation, tree_hash, save: true)
- relation_key = relation.keys.first.to_s
+ def create_sub_relations(relation_key, relation_definition, tree_hash, save: true)
return if tree_hash[relation_key].blank?
tree_array = [tree_hash[relation_key]].flatten
@@ -164,13 +175,13 @@ module Gitlab
# But we can't have it in the upper level or GC won't get rid of the AR objects
# after we save the batch.
Project.transaction do
- process_sub_relation(relation, relation_item)
+ process_sub_relation(relation_key, relation_definition, relation_item)
# For every subrelation that hangs from Project, save the associated records altogether
# This effectively batches all records per subrelation item, only keeping those in memory
# We have to keep in mind that more batch granularity << Memory, but >> Slowness
if save
- save_relation_hash([relation_item], relation_key)
+ save_relation_hash(relation_key, [relation_item])
tree_hash[relation_key].delete(relation_item)
end
end
@@ -179,37 +190,35 @@ module Gitlab
tree_hash.delete(relation_key) if save
end
- def process_sub_relation(relation, relation_item)
- relation.values.flatten.each do |sub_relation|
+ def process_sub_relation(relation_key, relation_definition, relation_item)
+ relation_definition.each do |sub_relation_key, sub_relation_definition|
# We just use author to get the user ID, do not attempt to create an instance.
- next if sub_relation == :author
+ next if sub_relation_key == :author
- create_sub_relations(sub_relation, relation_item, save: false) if sub_relation.is_a?(Hash)
+ sub_relation_key_s = sub_relation_key.to_s
- relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
- relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
- end
- end
+ # create dependent relations if present
+ if sub_relation_definition.present?
+ create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false)
+ end
- def assign_relation_hash(relation_item, sub_relation)
- if sub_relation.is_a?(Hash)
- relation_hash = relation_item[sub_relation.keys.first.to_s]
- sub_relation = sub_relation.keys.first
- else
- relation_hash = relation_item[sub_relation.to_s]
+ # transform relation hash to actual object
+ sub_relation_hash = relation_item[sub_relation_key_s]
+ if sub_relation_hash.present?
+ relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash)
+ end
end
-
- [relation_hash, sub_relation]
end
- def create_relation(relation, relation_hash_list)
+ def create_relation(relation_key, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
- Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: relation_hash,
- members_mapper: members_mapper,
- user: @user,
- project: @restored_project,
- excluded_keys: excluded_keys_for_relation(relation))
+ Gitlab::ImportExport::RelationFactory.create(
+ relation_sym: relation_key.to_sym,
+ relation_hash: relation_hash,
+ members_mapper: members_mapper,
+ user: @user,
+ project: @restored_project,
+ excluded_keys: excluded_keys_for_relation(relation_key))
end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 2255635acdf..f75f69b2c75 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -18,7 +18,10 @@ module Gitlab
def save
mkdir_p(@shared.export_path)
- File.write(full_path, project_json_tree)
+ project_tree = serialize_project_tree
+ fix_project_tree(project_tree)
+ File.write(full_path, project_tree.to_json)
+
true
rescue => e
@shared.error(e)
@@ -27,27 +30,31 @@ module Gitlab
private
- def project_json_tree
+ def fix_project_tree(project_tree)
if @params[:description].present?
- project_json['description'] = @params[:description]
+ project_tree['description'] = @params[:description]
end
- project_json['project_members'] += group_members_json
-
- RelationRenameService.add_new_associations(project_json)
+ project_tree['project_members'] += group_members_array
- project_json.to_json
+ RelationRenameService.add_new_associations(project_tree)
end
- def project_json
- @project_json ||= @project.as_json(reader.project_tree)
+ def serialize_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
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
- def group_members_json
+ def group_members_array
group_members.as_json(reader.group_members_tree).each do |group_member|
group_member['source_type'] = 'Project' # Make group members project members of the future import
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 8bdf6ca491d..9e81c6a3d07 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -7,42 +7,22 @@ module Gitlab
def initialize(shared:)
@shared = shared
- config_hash = ImportExport::Config.new.to_h.deep_symbolize_keys
- @tree = config_hash[:project_tree]
- @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes],
- excluded_attributes: config_hash[:excluded_attributes],
- methods: config_hash[:methods])
+
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(
+ config: ImportExport::Config.new.to_h)
end
# Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
# for outputting a project in JSON format, including its relations and sub relations.
def project_tree
- attributes = @attributes_finder.find(:project)
- project_attributes = attributes.is_a?(Hash) ? attributes[:project] : {}
-
- project_attributes.merge(include: build_hash(@tree))
+ attributes_finder.find_root(:project)
rescue => e
@shared.error(e)
false
end
def group_members_tree
- @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
- end
-
- private
-
- # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
- #
- # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file
- def build_hash(model_list)
- model_list.map do |model_objects|
- if model_objects.is_a?(Hash)
- Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder)
- else
- @attributes_finder.find(model_objects)
- end
- end
+ attributes_finder.find_root(:group_members)
end
end
end
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 86b28e4e20a..0ee9563c227 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -32,7 +32,7 @@ module Gitlab
class Handler
def initialize(env, message)
- @request = ActionDispatch::Request.new(env)
+ @request = Rack::Request.new(env)
@rewritten_fields = message['rewritten_fields']
@open_files = []
end
@@ -50,7 +50,7 @@ module Gitlab
value = decorate_params_value(value, @request.params[key])
end
- @request.update_param(key, value)
+ update_param(key, value)
end
yield
@@ -92,6 +92,20 @@ module Gitlab
::UploadedFile.from_params(params, key, allowed_paths)
end
+
+ # update_params ensures that both rails controllers and rack middleware can find
+ # workhorse accelerate files in the request
+ def update_param(key, value)
+ # we make sure we have key in POST otherwise update_params will add it in GET
+ @request.POST[key] ||= value
+
+ # this will force Rack::Request to properly update env keys
+ @request.update_param(key, value)
+
+ # ActionDispatch::Request is based on Rack::Request but it caches params
+ # inside other env keys, here we ensure everything is updated correctly
+ ActionDispatch::Request.new(@request.env).update_param(key, value)
+ end
end
def initialize(app)
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/push_options.rb b/lib/gitlab/push_options.rb
index a2296d265cd..93c0f3132d0 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -56,19 +56,23 @@ module Gitlab
next if [namespace, key].any?(&:nil?)
- options[namespace] ||= HashWithIndifferentAccess.new
-
- if option_multi_value?(namespace, key)
- options[namespace][key] ||= HashWithIndifferentAccess.new(0)
- options[namespace][key][value] += 1
- else
- options[namespace][key] = value
- end
+ store_option_info(options, namespace, key, value)
end
options
end
+ def store_option_info(options, namespace, key, value)
+ options[namespace] ||= HashWithIndifferentAccess.new
+
+ if option_multi_value?(namespace, key)
+ options[namespace][key] ||= HashWithIndifferentAccess.new(0)
+ options[namespace][key][value] += 1
+ else
+ options[namespace][key] = value
+ end
+ end
+
def option_multi_value?(namespace, key)
MULTI_VALUE_OPTIONS.any? { |arr| arr == [namespace, key] }
end
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index 533c74ba9b4..183191f31a6 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -122,7 +122,7 @@ module Gitlab
params '#issue | !merge_request'
types Issue, MergeRequest
condition do
- current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+ current_user.can?(:"admin_#{quick_action_target.to_ability_name}", quick_action_target)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
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/tasks/gitlab_danger.rake b/lib/tasks/gitlab_danger.rake
index c2f5843a9a5..e75539f048c 100644
--- a/lib/tasks/gitlab_danger.rake
+++ b/lib/tasks/gitlab_danger.rake
@@ -1,7 +1,7 @@
desc 'Run local Danger rules'
task :danger_local do
require 'gitlab_danger'
- require_relative '../../lib/gitlab/popen'
+ require 'gitlab/popen'
puts("#{GitlabDanger.local_warning_message}\n")
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d26ce9fa911..9ca3b357f77 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -810,6 +810,9 @@ msgstr ""
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr ""
+msgid "AdminDashboard|Error loading the statistics. Please try again"
+msgstr ""
+
msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered."
msgstr ""
@@ -837,6 +840,30 @@ msgstr ""
msgid "AdminSettings|When creating a new environment variable it will be protected by default."
msgstr ""
+msgid "AdminStatistics|Active Users"
+msgstr ""
+
+msgid "AdminStatistics|Forks"
+msgstr ""
+
+msgid "AdminStatistics|Issues"
+msgstr ""
+
+msgid "AdminStatistics|Merge Requests"
+msgstr ""
+
+msgid "AdminStatistics|Milestones"
+msgstr ""
+
+msgid "AdminStatistics|Notes"
+msgstr ""
+
+msgid "AdminStatistics|SSH Keys"
+msgstr ""
+
+msgid "AdminStatistics|Snippets"
+msgstr ""
+
msgid "AdminUsers|2FA Disabled"
msgstr ""
@@ -2035,10 +2062,10 @@ msgstr ""
msgid "Certificate (PEM)"
msgstr ""
-msgid "Change Label"
+msgid "Change assignee"
msgstr ""
-msgid "Change assignee"
+msgid "Change label"
msgstr ""
msgid "Change milestone"
@@ -2503,6 +2530,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 ""
@@ -2581,6 +2611,9 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Create cluster on"
+msgstr ""
+
msgid "ClusterIntegration|Did you know?"
msgstr ""
@@ -2632,6 +2665,9 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
+msgid "ClusterIntegration|Google GKE"
+msgstr ""
+
msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
@@ -2962,9 +2998,6 @@ msgstr ""
msgid "ClusterIntegration|pricing"
msgstr ""
-msgid "ClusterIntegration|properly configured"
-msgstr ""
-
msgid "ClusterIntegration|sign up"
msgstr ""
@@ -3019,6 +3052,9 @@ msgstr ""
msgid "Comment is being updated"
msgstr ""
+msgid "Comment/Reply (quoting selected text)"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -4189,9 +4225,15 @@ msgstr ""
msgid "Edit comment"
msgstr ""
+msgid "Edit description"
+msgstr ""
+
msgid "Edit environment"
msgstr ""
+msgid "Edit epic description"
+msgstr ""
+
msgid "Edit file"
msgstr ""
@@ -4204,25 +4246,22 @@ msgstr ""
msgid "Edit identity for %{user_name}"
msgstr ""
-msgid "Edit issue"
-msgstr ""
-
msgid "Edit issues"
msgstr ""
-msgid "Edit last comment (when focused on an empty textarea)"
+msgid "Edit public deploy key"
msgstr ""
-msgid "Edit merge request"
+msgid "Edit stage"
msgstr ""
-msgid "Edit public deploy key"
+msgid "Edit wiki page"
msgstr ""
-msgid "Edit stage"
+msgid "Edit your most recent comment in a thread (from an empty textarea)"
msgstr ""
-msgid "Edit wiki page"
+msgid "Editing"
msgstr ""
msgid "Email"
@@ -4543,6 +4582,9 @@ msgstr ""
msgid "Epic"
msgstr ""
+msgid "Epics (Ultimate / Gold license only)"
+msgstr ""
+
msgid "Error"
msgstr ""
@@ -5100,9 +5142,6 @@ msgstr ""
msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file."
msgstr ""
-msgid "Finding Project File"
-msgstr ""
-
msgid "Fingerprint"
msgstr ""
@@ -5127,12 +5166,6 @@ msgstr ""
msgid "FlowdockService|Flowdock is a collaboration web app for technical teams."
msgstr ""
-msgid "Focus Filter"
-msgstr ""
-
-msgid "Focus Search"
-msgstr ""
-
msgid "FogBugz Email"
msgstr ""
@@ -5376,6 +5409,9 @@ msgstr ""
msgid "Go back"
msgstr ""
+msgid "Go back (while searching for files"
+msgstr ""
+
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
msgstr ""
@@ -5397,16 +5433,17 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file permalink"
+msgid "Go to file (MRs only)"
msgstr ""
-msgid "Go to files"
+
+msgid "Go to file permalink (while viewing a file)"
msgstr ""
-msgid "Go to finding file"
+msgid "Go to files"
msgstr ""
-msgid "Go to groups"
+msgid "Go to find file"
msgstr ""
msgid "Go to issue boards"
@@ -5427,52 +5464,67 @@ msgstr ""
msgid "Go to metrics"
msgstr ""
-msgid "Go to milestones"
-msgstr ""
-
-msgid "Go to network graph"
-msgstr ""
-
msgid "Go to parent"
msgstr ""
msgid "Go to project"
msgstr ""
-msgid "Go to projects"
+msgid "Go to releases"
msgstr ""
msgid "Go to repository charts"
msgstr ""
+msgid "Go to repository graph"
+msgstr ""
+
msgid "Go to snippets"
msgstr ""
msgid "Go to the activity feed"
msgstr ""
+msgid "Go to the milestone list"
+msgstr ""
+
msgid "Go to the project's activity feed"
msgstr ""
msgid "Go to the project's overview page"
msgstr ""
-msgid "Go to todos"
+msgid "Go to wiki"
msgstr ""
-msgid "Go to wiki"
+msgid "Go to your To-Do list"
msgstr ""
msgid "Go to your fork"
msgstr ""
+msgid "Go to your groups"
+msgstr ""
+
+msgid "Go to your issues"
+msgstr ""
+
+msgid "Go to your merge requests"
+msgstr ""
+
+msgid "Go to your projects"
+msgstr ""
+
+msgid "Go to your snippets"
+msgstr ""
+
msgid "Google Code import"
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"
@@ -6233,6 +6285,9 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Issues / Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -7254,18 +7309,6 @@ msgstr ""
msgid "Move this issue to another project."
msgstr ""
-msgid "Move to next file"
-msgstr ""
-
-msgid "Move to next unresolved discussion"
-msgstr ""
-
-msgid "Move to previous file"
-msgstr ""
-
-msgid "Move to previous unresolved discussion"
-msgstr ""
-
msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
msgstr ""
@@ -7326,9 +7369,6 @@ msgstr ""
msgid "Network"
msgstr ""
-msgid "Network Graph"
-msgstr ""
-
msgid "Never"
msgstr ""
@@ -7451,6 +7491,12 @@ msgstr ""
msgid "Next"
msgstr ""
+msgid "Next file in diff (MRs only)"
+msgstr ""
+
+msgid "Next unresolved discussion (MRs only)"
+msgstr ""
+
msgid "Nickname"
msgstr ""
@@ -8446,6 +8492,12 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
+msgid "Previous file in diff (MRs only)"
+msgstr ""
+
+msgid "Previous unresolved discussion (MRs only)"
+msgstr ""
+
msgid "Prioritize"
msgstr ""
@@ -8824,10 +8876,7 @@ msgstr ""
msgid "Project Badges"
msgstr ""
-msgid "Project File"
-msgstr ""
-
-msgid "Project Files browsing"
+msgid "Project Files"
msgstr ""
msgid "Project ID"
@@ -9621,9 +9670,6 @@ msgstr ""
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
-msgid "Reply (quoting selected text)"
-msgstr ""
-
msgid "Reply by email"
msgstr ""
@@ -9672,6 +9718,9 @@ msgstr ""
msgid "Repository"
msgstr ""
+msgid "Repository Graph"
+msgstr ""
+
msgid "Repository Settings"
msgstr ""
@@ -10499,9 +10548,6 @@ msgstr ""
msgid "Show whitespace changes"
msgstr ""
-msgid "Show/hide this dialog"
-msgstr ""
-
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
@@ -10525,6 +10571,9 @@ msgstr ""
msgid "Sign in via 2FA code"
msgstr ""
+msgid "Sign in with Google"
+msgstr ""
+
msgid "Sign out"
msgstr ""
@@ -10936,6 +10985,9 @@ msgstr ""
msgid "Start date"
msgstr ""
+msgid "Start search"
+msgstr ""
+
msgid "Start the Runner!"
msgstr ""
@@ -10966,6 +11018,9 @@ msgstr ""
msgid "State your message to activate"
msgstr ""
+msgid "Statistics"
+msgstr ""
+
msgid "Status"
msgstr ""
@@ -12274,6 +12329,9 @@ msgstr ""
msgid "Toggle the Performance Bar"
msgstr ""
+msgid "Toggle this dialog"
+msgstr ""
+
msgid "Toggle thread"
msgstr ""
@@ -14131,7 +14189,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/package.json b/package.json
index 23e611ae6cc..4256b8bfdcc 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,8 @@
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.5.5",
- "@gitlab/svgs": "^1.71.0",
- "@gitlab/ui": "5.21.0",
+ "@gitlab/svgs": "^1.72.0",
+ "@gitlab/ui": "5.21.1",
"@gitlab/visual-review-tools": "1.0.1",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
@@ -197,6 +197,7 @@
"stylelint": "^10.1.0",
"stylelint-config-recommended": "^2.2.0",
"stylelint-scss": "^3.9.2",
+ "timezone-mock": "^1.0.8",
"vue-jest": "^4.0.0-beta.2",
"webpack-dev-server": "^3.1.14",
"yarn-deduplicate": "^1.1.1"
diff --git a/qa/README.md b/qa/README.md
index dede3cd2473..332e5c8170f 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -36,7 +36,7 @@ using `package-and-qa-manual` manual action, to test if everything works fine.
You can use GitLab QA to exercise tests on any live instance! If you don't
have an instance available you can follow the instructions below to use
-the [GitLab Development Kit (GDK)][GDK].
+the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit).
This is the recommended option if you would like to contribute to the tests.
Note: GitLab QA uses [Selenium WebDriver](https://www.seleniumhq.org/) via
@@ -146,8 +146,6 @@ directory** (one level up from this directory):
docker build -t gitlab/gitlab-ce-qa:nightly --file ./qa/Dockerfile ./
```
-[GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/
-
### Quarantined tests
Tests can be put in quarantine by assigning `:quarantine` metadata. This means
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index e3039149ab4..4676dc8d077 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -61,6 +61,10 @@ module QA
end
end
+ def sign_out_if_signed_in
+ sign_out if has_personal_area?(wait: 0)
+ end
+
def click_settings_link
retry_until(reload: false) do
within_user_menu do
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index 67459208c8d..f877ba76b38 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -64,3 +64,5 @@ module QA
end
end
end
+
+QA::Page::MergeRequest::New.prepend_if_ee('QA::EE::Page::MergeRequest::New')
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index 44d9dc8f296..b5beba64c61 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -10,6 +10,7 @@ module QA
end
attribute :id
+ attribute :name
def initialize
@path = Runtime::Namespace.name
@@ -47,6 +48,11 @@ module QA
super
end
+ def add_member(user, access_level = '30')
+ # 30 = developer access
+ post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
+ end
+
def api_get_path
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index 45ab2396a04..53126c67ba3 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -5,7 +5,8 @@ require 'securerandom'
module QA
module Resource
class MergeRequest < Base
- attr_accessor :id,
+ attr_accessor :approval_rules,
+ :id,
:title,
:description,
:source_branch,
@@ -46,6 +47,7 @@ module QA
end
def initialize
+ @approval_rules = nil
@title = 'QA test - merge request'
@description = 'This is a test merge request'
@source_branch = "qa-test-feature-#{SecureRandom.hex(8)}"
@@ -63,16 +65,17 @@ module QA
project.visit!
Page::Project::Show.perform(&:new_merge_request)
- Page::MergeRequest::New.perform do |page|
- page.fill_title(@title)
- page.fill_description(@description)
- page.choose_milestone(@milestone) if @milestone
- page.assign_to_me if @assignee == 'me'
+ Page::MergeRequest::New.perform do |new|
+ new.fill_title(@title)
+ new.fill_description(@description)
+ new.choose_milestone(@milestone) if @milestone
+ new.assign_to_me if @assignee == 'me'
labels.each do |label|
- page.select_label(label)
+ new.select_label(label)
end
+ new.add_approval_rules(approval_rules) if approval_rules
- page.create_merge_request
+ new.create_merge_request
end
end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 4a29a14c5c2..157064dfe37 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -75,6 +75,11 @@ module QA
super
end
+ def add_member(user, access_level = '30')
+ # 30 = developer access
+ post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
+ end
+
def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}"
end
@@ -83,6 +88,10 @@ module QA
"#{api_get_path}/repository/archive.#{type}"
end
+ def api_members_path
+ "#{api_get_path}/members"
+ end
+
def api_post_path
'/projects'
end
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
index eec46f46d99..911d2b2f506 100644
--- a/qa/qa/resource/user.rb
+++ b/qa/qa/resource/user.rb
@@ -9,6 +9,7 @@ module QA
attr_writer :username, :password
attr_accessor :provider, :extern_uid
+ attribute :id
attribute :name
attribute :email
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/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/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/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 35487682462..5d87dbdee8b 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -74,17 +74,19 @@ describe RegistrationsController do
end
context 'when reCAPTCHA is enabled' do
- def fail_recaptcha
- # Without this, `verify_recaptcha` arbitrarily returns true in test env
- Recaptcha.configuration.skip_verify_env.delete('test')
- end
-
before do
stub_application_setting(recaptcha_enabled: true)
end
+ after do
+ # Avoid test ordering issue and ensure `verify_recaptcha` returns true
+ unless Recaptcha.configuration.skip_verify_env.include?('test')
+ Recaptcha.configuration.skip_verify_env << 'test'
+ end
+ end
+
it 'displays an error when the reCAPTCHA is not solved' do
- fail_recaptcha
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
post(:create, params: user_params)
@@ -93,11 +95,6 @@ describe RegistrationsController do
end
it 'redirects to the dashboard when the recaptcha is solved' do
- # Avoid test ordering issue and ensure `verify_recaptcha` returns true
- unless Recaptcha.configuration.skip_verify_env.include?('test')
- Recaptcha.configuration.skip_verify_env << 'test'
- end
-
post(:create, params: user_params)
expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
@@ -105,7 +102,6 @@ describe RegistrationsController do
it 'does not require reCAPTCHA if disabled by feature flag' do
stub_feature_flags(registrations_recaptcha: false)
- fail_recaptcha
post(:create, params: user_params)
diff --git a/spec/factories/external_pull_requests.rb b/spec/factories/external_pull_requests.rb
new file mode 100644
index 00000000000..08d0fa4d419
--- /dev/null
+++ b/spec/factories/external_pull_requests.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :external_pull_request do
+ sequence(:pull_request_iid)
+ project
+ source_branch 'feature'
+ source_repository 'the-repository'
+ source_sha '97de212e80737a608d939f648d959671fb0a0142'
+ target_branch 'master'
+ target_repository 'the-repository'
+ target_sha 'a09386439ca39abe575675ffd4b89ae824fec22f'
+ status :open
+
+ trait(:closed) { status 'closed' }
+ end
+end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index ee5be82cd19..ae3988bdd69 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -271,5 +271,88 @@ ZDXgrA==
auto_ssl_enabled { true }
certificate_source { :gitlab_provided }
end
+
+ trait :explicit_ecdsa do
+ certificate '-----BEGIN CERTIFICATE-----
+MIID1zCCAzkCCQDatOIwBlktwjAKBggqhkjOPQQDAjBPMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCTlkxCzAJBgNVBAcMAk5ZMQswCQYDVQQLDAJJVDEZMBcGA1UEAwwQ
+dGVzdC1jZXJ0aWZpY2F0ZTAeFw0xOTA4MjkxMTE1NDBaFw0yMTA4MjgxMTE1NDBa
+ME8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTELMAkGA1UEBwwCTlkxCzAJBgNV
+BAsMAklUMRkwFwYDVQQDDBB0ZXN0LWNlcnRpZmljYXRlMIICXDCCAc8GByqGSM49
+AgEwggHCAgEBME0GByqGSM49AQECQgH/////////////////////////////////
+/////////////////////////////////////////////////////zCBngRCAf//
+////////////////////////////////////////////////////////////////
+///////////////////8BEFRlT65YY4cmh+SmiGgtoVA7qLacluZsxXzuLSJkY7x
+CeFWGTlR7H6TexZSwL07sb8HNXPfiD0sNPHvRR/Ua1A/AAMVANCeiAApHLhTlsxn
+FzkyhKqg2mS6BIGFBADGhY4GtwQE6c2ePstmI5W0QpxkgTkFP7Uh+CivYGtNPbqh
+S1537+dZKP4dwSei/6jeM0izwYVqQpv5fn4xwuW9ZgEYOSlqeJo7wARcil+0LH0b
+2Zj1RElXm0RoF6+9Fyc+ZiyX7nKZXvQmQMVQuQE/rQdhNTxwhqJywkCIvpR2n9Fm
+UAJCAf//////////////////////////////////////////+lGGh4O/L5Zrf8wB
+SPcJpdA7tcm4iZxHrrtvtx6ROGQJAgEBA4GGAAQBVG/4c/hgl36toHj+eGL4pqv7
+l7M+ZKQJ4vz0Y9E6xIx+gvfVaZ58krmbBAP53ikwneQbFdcvw3L/ACPEib/qWjkB
+ogykguy3OwHtKLYNnDWIsfiLumEjElhcBMZVXiXhb5txf11uXAWn5n6Qhey5YKPM
+NjLLqDqaG19efCLCd21A0TcwCgYIKoZIzj0EAwIDgYsAMIGHAkEm68kYFVnN1c2N
+OjSJpIDdFWGVYJHyMDI5WgQyhm4hAioXJ0T22Zab8Wmq+hBYRJNcHoaV894blfqR
+V3ZJgam8EQJCAcnPpJQ0IqoT1pAQkaL3+Ka8ZaaCd6/8RnoDtGvWljisuyH65SRu
+kmYv87bZe1KqOZDoaDBdfVsoxcGbik19lBPV
+-----END CERTIFICATE-----'
+
+ key '-----BEGIN EC PARAMETERS-----
+MIIBwgIBATBNBgcqhkjOPQEBAkIB////////////////////////////////////
+//////////////////////////////////////////////////8wgZ4EQgH/////
+////////////////////////////////////////////////////////////////
+/////////////////ARBUZU+uWGOHJofkpohoLaFQO6i2nJbmbMV87i0iZGO8Qnh
+Vhk5Uex+k3sWUsC9O7G/BzVz34g9LDTx70Uf1GtQPwADFQDQnogAKRy4U5bMZxc5
+MoSqoNpkugSBhQQAxoWOBrcEBOnNnj7LZiOVtEKcZIE5BT+1Ifgor2BrTT26oUte
+d+/nWSj+HcEnov+o3jNIs8GFakKb+X5+McLlvWYBGDkpaniaO8AEXIpftCx9G9mY
+9URJV5tEaBevvRcnPmYsl+5ymV70JkDFULkBP60HYTU8cIaicsJAiL6Udp/RZlAC
+QgH///////////////////////////////////////////pRhoeDvy+Wa3/MAUj3
+CaXQO7XJuImcR667b7cekThkCQIBAQ==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MIICnQIBAQRCAZZRG4FJO+OK29ygycrNzjxQDB+dp+QPo1Pk6RAl5PcraohyhFnI
+MGUL4ba1efZUxCbAWxjVRSi7QEUNYCCdUPAtoIIBxjCCAcICAQEwTQYHKoZIzj0B
+AQJCAf//////////////////////////////////////////////////////////
+////////////////////////////MIGeBEIB////////////////////////////
+//////////////////////////////////////////////////////////wEQVGV
+PrlhjhyaH5KaIaC2hUDuotpyW5mzFfO4tImRjvEJ4VYZOVHsfpN7FlLAvTuxvwc1
+c9+IPSw08e9FH9RrUD8AAxUA0J6IACkcuFOWzGcXOTKEqqDaZLoEgYUEAMaFjga3
+BATpzZ4+y2YjlbRCnGSBOQU/tSH4KK9ga009uqFLXnfv51ko/h3BJ6L/qN4zSLPB
+hWpCm/l+fjHC5b1mARg5KWp4mjvABFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfu
+cple9CZAxVC5AT+tB2E1PHCGonLCQIi+lHaf0WZQAkIB////////////////////
+///////////////////////6UYaHg78vlmt/zAFI9wml0Du1ybiJnEeuu2+3HpE4
+ZAkCAQGhgYkDgYYABAFUb/hz+GCXfq2geP54Yvimq/uXsz5kpAni/PRj0TrEjH6C
+99VpnnySuZsEA/neKTCd5BsV1y/Dcv8AI8SJv+paOQGiDKSC7Lc7Ae0otg2cNYix
++Iu6YSMSWFwExlVeJeFvm3F/XW5cBafmfpCF7Llgo8w2MsuoOpobX158IsJ3bUDR
+Nw==
+-----END EC PRIVATE KEY-----'
+ end
+
+ trait :ecdsa do
+ certificate '-----BEGIN CERTIFICATE-----
+MIIB8zCCAVUCCQCGKuPQ6SBxUTAKBggqhkjOPQQDAjA+MQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw
+HhcNMTkwOTAyMDkyMDUxWhcNMjEwOTAxMDkyMDUxWjA+MQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCVVMxCzAJBgNVBAcMAlVTMRUwEwYDVQQDDAxzaHVzaGxpbi5kZXYw
+gZswEAYHKoZIzj0CAQYFK4EEACMDgYYABAH9Jd7ZWnTasgltZRbIMreihycOh/G4
+TXpkp8tTtEsuD+sh8au3Jywsi89RSZ6vgVoCY7//DQ2vamYnyBZqbL+cTQBsQ7wD
+UEaSyP0R3P4b6Ox347pYzXwSdSOra9Cm4TMQe+prVMesxulqIm7G7CTI+9J8LHlJ
+z0wUDQz/o+tUSYwv6zAKBggqhkjOPQQDAgOBiwAwgYcCQUOlTnn2QP/uYSh1dUSl
+R9WYUg5+PQMg7kS+4K/5+5gonWCvaMcP+2P7hltUcvq41l3uMKKCZRU/x60/FMHc
+1ZXdAkIBuVtm9RJXziNOKS4TcpH9os/FuREW8YQlpec58LDZdlivcHnikHZ4LCri
+T7zu3VY6Rq+V/IKpsQwQjmoTJ0IpCM8=
+-----END CERTIFICATE-----'
+
+ key '-----BEGIN EC PARAMETERS-----
+BgUrgQQAIw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MIHbAgEBBEFa72+eREW25IHbke0TiWFdW1R1ad9Nyqaz7CDtv5Kqdgd6Kcl8V2az
+Lr6z1PS+JSERWzRP+fps7kdFRrtqy/ECpKAHBgUrgQQAI6GBiQOBhgAEAf0l3tla
+dNqyCW1lFsgyt6KHJw6H8bhNemSny1O0Sy4P6yHxq7cnLCyLz1FJnq+BWgJjv/8N
+Da9qZifIFmpsv5xNAGxDvANQRpLI/RHc/hvo7HfjuljNfBJ1I6tr0KbhMxB76mtU
+x6zG6WoibsbsJMj70nwseUnPTBQNDP+j61RJjC/r
+-----END EC PRIVATE KEY-----'
+ end
end
end
diff --git a/spec/features/admin/clusters/applications_spec.rb b/spec/features/admin/clusters/applications_spec.rb
new file mode 100644
index 00000000000..8310811b43d
--- /dev/null
+++ b/spec/features/admin/clusters/applications_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
+
+describe 'Instance-level Cluster Applications', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:user) { create(:admin) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'Installing applications' do
+ include_examples "installing applications on a cluster" do
+ let(:cluster_path) { admin_cluster_path(cluster) }
+ let(:cluster_factory_args) { [:instance] }
+ end
+ end
+end
diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb
index e204e0a515d..6cb345c5066 100644
--- a/spec/features/admin/dashboard_spec.rb
+++ b/spec/features/admin/dashboard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'admin visits dashboard' do
+describe 'admin visits dashboard', :js do
include ProjectForksHelper
before do
diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb
new file mode 100644
index 00000000000..cb8fd8c607c
--- /dev/null
+++ b/spec/features/clusters/installing_applications_shared_examples.rb
@@ -0,0 +1,228 @@
+# frozen_string_literal: true
+
+shared_examples "installing applications on a cluster" do
+ before do
+ visit cluster_path
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, *cluster_factory_args) }
+
+ it 'user is unable to install applications' do
+ expect(page).not_to have_text('Helm')
+ expect(page).not_to have_text('Install')
+ end
+ end
+
+ context 'when cluster is created' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) }
+
+ it 'user can install applications' do
+ wait_for_requests
+
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
+ end
+ end
+
+ context 'when user installs Helm' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+
+ wait_for_requests
+ end
+
+ it 'shows the status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
+ expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
+ expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
+ end
+ end
+
+ context 'when user installs Knative' do
+ before do
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+ end
+
+ context 'on an abac cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, *cluster_factory_args) }
+
+ it 'shows info block and not be installable' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).to have_css('.rbac-notice')
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ end
+ end
+ end
+
+ context 'on an rbac cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) }
+
+ it 'does not show callout block and be installable' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).not_to have_css('p', text: 'You must have an RBAC-enabled cluster', visible: :all)
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+ end
+ end
+
+ describe 'when user clicks install button' do
+ def domainname_form_value
+ page.find('.js-knative-domainname').value
+ end
+
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+
+ page.find('.js-knative-domainname').set("domain.example.org")
+
+ click_button 'Install'
+
+ wait_for_requests
+
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_knative.make_installing!
+ Clusters::Cluster.last.application_knative.make_installed!
+ Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1')
+ end
+ end
+
+ it 'shows status transition' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(domainname_form_value).to eq('domain.example.org')
+ expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster')
+ expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes'
+ end
+
+ it 'can then update the domain' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(ClusterPatchAppWorker).to receive(:perform_async)
+
+ expect(domainname_form_value).to eq('domain.example.org')
+
+ page.find('.js-knative-domainname').set("new.domain.example.org")
+
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(domainname_form_value).to eq('new.domain.example.org')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user installs Cert Manager' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ click_button 'Install'
+ end
+ end
+
+ it 'shows status transition' do
+ def email_form_value
+ page.find('.js-email').value
+ end
+
+ page.within('.js-cluster-application-row-cert_manager') do
+ expect(email_form_value).to eq(cluster.user.email)
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ page.find('.js-email').set("new_email@example.org")
+ Clusters::Cluster.last.application_cert_manager.make_installing!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_cert_manager.make_installed!
+
+ expect(email_form_value).to eq('new_email@example.org')
+ expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
+ end
+ end
+
+ context 'when user installs Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-ingress') do
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+ page.find(:css, '.js-cluster-application-install-button').click
+
+ wait_for_requests
+ end
+ end
+
+ it 'shows the status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
+
+ # The application becomes installed but we keep waiting for external IP address
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed')
+ expect(page).to have_selector('.js-no-endpoint-message')
+ expect(page).to have_selector('.js-ingress-ip-loading-icon')
+
+ # We receive the external IP address and display
+ Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
+
+ expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
+ expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
+ expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
+ expect(page).not_to have_css('p', text: 'The endpoint is in the process of being assigned', visible: :all)
+ expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/clusters/applications_spec.rb b/spec/features/groups/clusters/applications_spec.rb
new file mode 100644
index 00000000000..5d48df234eb
--- /dev/null
+++ b/spec/features/groups/clusters/applications_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
+
+describe 'Group-level Cluster Applications', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'Installing applications' do
+ include_examples "installing applications on a cluster" do
+ let(:cluster_path) { group_cluster_path(group, cluster) }
+ let(:cluster_factory_args) { [:group, groups: [group]] }
+ end
+ end
+end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 435b3cd2555..7d89b8e97a6 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
describe 'Merge request > User posts notes', :js do
include NoteInteractionHelpers
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
+
let(:user) { project.creator }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
@@ -33,17 +34,21 @@ describe 'Merge request > User posts notes', :js do
end
describe 'with text' do
+ let(:text) { 'This is awesome' }
+
before do
page.within('.js-main-target-form') do
- fill_in 'note[note]', with: 'This is awesome'
+ fill_in 'note[note]', with: text
end
end
- it 'has enable submit button and preview button' do
+ it 'has enable submit button, preview button and saves content to local storage' do
page.within('.js-main-target-form') do
expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
end
+
+ expect(page.evaluate_script("localStorage['autosave/Note/MergeRequest/#{merge_request.id}']")).to eq(text)
end
end
end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 6262f1ce055..c42eb8560a4 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -64,7 +64,7 @@ describe 'Merge request > User selects branches for new MR', :js do
click_button "Check out branch"
- expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
+ expect(page).to have_content 'git checkout -b "orphaned-branch" "origin/orphaned-branch"'
end
it 'allows filtering multiple dropdowns' do
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 3d15095e2da..ce971b158a3 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
+require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
-describe 'Clusters Applications', :js do
+describe 'Project-level Cluster Applications', :js do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
@@ -14,229 +15,9 @@ describe 'Clusters Applications', :js do
end
describe 'Installing applications' do
- before do
- visit project_cluster_path(project, cluster)
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- it 'user is unable to install applications' do
- expect(page).not_to have_css('.js-cluster-application-row-helm')
- expect(page).not_to have_css('.js-cluster-application-install-button')
- end
- end
-
- context 'when cluster is created' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it 'user can install applications' do
- wait_for_requests
-
- page.within('.js-cluster-application-row-helm') do
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
- end
- end
-
- context 'when user installs Helm' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-helm') do
- page.find(:css, '.js-cluster-application-install-button').click
- end
-
- wait_for_requests
- end
-
- it 'they see status transition' do
- page.within('.js-cluster-application-row-helm') do
- # FE sends request and gets the response, then the buttons is "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_helm.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_helm.make_installed!
-
- expect(page).not_to have_css('.js-cluster-application-install-button')
- expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
- end
- end
-
- context 'when user installs Knative' do
- before do
- create(:clusters_applications_helm, :installed, cluster: cluster)
- end
-
- context 'on an abac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) }
-
- it 'shows info block and not be installable' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.rbac-notice')
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- end
- end
- end
-
- context 'on an rbac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it 'does not show callout block and be installable' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).not_to have_css('.rbac-notice')
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
- end
- end
-
- describe 'when user clicks install button' do
- def domainname_form_value
- page.find('.js-knative-domainname').value
- end
-
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
-
- page.find('.js-knative-domainname').set("domain.example.org")
-
- click_button 'Install'
-
- wait_for_requests
-
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_knative.make_installing!
- Clusters::Cluster.last.application_knative.make_installed!
- Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1')
- end
- end
-
- it 'shows status transition' do
- page.within('.js-cluster-application-row-knative') do
- expect(domainname_form_value).to eq('domain.example.org')
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster')
- expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes'
- end
-
- it 'can then update the domain' do
- page.within('.js-cluster-application-row-knative') do
- expect(ClusterPatchAppWorker).to receive(:perform_async)
-
- expect(domainname_form_value).to eq('domain.example.org')
-
- page.find('.js-knative-domainname').set("new.domain.example.org")
-
- click_button 'Save changes'
-
- wait_for_requests
-
- expect(domainname_form_value).to eq('new.domain.example.org')
- end
- end
- end
- end
- end
-
- context 'when user installs Cert Manager' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- create(:clusters_applications_helm, :installed, cluster: cluster)
-
- page.within('.js-cluster-application-row-cert_manager') do
- click_button 'Install'
- end
- end
-
- it 'shows status transition' do
- def email_form_value
- page.find('.js-email').value
- end
-
- page.within('.js-cluster-application-row-cert_manager') do
- expect(email_form_value).to eq(cluster.user.email)
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- page.find('.js-email').set("new_email@example.org")
- Clusters::Cluster.last.application_cert_manager.make_installing!
-
- expect(email_form_value).to eq('new_email@example.org')
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_cert_manager.make_installed!
-
- expect(email_form_value).to eq('new_email@example.org')
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
- end
- end
-
- context 'when user installs Ingress' do
- context 'when user installs application: Ingress' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- create(:clusters_applications_helm, :installed, cluster: cluster)
-
- page.within('.js-cluster-application-row-ingress') do
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
- page.find(:css, '.js-cluster-application-install-button').click
-
- wait_for_requests
- end
- end
-
- it 'they see status transition' do
- page.within('.js-cluster-application-row-ingress') do
- # FE sends request and gets the response, then the buttons is "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_ingress.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- # The application becomes installed but we keep waiting for external IP address
- Clusters::Cluster.last.application_ingress.make_installed!
-
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed')
- expect(page).to have_selector('.js-no-endpoint-message')
- expect(page).to have_selector('.js-ingress-ip-loading-icon')
-
- # We receive the external IP address and display
- Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
-
- expect(page).not_to have_css('.js-cluster-application-install-button')
- expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
- expect(page).not_to have_selector('.js-no-endpoint-message')
- expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
- end
-
- expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
- end
- end
- end
+ include_examples "installing applications on a cluster" do
+ let(:cluster_path) { project_cluster_path(project, cluster) }
+ let(:cluster_factory_args) { [projects: [project]] }
end
end
end
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/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 879ff01f294..ef8749be0be 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -42,6 +42,24 @@ describe IssuesFinder do
end
end
+ context 'filtering by projects' do
+ context 'when projects are passed in a list of ids' do
+ let(:params) { { projects: [project1.id] } }
+
+ it 'returns the issue belonging to the projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'when projects are passed in a subquery' do
+ let(:params) { { projects: Project.id_in(project1.id) } }
+
+ it 'returns the issue belonging to the projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+ end
+
context 'filtering by group_id' do
let(:params) { { group_id: group.id } }
@@ -49,6 +67,30 @@ describe IssuesFinder do
it 'returns all group issues' do
expect(issues).to contain_exactly(issue1)
end
+
+ context 'when projects outside the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id] } }
+
+ it 'returns no issues' do
+ expect(issues).to be_empty
+ end
+ end
+
+ context 'when projects of the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project1.id] } }
+
+ it 'returns the issue within the group and projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'when projects of the group are passed as a subquery' do
+ let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } }
+
+ it 'returns the issue within the group and projects' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
end
context 'when include_subgroup param is true' do
@@ -59,6 +101,14 @@ describe IssuesFinder do
it 'returns all group and subgroup issues' do
expect(issues).to contain_exactly(issue1, issue4)
end
+
+ context 'when mixed projects are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } }
+
+ it 'returns the issue within the group and projects' do
+ expect(issues).to contain_exactly(issue4)
+ end
+ end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 78224f0b9da..6c0bbeff4f4 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5)
end
- it 'filters by project' do
+ it 'filters by project_id' do
params = { project_id: project1.id, scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute
@@ -21,6 +21,14 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1)
end
+ it 'filters by projects' do
+ params = { projects: [project2.id, project3.id] }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3, merge_request4)
+ end
+
it 'filters by commit sha' do
merge_requests = described_class.new(
user,
@@ -49,6 +57,16 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request5)
end
+
+ it 'filters by group projects including subgroups' do
+ # project3 is not in the group, so it should not return merge_request4
+ projects = [project3.id, project4.id]
+ params = { group_id: group.id, include_subgroups: true, projects: projects }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request5)
+ end
end
it 'filters by non_archived' do
diff --git a/spec/fixtures/api/schemas/entities/merge_request_noteable.json b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
index 88b0fecc24c..d37f5b864d7 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_noteable.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_noteable.json
@@ -1,6 +1,10 @@
{
"type": "object",
"properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": "string" },
"merge_params": { "type": ["object", "null"] },
"state": { "type": "string" },
"source_branch": { "type": "string" },
diff --git a/spec/fixtures/api/schemas/statistics.json b/spec/fixtures/api/schemas/statistics.json
new file mode 100644
index 00000000000..ef2f39aad9d
--- /dev/null
+++ b/spec/fixtures/api/schemas/statistics.json
@@ -0,0 +1,29 @@
+{
+ "type": "object",
+ "required" : [
+ "forks",
+ "issues",
+ "merge_requests",
+ "notes",
+ "snippets",
+ "ssh_keys",
+ "milestones",
+ "users",
+ "projects",
+ "groups",
+ "active_users"
+ ],
+ "properties" : {
+ "forks": { "type": "string" },
+ "issues'": { "type": "string" },
+ "merge_requests'": { "type": "string" },
+ "notes'": { "type": "string" },
+ "snippets'": { "type": "string" },
+ "ssh_keys'": { "type": "string" },
+ "milestones'": { "type": "string" },
+ "users'": { "type": "string" },
+ "projects'": { "type": "string" },
+ "groups'": { "type": "string" },
+ "active_users'": { "type": "string" }
+ }
+}
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
new file mode 100644
index 00000000000..25b1d432e2d
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -0,0 +1,73 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import StatisticsPanelApp from '~/admin/statistics_panel/components/app.vue';
+import statisticsLabels from '~/admin/statistics_panel/constants';
+import createStore from '~/admin/statistics_panel/store';
+import { GlLoadingIcon } from '@gitlab/ui';
+import mockStatistics from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Admin statistics app', () => {
+ let wrapper;
+ let store;
+ let axiosMock;
+
+ const createComponent = () => {
+ wrapper = shallowMount(StatisticsPanelApp, {
+ localVue,
+ store,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(200);
+ store = createStore();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findStats = idx => wrapper.findAll('.js-stats').at(idx);
+
+ describe('template', () => {
+ describe('when app is loading', () => {
+ it('renders a loading indicator', () => {
+ store.dispatch('requestStatistics');
+ createComponent();
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when app has finished loading', () => {
+ const statistics = convertObjectPropsToCamelCase(mockStatistics, { deep: true });
+
+ it.each`
+ statistic | count | index
+ ${'forks'} | ${12} | ${0}
+ ${'issues'} | ${180} | ${1}
+ ${'mergeRequests'} | ${31} | ${2}
+ ${'notes'} | ${986} | ${3}
+ ${'snippets'} | ${50} | ${4}
+ ${'sshKeys'} | ${10} | ${5}
+ ${'milestones'} | ${40} | ${6}
+ ${'activeUsers'} | ${50} | ${7}
+ `('renders the count for the $statistic statistic', ({ statistic, count, index }) => {
+ const label = statisticsLabels[statistic];
+ store.dispatch('receiveStatisticsSuccess', statistics);
+ createComponent();
+
+ expect(findStats(index).text()).toContain(label);
+ expect(findStats(index).text()).toContain(count);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/statistics_panel/mock_data.js b/spec/frontend/admin/statistics_panel/mock_data.js
new file mode 100644
index 00000000000..6d861059dfd
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/mock_data.js
@@ -0,0 +1,15 @@
+const mockStatistics = {
+ forks: 12,
+ issues: 180,
+ merge_requests: 31,
+ notes: 986,
+ snippets: 50,
+ ssh_keys: 10,
+ milestones: 40,
+ users: 50,
+ projects: 29,
+ groups: 9,
+ active_users: 50,
+};
+
+export default mockStatistics;
diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js
new file mode 100644
index 00000000000..9b18b1aebda
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js
@@ -0,0 +1,115 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as actions from '~/admin/statistics_panel/store/actions';
+import * as types from '~/admin/statistics_panel/store/mutation_types';
+import getInitialState from '~/admin/statistics_panel/store/state';
+import mockStatistics from '../mock_data';
+
+describe('Admin statistics panel actions', () => {
+ let mock;
+ let state;
+
+ beforeEach(() => {
+ state = getInitialState();
+ mock = new MockAdapter(axios);
+ });
+
+ describe('fetchStatistics', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(200, mockStatistics);
+ });
+
+ it('dispatches success with received data', done =>
+ testAction(
+ actions.fetchStatistics,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestStatistics' },
+ {
+ type: 'receiveStatisticsSuccess',
+ payload: expect.objectContaining(
+ convertObjectPropsToCamelCase(mockStatistics, { deep: true }),
+ ),
+ },
+ ],
+ done,
+ ));
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(500);
+ });
+
+ it('dispatches error', done =>
+ testAction(
+ actions.fetchStatistics,
+ null,
+ state,
+ [],
+ [
+ {
+ type: 'requestStatistics',
+ },
+ {
+ type: 'receiveStatisticsError',
+ payload: new Error('Request failed with status code 500'),
+ },
+ ],
+ done,
+ ));
+ });
+ });
+
+ describe('requestStatistic', () => {
+ it('should commit the request mutation', done =>
+ testAction(
+ actions.requestStatistics,
+ null,
+ state,
+ [{ type: types.REQUEST_STATISTICS }],
+ [],
+ done,
+ ));
+ });
+
+ describe('receiveStatisticsSuccess', () => {
+ it('should commit received data', done =>
+ testAction(
+ actions.receiveStatisticsSuccess,
+ mockStatistics,
+ state,
+ [
+ {
+ type: types.RECEIVE_STATISTICS_SUCCESS,
+ payload: mockStatistics,
+ },
+ ],
+ [],
+ done,
+ ));
+ });
+
+ describe('receiveStatisticsError', () => {
+ it('should commit error', done => {
+ testAction(
+ actions.receiveStatisticsError,
+ 500,
+ state,
+ [
+ {
+ type: types.RECEIVE_STATISTICS_ERROR,
+ payload: 500,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/statistics_panel/store/getters_spec.js b/spec/frontend/admin/statistics_panel/store/getters_spec.js
new file mode 100644
index 00000000000..152d82531ed
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/store/getters_spec.js
@@ -0,0 +1,48 @@
+import createState from '~/admin/statistics_panel/store/state';
+import * as getters from '~/admin/statistics_panel/store/getters';
+
+describe('Admin statistics panel getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('getStatistics', () => {
+ describe('when statistics data exists', () => {
+ it('returns an array of statistics objects with key, label and value', () => {
+ state.statistics = { forks: 10, issues: 20 };
+
+ const statisticsLabels = {
+ forks: 'Forks',
+ issues: 'Issues',
+ };
+
+ const statisticsData = [
+ { key: 'forks', label: 'Forks', value: 10 },
+ { key: 'issues', label: 'Issues', value: 20 },
+ ];
+
+ expect(getters.getStatistics(state)(statisticsLabels)).toEqual(statisticsData);
+ });
+ });
+
+ describe('when no statistics data exists', () => {
+ it('returns an array of statistics objects with key, label and sets value to null', () => {
+ state.statistics = null;
+
+ const statisticsLabels = {
+ forks: 'Forks',
+ issues: 'Issues',
+ };
+
+ const statisticsData = [
+ { key: 'forks', label: 'Forks', value: null },
+ { key: 'issues', label: 'Issues', value: null },
+ ];
+
+ expect(getters.getStatistics(state)(statisticsLabels)).toEqual(statisticsData);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
new file mode 100644
index 00000000000..179f38d2bc5
--- /dev/null
+++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
@@ -0,0 +1,41 @@
+import mutations from '~/admin/statistics_panel/store/mutations';
+import * as types from '~/admin/statistics_panel/store/mutation_types';
+import getInitialState from '~/admin/statistics_panel/store/state';
+import mockStatistics from '../mock_data';
+
+describe('Admin statistics panel mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = getInitialState();
+ });
+
+ describe(`${types.REQUEST_STATISTICS}`, () => {
+ it('sets isLoading to true', () => {
+ mutations[types.REQUEST_STATISTICS](state);
+
+ expect(state.isLoading).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_STATISTICS_SUCCESS}`, () => {
+ it('updates the store with the with statistics', () => {
+ mutations[types.RECEIVE_STATISTICS_SUCCESS](state, mockStatistics);
+
+ expect(state.isLoading).toBe(false);
+ expect(state.error).toBe(null);
+ expect(state.statistics).toEqual(mockStatistics);
+ });
+ });
+
+ describe(`${types.RECEIVE_STATISTICS_ERROR}`, () => {
+ it('sets error and clears data', () => {
+ const error = 500;
+ mutations[types.RECEIVE_STATISTICS_ERROR](state, error);
+
+ expect(state.isLoading).toBe(false);
+ expect(state.error).toBe(error);
+ expect(state.statistics).toEqual(null);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 1d8984cea0a..fbcab078993 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -89,7 +89,7 @@ describe('Applications', () => {
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
});
@@ -126,7 +126,7 @@ describe('Applications', () => {
});
it('renders a row for Knative', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-knative')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
});
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/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index 721d0b8172d..76675a78db2 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -171,6 +171,32 @@ describe('Issue boards new issue form', () => {
.then(done)
.catch(done.fail);
});
+
+ it('sets detail weight after submit', done => {
+ boardsStore.weightFeatureAvailable = true;
+ vm.title = 'submit issue';
+
+ Vue.nextTick()
+ .then(submitIssue)
+ .then(() => {
+ expect(boardsStore.detail.list.weight).toBe(list.weight);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not set detail weight after submit', done => {
+ boardsStore.weightFeatureAvailable = false;
+ vm.title = 'submit issue';
+
+ Vue.nextTick()
+ .then(submitIssue)
+ .then(() => {
+ expect(boardsStore.detail.list.weight).toBe(list.weight);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
describe('submit error', () => {
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index ea22ae5c4e7..50ad1442873 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -12,6 +12,7 @@ export const listObj = {
position: 0,
title: 'Test',
list_type: 'label',
+ weight: 3,
label: {
id: 5000,
title: 'Testing',
@@ -26,6 +27,7 @@ export const listObjDuplicate = {
position: 1,
title: 'Test',
list_type: 'label',
+ weight: 3,
label: {
id: listObj.label.id,
title: 'Testing',
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
index 43c5d3ec980..8fc1e0a4e88 100644
--- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -84,6 +84,20 @@ describe Gitlab::Ci::Build::Policy::Refs do
.not_to be_satisfied_by(pipeline)
end
end
+
+ context 'when source is external_pull_request_event' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :external_pull_request_event) }
+
+ it 'is satisfied with only: external_pull_request' do
+ expect(described_class.new(%w[external_pull_requests]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied with only: external_pull_request_event' do
+ expect(described_class.new(%w[external_pull_request_events]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
end
context 'when matching a ref by a regular expression' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index bf9ff922c05..ba4f841cf43 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -128,4 +128,38 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline.target_sha).to eq(merge_request.target_branch_sha)
end
end
+
+ context 'when pipeline is running for an external pull request' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :external_pull_request_event,
+ origin_ref: 'feature',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ source_sha: external_pull_request.source_sha,
+ target_sha: external_pull_request.target_sha,
+ trigger_request: nil,
+ schedule: nil,
+ external_pull_request: external_pull_request,
+ project: project,
+ current_user: user)
+ end
+
+ let(:external_pull_request) { build(:external_pull_request, project: project) }
+
+ before do
+ step.perform!
+ end
+
+ it 'correctly indicated that this is an external pull request pipeline' do
+ expect(pipeline).to be_external_pull_request_event
+ expect(pipeline.external_pull_request).to eq(external_pull_request)
+ end
+
+ it 'correctly sets source sha and target sha to pipeline' do
+ expect(pipeline.source_sha).to eq(external_pull_request.source_sha)
+ expect(pipeline.target_sha).to eq(external_pull_request.target_sha)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 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/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 710564b7540..1b4d366ce7b 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -11,16 +11,62 @@ describe Gitlab::Danger::Helper do
class FakeDanger
include Gitlab::Danger::Helper
- attr_reader :git
+ attr_reader :git, :gitlab
- def initialize(git:)
+ def initialize(git:, gitlab:)
@git = git
+ @gitlab = gitlab
end
end
let(:fake_git) { double('fake-git') }
- subject(:helper) { FakeDanger.new(git: fake_git) }
+ let(:mr_author) { nil }
+ let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) }
+
+ subject(:helper) { FakeDanger.new(git: fake_git, gitlab: fake_gitlab) }
+
+ describe '#gitlab_helper' do
+ context 'when gitlab helper is not available' do
+ let(:fake_gitlab) { nil }
+
+ it 'returns nil' do
+ expect(helper.gitlab_helper).to be_nil
+ end
+ end
+
+ context 'when gitlab helper is available' do
+ it 'returns the gitlab helper' do
+ expect(helper.gitlab_helper).to eq(fake_gitlab)
+ end
+ end
+ end
+
+ describe '#release_automation?' do
+ context 'when gitlab helper is not available' do
+ it 'returns false' do
+ expect(helper.release_automation?).to be_falsey
+ end
+ end
+
+ context 'when gitlab helper is available' do
+ context "but the MR author isn't the RELEASE_TOOLS_BOT" do
+ let(:mr_author) { 'johnmarston' }
+
+ it 'returns false' do
+ expect(helper.release_automation?).to be_falsey
+ end
+ end
+
+ context 'and the MR author is the RELEASE_TOOLS_BOT' do
+ let(:mr_author) { described_class::RELEASE_TOOLS_BOT }
+
+ it 'returns true' do
+ expect(helper.release_automation?).to be_truthy
+ end
+ end
+ end
+ end
describe '#all_changed_files' do
subject { helper.all_changed_files }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 47ba7eff8ed..dafa4243145 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -23,6 +23,7 @@ issues:
- epic_issue
- epic
- designs
+- design_versions
events:
- author
- project
@@ -126,6 +127,8 @@ merge_requests:
- blocks_as_blockee
- blocking_merge_requests
- blocked_merge_requests
+external_pull_requests:
+- project
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -155,6 +158,7 @@ ci_pipelines:
- pipeline_schedule
- merge_requests_as_head_pipeline
- merge_request
+- external_pull_request
- deployments
- environments
- chat_data
@@ -402,6 +406,7 @@ project:
- merge_trains
- designs
- project_aliases
+- external_pull_requests
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index fef84c87509..cc8ca1d87e3 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -12,7 +12,7 @@ describe 'Import/Export attribute configuration' do
let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys }
let(:relation_names) do
- names = names_from_tree(config_hash['project_tree'])
+ names = names_from_tree(config_hash.dig('tree', 'project'))
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
new file mode 100644
index 00000000000..3cbc1375d6e
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::ImportExport::AttributesFinder do
+ describe '#find_root' do
+ subject { described_class.new(config: config).find_root(model_key) }
+
+ let(:test_config) { 'spec/support/import_export/import_export.yml' }
+ let(:config) { Gitlab::ImportExport::Config.new.to_h }
+ let(:model_key) { :project }
+
+ let(:project_tree_hash) do
+ {
+ except: [:id, :created_at],
+ include: [
+ { issues: { include: [] } },
+ { labels: { include: [] } },
+ { merge_requests: {
+ except: [:iid],
+ include: [
+ { merge_request_diff: {
+ include: [],
+ preload: { source_project: nil }
+ } },
+ { merge_request_test: { include: [] } }
+ ],
+ only: [:id],
+ preload: {
+ merge_request_diff: { source_project: nil },
+ merge_request_test: nil
+ }
+ } },
+ { commit_statuses: {
+ include: [{ commit: { include: [] } }],
+ preload: { commit: nil }
+ } },
+ { project_members: {
+ include: [{ user: { include: [],
+ 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
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
+ end
+
+ it 'generates hash from project tree config' do
+ is_expected.to match(project_tree_hash)
+ end
+
+ context 'individual scenarios' do
+ it 'generates the correct hash for a single project relation' do
+ setup_yaml(tree: { project: [:issues] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a single project feature relation' do
+ setup_yaml(tree: { project: [:project_feature] })
+
+ is_expected.to match(
+ include: [{ project_feature: { include: [] } }],
+ preload: { project_feature: nil }
+ )
+ end
+
+ it 'generates the correct hash for a multiple project relation' do
+ setup_yaml(tree: { project: [:issues, :snippets] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [] } },
+ { snippets: { include: [] } }],
+ preload: { issues: nil, snippets: nil }
+ )
+ end
+
+ it 'generates the correct hash for a single sub-relation' do
+ setup_yaml(tree: { project: [issues: [:notes]] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [{ notes: { include: [] } }],
+ preload: { notes: nil } } }],
+ preload: { issues: { notes: nil } }
+ )
+ end
+
+ it 'generates the correct hash for a multiple sub-relation' do
+ setup_yaml(tree: { project: [merge_requests: [:notes, :merge_request_diff]] })
+
+ is_expected.to match(
+ include: [{ merge_requests:
+ { include: [{ notes: { include: [] } },
+ { merge_request_diff: { include: [] } }],
+ preload: { merge_request_diff: nil, notes: nil } } }],
+ preload: { merge_requests: { merge_request_diff: nil, notes: nil } }
+ )
+ end
+
+ it 'generates the correct hash for a sub-relation with another sub-relation' do
+ setup_yaml(tree: { project: [merge_requests: [notes: [:author]]] })
+
+ is_expected.to match(
+ include: [{ merge_requests: {
+ include: [{ notes: { include: [{ author: { include: [] } }],
+ preload: { author: nil } } }],
+ preload: { notes: { author: nil } }
+ } }],
+ preload: { merge_requests: { notes: { author: nil } } }
+ )
+ end
+
+ it 'generates the correct hash for a relation with included attributes' do
+ setup_yaml(tree: { project: [:issues] },
+ included_attributes: { issues: [:name, :description] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [],
+ only: [:name, :description] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a relation with excluded attributes' do
+ setup_yaml(tree: { project: [:issues] },
+ excluded_attributes: { issues: [:name] })
+
+ is_expected.to match(
+ include: [{ issues: { except: [:name],
+ include: [] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a relation with both excluded and included attributes' do
+ setup_yaml(tree: { project: [:issues] },
+ excluded_attributes: { issues: [:name] },
+ included_attributes: { issues: [:description] })
+
+ is_expected.to match(
+ include: [{ issues: { except: [:name],
+ include: [],
+ only: [:description] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ it 'generates the correct hash for a relation with custom methods' do
+ setup_yaml(tree: { project: [:issues] },
+ methods: { issues: [:name] })
+
+ is_expected.to match(
+ include: [{ issues: { include: [],
+ methods: [:name] } }],
+ preload: { issues: nil }
+ )
+ end
+
+ def setup_yaml(hash)
+ allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
+ end
+ end
+ end
+
+ describe '#find_relations_tree' do
+ subject { described_class.new(config: config).find_relations_tree(model_key) }
+
+ let(:tree) { { project: { issues: {} } } }
+ let(:model_key) { :project }
+
+ context 'when initialized with config including tree' do
+ let(:config) { { tree: tree } }
+
+ context 'when relation is in top-level keys of the tree' do
+ it { is_expected.to eq({ issues: {} }) }
+ end
+
+ context 'when the relation is not in top-level keys' do
+ let(:model_key) { :issues }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when tree is not present in config' do
+ let(:config) { {} }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#find_excluded_keys' do
+ subject { described_class.new(config: config).find_excluded_keys(klass_name) }
+
+ let(:klass_name) { 'project' }
+
+ context 'when initialized with excluded_attributes' do
+ let(:config) { { excluded_attributes: excluded_attributes } }
+ let(:excluded_attributes) { { project: [:name, :path], issues: [:milestone_id] } }
+
+ it { is_expected.to eq(%w[name path]) }
+ end
+
+ context 'when excluded_attributes are not present in config' do
+ let(:config) { {} }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/config_spec.rb b/spec/lib/gitlab/import_export/config_spec.rb
index cf396dba382..f09a29b84db 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -1,163 +1,177 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
describe Gitlab::ImportExport::Config do
let(:yaml_file) { described_class.new }
describe '#to_h' do
- context 'when using CE' do
- before do
- allow(yaml_file)
- .to receive(:merge?)
- .and_return(false)
+ subject { yaml_file.to_h }
+
+ context 'when using default config' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ee) do
+ [true, false]
end
- it 'just returns the parsed Hash without the EE section' do
- expected = YAML.load_file(Gitlab::ImportExport.config_file)
- expected.delete('ee')
+ with_them do
+ before do
+ allow(Gitlab).to receive(:ee?) { ee }
+ end
- expect(yaml_file.to_h).to eq(expected)
+ it 'parses default 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, :preloads)
+ end
end
end
- context 'when using EE' do
- before do
- allow(yaml_file)
- .to receive(:merge?)
- .and_return(true)
- end
+ context 'when using custom config' do
+ let(:config) do
+ <<-EOF.strip_heredoc
+ tree:
+ project:
+ - labels:
+ - :priorities
+ - milestones:
+ - events:
+ - :push_event_payload
- it 'merges the EE project tree into the CE project tree' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [
- {
- 'issues' => [
- :id,
- :title,
- { 'notes' => [:id, :note, { 'author' => [:name] }] }
- ]
- }
- ],
- 'ee' => {
- 'project_tree' => [
- {
- 'issues' => [
- :description,
- { 'notes' => [:date, { 'author' => [:email] }] }
- ]
- },
- { 'foo' => [{ 'bar' => %i[baz] }] }
- ]
- }
- })
+ included_attributes:
+ user:
+ - :id
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [
- {
- 'issues' => [
- :id,
- :title,
- {
- 'notes' => [
- :id,
- :note,
- { 'author' => [:name, :email] },
- :date
- ]
- },
- :description
- ]
- },
- { 'foo' => [{ 'bar' => %i[baz] }] }
- ]
- })
+ excluded_attributes:
+ project:
+ - :name
+
+ methods:
+ labels:
+ - :type
+ events:
+ - :action
+
+ preloads:
+ statuses:
+ project:
+
+ ee:
+ tree:
+ project:
+ protected_branches:
+ - :unprotect_access_levels
+ included_attributes:
+ user:
+ - :name_ee
+ excluded_attributes:
+ project:
+ - :name_without_ee
+ methods:
+ labels:
+ - :type_ee
+ events_ee:
+ - :action_ee
+ preloads:
+ statuses:
+ bridge_ee:
+ EOF
end
- it 'merges the excluded attributes list' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [],
- 'excluded_attributes' => {
- 'project' => %i[id title],
- 'notes' => %i[id]
- },
- 'ee' => {
- 'project_tree' => [],
- 'excluded_attributes' => {
- 'project' => %i[date],
- 'foo' => %i[bar baz]
- }
- }
- })
-
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [],
- 'excluded_attributes' => {
- 'project' => %i[id title date],
- 'notes' => %i[id],
- 'foo' => %i[bar baz]
- }
- })
+ let(:config_hash) { YAML.safe_load(config, [Symbol]) }
+
+ before do
+ allow_any_instance_of(described_class).to receive(:parse_yaml) do
+ config_hash.deep_dup
+ end
end
- it 'merges the included attributes list' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [],
- 'included_attributes' => {
- 'project' => %i[id title],
- 'notes' => %i[id]
- },
- 'ee' => {
- 'project_tree' => [],
- 'included_attributes' => {
- 'project' => %i[date],
- 'foo' => %i[bar baz]
+ context 'when using CE' do
+ before do
+ allow(Gitlab).to receive(:ee?) { false }
+ end
+
+ it 'just returns the normalized Hash' do
+ is_expected.to eq(
+ {
+ tree: {
+ project: {
+ labels: {
+ priorities: {}
+ },
+ milestones: {
+ events: {
+ push_event_payload: {}
+ }
+ }
+ }
+ },
+ included_attributes: {
+ user: [:id]
+ },
+ excluded_attributes: {
+ project: [:name]
+ },
+ methods: {
+ labels: [:type],
+ events: [:action]
+ },
+ preloads: {
+ statuses: {
+ project: nil
+ }
}
}
- })
-
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [],
- 'included_attributes' => {
- 'project' => %i[id title date],
- 'notes' => %i[id],
- 'foo' => %i[bar baz]
- }
- })
+ )
+ end
end
- it 'merges the methods list' do
- allow(yaml_file)
- .to receive(:parse_yaml)
- .and_return({
- 'project_tree' => [],
- 'methods' => {
- 'project' => %i[id title],
- 'notes' => %i[id]
- },
- 'ee' => {
- 'project_tree' => [],
- 'methods' => {
- 'project' => %i[date],
- 'foo' => %i[bar baz]
+ context 'when using EE' do
+ before do
+ allow(Gitlab).to receive(:ee?) { true }
+ end
+
+ it 'just returns the normalized Hash' do
+ is_expected.to eq(
+ {
+ tree: {
+ project: {
+ labels: {
+ priorities: {}
+ },
+ milestones: {
+ events: {
+ push_event_payload: {}
+ }
+ },
+ protected_branches: {
+ unprotect_access_levels: {}
+ }
+ }
+ },
+ included_attributes: {
+ user: [:id, :name_ee]
+ },
+ excluded_attributes: {
+ project: [:name, :name_without_ee]
+ },
+ methods: {
+ labels: [:type, :type_ee],
+ events: [:action],
+ events_ee: [:action_ee]
+ },
+ preloads: {
+ statuses: {
+ project: nil,
+ bridge_ee: nil
+ }
}
}
- })
-
- expect(yaml_file.to_h).to eq({
- 'project_tree' => [],
- 'methods' => {
- 'project' => %i[id title date],
- 'notes' => %i[id],
- 'foo' => %i[bar baz]
- }
- })
+ )
+ end
end
end
end
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/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
index 5ed9fef1597..3442e22c11f 100644
--- a/spec/lib/gitlab/import_export/model_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -8,7 +8,7 @@ describe 'Import/Export model configuration' do
let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys }
let(:model_names) do
- names = names_from_tree(config_hash['project_tree'])
+ names = names_from_tree(config_hash.dig('tree', 'project'))
# Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually.
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 0aef4887c75..87be7857e67 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -512,6 +512,24 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
end
end
+
+ context 'with external authorization classification labels' do
+ it 'converts empty external classification authorization labels to nil' do
+ project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } })
+
+ restored_project_json
+
+ expect(project.external_authorization_classification_label).to be_nil
+ end
+
+ it 'preserves valid external classification authorization labels' do
+ project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } })
+
+ restored_project_json
+
+ expect(project.external_authorization_classification_label).to eq("foobar")
+ end
+ end
end
describe '#restored_project' do
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/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index f93ff074770..87f665bd995 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -2,96 +2,45 @@ require 'spec_helper'
describe Gitlab::ImportExport::Reader do
let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
- let(:test_config) { 'spec/support/import_export/import_export.yml' }
- let(:project_tree_hash) do
- {
- except: [:id, :created_at],
- include: [:issues, :labels,
- { merge_requests: {
- only: [:id],
- except: [:iid],
- include: [:merge_request_diff, :merge_request_test]
- } },
- { commit_statuses: { include: :commit } },
- { project_members: { include: { user: { only: [:email] } } } }]
- }
- end
-
- before do
- allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
- end
-
- it 'generates hash from project tree config' do
- expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash)
- end
-
- context 'individual scenarios' do
- it 'generates the correct hash for a single project relation' do
- setup_yaml(project_tree: [:issues])
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
- end
-
- it 'generates the correct hash for a single project feature relation' do
- setup_yaml(project_tree: [:project_feature])
- expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature])
- end
+ describe '#project_tree' do
+ subject { described_class.new(shared: shared).project_tree }
- it 'generates the correct hash for a multiple project relation' do
- setup_yaml(project_tree: [:issues, :snippets])
+ it 'delegates to AttributesFinder#find_root' do
+ expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
+ .to receive(:find_root)
+ .with(:project)
- expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets])
+ subject
end
- it 'generates the correct hash for a single sub-relation' do
- setup_yaml(project_tree: [issues: [:notes]])
+ context 'when exception raised' do
+ before do
+ expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
+ .to receive(:find_root)
+ .with(:project)
+ .and_raise(StandardError)
+ end
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }])
- end
-
- it 'generates the correct hash for a multiple sub-relation' do
- setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]])
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }])
- end
+ it { is_expected.to be false }
- it 'generates the correct hash for a sub-relation with another sub-relation' do
- setup_yaml(project_tree: [merge_requests: [notes: :author]])
+ it 'logs the error' do
+ expect(shared).to receive(:error).with(instance_of(StandardError))
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }])
+ subject
+ end
end
+ end
- it 'generates the correct hash for a relation with included attributes' do
- setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }])
- end
-
- it 'generates the correct hash for a relation with excluded attributes' do
- setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }])
- end
-
- it 'generates the correct hash for a relation with both excluded and included attributes' do
- setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }])
- end
-
- it 'generates the correct hash for a relation with custom methods' do
- setup_yaml(project_tree: [:issues], methods: { issues: [:name] })
-
- expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
- end
+ describe '#group_members_tree' do
+ subject { described_class.new(shared: shared).group_members_tree }
- it 'generates the correct hash for group members' do
- expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } })
- end
+ it 'delegates to AttributesFinder#find_root' do
+ expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
+ .to receive(:find_root)
+ .with(:group_members)
- def setup_yaml(hash)
- allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
+ subject
end
end
end
diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
index 15748407f0c..17bb5bcc155 100644
--- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:user) { create(:admin) }
let(:group) { create(:group, :nested) }
- let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let!(:project) { create(:project, :builds_disabled, :issues_disabled, group: group, name: 'project', path: 'project') }
let(:shared) { project.import_export_shared }
before do
@@ -24,7 +24,6 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:import_path) { 'spec/lib/gitlab/import_export' }
let(:file_content) { IO.read("#{import_path}/project.json") }
let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
- let(:tree_hash) { project_tree_restorer.instance_variable_get(:@tree_hash) }
before do
allow(shared).to receive(:export_path).and_return(import_path)
@@ -92,21 +91,25 @@ describe Gitlab::ImportExport::RelationRenameService do
end
context 'when exporting' do
- let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: user, shared: shared) }
- let(:project_tree) { project_tree_saver.send(:project_json) }
+ let(:export_content_path) { project_tree_saver.full_path }
+ let(:export_content_hash) { ActiveSupport::JSON.decode(File.read(export_content_path)) }
+ let(:injected_hash) { renames.values.product([{}]).to_h }
- it 'adds old relationships to the exported file' do
- project_tree.merge!(renames.values.map { |new_name| [new_name, []] }.to_h)
+ let(:project_tree_saver) do
+ Gitlab::ImportExport::ProjectTreeSaver.new(
+ project: project, current_user: user, shared: shared)
+ end
- allow(project_tree_saver).to receive(:save) do |arg|
- project_tree_saver.send(:project_json_tree)
+ it 'adds old relationships to the exported file' do
+ # we inject relations with new names that should be rewritten
+ expect(project_tree_saver).to receive(:serialize_project_tree).and_wrap_original do |method, *args|
+ method.call(*args).merge(injected_hash)
end
- result = project_tree_saver.save
-
- saved_data = ActiveSupport::JSON.decode(result)
+ expect(project_tree_saver.save).to eq(true)
- expect(saved_data.keys).to include(*(renames.keys + renames.values))
+ expect(export_content_hash.keys).to include(*renames.keys)
+ expect(export_content_hash.keys).to include(*renames.values)
end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index d34c6d2421b..e9750d23c53 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -270,6 +270,7 @@ Ci::Pipeline:
- protected
- iid
- merge_request_id
+- external_pull_request_id
Ci::Stage:
- id
- name
@@ -715,3 +716,16 @@ List:
- updated_at
- milestone_id
- user_id
+ExternalPullRequest:
+- id
+- created_at
+- updated_at
+- project_id
+- pull_request_iid
+- status
+- source_branch
+- target_branch
+- source_repository
+- target_repository
+- source_sha
+- target_sha
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/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/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/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 63ca383ac4b..146e479adef 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -20,6 +20,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to belong_to(:merge_request) }
+ it { is_expected.to belong_to(:external_pull_request) }
it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) }
@@ -885,6 +886,25 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ context 'when source is external pull request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
+ end
+
+ let(:pull_request) { create(:external_pull_request, project: project) }
+
+ it 'exposes external pull request pipeline variables' do
+ expect(subject.to_hash)
+ .to include(
+ 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
+ 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
+ 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
+ )
+ end
+ end
end
describe '#protected_ref?' do
diff --git a/spec/models/external_pull_request_spec.rb b/spec/models/external_pull_request_spec.rb
new file mode 100644
index 00000000000..e85d5b2f6c7
--- /dev/null
+++ b/spec/models/external_pull_request_spec.rb
@@ -0,0 +1,220 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExternalPullRequest do
+ let(:project) { create(:project) }
+ let(:source_branch) { 'the-branch' }
+ let(:status) { :open }
+
+ it { is_expected.to belong_to(:project) }
+
+ shared_examples 'has errors on' do |attribute|
+ it "has errors for #{attribute}" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[attribute]).not_to be_empty
+ end
+ end
+
+ describe 'validations' do
+ context 'when source branch not present' do
+ subject { build(:external_pull_request, source_branch: nil) }
+
+ it_behaves_like 'has errors on', :source_branch
+ end
+
+ context 'when status not present' do
+ subject { build(:external_pull_request, status: nil) }
+
+ it_behaves_like 'has errors on', :status
+ end
+
+ context 'when pull request is from a fork' do
+ subject { build(:external_pull_request, source_repository: 'the-fork', target_repository: 'the-target') }
+
+ it_behaves_like 'has errors on', :base
+ end
+ end
+
+ describe 'create_or_update_from_params' do
+ subject { described_class.create_or_update_from_params(params) }
+
+ context 'when pull request does not exist' do
+ context 'when params are correct' do
+ let(:params) do
+ {
+ project_id: project.id,
+ pull_request_iid: 123,
+ source_branch: 'feature',
+ target_branch: 'master',
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: '97de212e80737a608d939f648d959671fb0a0142',
+ target_sha: 'a09386439ca39abe575675ffd4b89ae824fec22f',
+ status: :open
+ }
+ end
+
+ it 'saves the model successfully and returns it' do
+ expect(subject).to be_persisted
+ expect(subject).to be_valid
+ end
+
+ it 'yields the model' do
+ yielded_value = nil
+
+ result = described_class.create_or_update_from_params(params) do |pull_request|
+ yielded_value = pull_request
+ end
+
+ expect(result).to eq(yielded_value)
+ end
+ end
+
+ context 'when params are not correct' do
+ let(:params) do
+ {
+ pull_request_iid: 123,
+ source_branch: 'feature',
+ target_branch: 'master',
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: nil,
+ target_sha: nil,
+ status: :open
+ }
+ end
+
+ it 'returns an invalid model' do
+ expect(subject).not_to be_persisted
+ expect(subject).not_to be_valid
+ end
+ end
+ end
+
+ context 'when pull request exists' do
+ let!(:pull_request) do
+ create(:external_pull_request,
+ project: project,
+ source_sha: '97de212e80737a608d939f648d959671fb0a0142')
+ end
+
+ context 'when params are correct' do
+ let(:params) do
+ {
+ pull_request_iid: pull_request.pull_request_iid,
+ source_branch: pull_request.source_branch,
+ target_branch: pull_request.target_branch,
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: 'ce84140e8b878ce6e7c4d298c7202ff38170e3ac',
+ target_sha: pull_request.target_sha,
+ status: :open
+ }
+ end
+
+ it 'updates the model successfully and returns it' do
+ expect(subject).to be_valid
+ expect(subject.source_sha).to eq(params[:source_sha])
+ expect(pull_request.reload.source_sha).to eq(params[:source_sha])
+ end
+ end
+
+ context 'when params are not correct' do
+ let(:params) do
+ {
+ pull_request_iid: pull_request.pull_request_iid,
+ source_branch: pull_request.source_branch,
+ target_branch: pull_request.target_branch,
+ source_repository: 'the-repository',
+ target_repository: 'the-repository',
+ source_sha: nil,
+ target_sha: nil,
+ status: :open
+ }
+ end
+
+ it 'returns an invalid model' do
+ expect(subject).not_to be_valid
+ expect(pull_request.reload.source_sha).not_to be_nil
+ expect(pull_request.target_sha).not_to be_nil
+ end
+ end
+ end
+ end
+
+ describe '#open?' do
+ it 'returns true if status is open' do
+ pull_request = create(:external_pull_request, status: :open)
+
+ expect(pull_request).to be_open
+ end
+
+ it 'returns false if status is not open' do
+ pull_request = create(:external_pull_request, status: :closed)
+
+ expect(pull_request).not_to be_open
+ end
+ end
+
+ describe '#closed?' do
+ it 'returns true if status is closed' do
+ pull_request = build(:external_pull_request, status: :closed)
+
+ expect(pull_request).to be_closed
+ end
+
+ it 'returns false if status is not closed' do
+ pull_request = build(:external_pull_request, status: :open)
+
+ expect(pull_request).not_to be_closed
+ end
+ end
+
+ describe '#actual_branch_head?' do
+ let(:project) { create(:project, :repository) }
+ let(:branch) { project.repository.branches.first }
+ let(:source_branch) { branch.name }
+
+ let(:pull_request) do
+ create(:external_pull_request,
+ project: project,
+ source_branch: source_branch,
+ source_sha: source_sha)
+ end
+
+ context 'when source sha matches the head of the branch' do
+ let(:source_sha) { branch.target }
+
+ it 'returns true' do
+ expect(pull_request).to be_actual_branch_head
+ end
+ end
+
+ context 'when source sha does not match the head of the branch' do
+ let(:source_sha) { project.repository.commit('HEAD').sha }
+
+ it 'returns true' do
+ expect(pull_request).not_to be_actual_branch_head
+ end
+ end
+ end
+
+ describe '#from_fork?' do
+ it 'returns true if source_repository differs from target_repository' do
+ pull_request = build(:external_pull_request,
+ source_repository: 'repository-1',
+ target_repository: 'repository-2')
+
+ expect(pull_request).to be_from_fork
+ end
+
+ it 'returns false if source_repository is the same as target_repository' do
+ pull_request = build(:external_pull_request,
+ source_repository: 'repository-1',
+ target_repository: 'repository-1')
+
+ expect(pull_request).not_to be_from_fork
+ end
+ end
+end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 519c519fbcf..5168064bb84 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -151,6 +151,24 @@ describe PagesDomain do
end
end
end
+
+ context 'with ecdsa certificate' do
+ it "is valid" do
+ domain = build(:pages_domain, :ecdsa)
+
+ expect(domain).to be_valid
+ end
+
+ context 'when curve is set explicitly by parameters' do
+ it 'adds errors to private key' do
+ domain = build(:pages_domain, :explicit_ecdsa)
+
+ expect(domain).to be_invalid
+
+ expect(domain.errors[:key]).not_to be_empty
+ end
+ end
+ end
end
describe 'validations' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index bfbcac60fea..e2a684c42ae 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -99,6 +99,7 @@ describe Project do
it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
it { is_expected.to have_many(:cycle_analytics_stages) }
+ it { is_expected.to have_many(:external_pull_requests) }
it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
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/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/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/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb
new file mode 100644
index 00000000000..91fc4d4c123
--- /dev/null
+++ b/spec/requests/api/statistics_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Statistics, 'Statistics' do
+ include ProjectForksHelper
+ TABLES_TO_ANALYZE = %w[
+ projects
+ users
+ namespaces
+ issues
+ merge_requests
+ notes
+ snippets
+ fork_networks
+ fork_network_members
+ keys
+ milestones
+ ].freeze
+
+ let(:path) { "/application/statistics" }
+
+ describe "GET /application/statistics" do
+ context 'when no user' do
+ it "returns authentication error" do
+ get api(path, nil)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context "when not an admin" do
+ let(:user) { create(:user) }
+
+ it "returns forbidden error" do
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ let(:admin) { create(:admin) }
+
+ it 'matches the response schema' do
+ get api(path, admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('statistics')
+ end
+
+ it 'gives the right statistics' do
+ projects = create_list(:project, 4, namespace: create(:namespace, owner: admin))
+ issues = create_list(:issue, 2, project: projects.first, updated_by: admin)
+
+ create_list(:snippet, 2, :public, author: admin)
+ create_list(:note, 2, author: admin, project: projects.first, noteable: issues.first)
+ create_list(:milestone, 3, project: projects.first)
+ create(:key, user: admin)
+ create(:merge_request, source_project: projects.first)
+ fork_project(projects.first, admin)
+
+ # Make sure the reltuples have been updated
+ # to get a correct count on postgresql
+ TABLES_TO_ANALYZE.each do |table|
+ ActiveRecord::Base.connection.execute("ANALYZE #{table}")
+ end
+
+ get api(path, admin)
+
+ expected_statistics = {
+ issues: 2,
+ merge_requests: 1,
+ notes: 2,
+ snippets: 2,
+ forks: 1,
+ ssh_keys: 1,
+ milestones: 3,
+ users: 1,
+ projects: 5,
+ groups: 1,
+ active_users: 1
+ }
+
+ expected_statistics.each do |entity, count|
+ expect(json_response[entity.to_s]).to eq(count.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index d1b58aac104..97de26650db 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -11,6 +11,8 @@ require 'spec_helper'
# because they are 3 edge cases of using wiki pages.
describe API::Wikis do
+ include WorkhorseHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(user) } }
let(:project_wiki) { create(:project_wiki, project: project, user: user) }
@@ -155,7 +157,7 @@ describe API::Wikis do
it 'pushes attachment to the wiki repository' do
allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
- post(api(url, user), params: payload)
+ workhorse_post_with_file(api(url, user), file_key: :file, params: payload)
expect(response).to have_gitlab_http_status(201)
expect(json_response).to eq result_hash.deep_stringify_keys
@@ -180,6 +182,15 @@ describe API::Wikis do
expect(json_response.size).to eq(1)
expect(json_response['error']).to eq('file is invalid')
end
+
+ it 'is backward compatible with regular multipart uploads' do
+ allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
+
+ post(api(url, user), params: payload)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to eq result_hash.deep_stringify_keys
+ end
end
describe 'GET /projects/:id/wikis' do
diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb
new file mode 100644
index 00000000000..aca4644289d
--- /dev/null
+++ b/spec/requests/projects/uploads_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'File uploads' do
+ include WorkhorseHelpers
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ describe 'POST /:namespace/:project/create/:branch' do
+ let(:branch) { 'master' }
+ let(:create_url) { project_blob_path(project, branch) }
+ let(:blob_url) { project_blob_path(project, "#{branch}/dk.png") }
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: branch,
+ branch_name: branch,
+ file: fixture_file_upload('spec/fixtures/dk.png'),
+ commit_message: 'Add an image'
+ }
+ end
+
+ before do
+ project.add_maintainer(user)
+
+ login_as(user)
+ end
+
+ it 'redirects to blob' do
+ workhorse_post_with_file(create_url, file_key: :file, params: params)
+
+ expect(response).to redirect_to(blob_url)
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
index d1483c3c41e..cf0b8ea9b40 100644
--- a/spec/serializers/merge_request_serializer_spec.rb
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe MergeRequestSerializer do
- let(:user) { create(:user) }
- let(:resource) { create(:merge_request) }
+ set(:user) { create(:user) }
+ set(:resource) { create(:merge_request, description: "Description") }
+
let(:json_entity) do
described_class.new(current_user: user)
.represent(resource, serializer: serializer)
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 6cec93a53fd..fe86982af91 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -23,6 +23,7 @@ describe Ci::CreatePipelineService do
trigger_request: nil,
variables_attributes: nil,
merge_request: nil,
+ external_pull_request: nil,
push_options: nil,
source_sha: nil,
target_sha: nil,
@@ -36,8 +37,11 @@ describe Ci::CreatePipelineService do
source_sha: source_sha,
target_sha: target_sha }
- described_class.new(project, user, params).execute(
- source, save_on_errors: save_on_errors, trigger_request: trigger_request, merge_request: merge_request)
+ described_class.new(project, user, params).execute(source,
+ save_on_errors: save_on_errors,
+ trigger_request: trigger_request,
+ merge_request: merge_request,
+ external_pull_request: external_pull_request)
end
# rubocop:enable Metrics/ParameterLists
@@ -756,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
@@ -969,6 +972,152 @@ describe Ci::CreatePipelineService do
end
end
+ describe 'Pipeline for external pull requests' do
+ let(:pipeline) do
+ execute_service(source: source,
+ external_pull_request: pull_request,
+ ref: ref_name,
+ source_sha: source_sha,
+ target_sha: target_sha)
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:ref_name) { 'refs/heads/feature' }
+ let(:source_sha) { project.commit(ref_name).id }
+ let(:target_sha) { nil }
+
+ context 'when source is external pull request' do
+ let(:source) { :external_pull_request_event }
+
+ context 'when config has external_pull_requests keywords' do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo',
+ only: ['external_pull_requests']
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo',
+ except: ['external_pull_requests']
+ }
+ }
+ end
+
+ context 'when external pull request is specified' do
+ let(:pull_request) { create(:external_pull_request, project: project, source_branch: 'feature', target_branch: 'master') }
+ let(:ref_name) { pull_request.source_ref }
+
+ it 'creates an external pull request pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_external_pull_request_event
+ expect(pipeline.external_pull_request).to eq(pull_request)
+ expect(pipeline.source_sha).to eq(source_sha)
+ expect(pipeline.builds.order(:stage_id)
+ .map(&:name))
+ .to eq(%w[build test])
+ end
+
+ context 'when ref is tag' do
+ let(:ref_name) { 'refs/tags/v1.1.0' }
+
+ it 'does not create an extrnal pull request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:tag]).to eq(["is not included in the list"])
+ end
+ end
+
+ context 'when pull request is created from fork' do
+ it 'does not create an external pull request pipeline'
+ end
+
+ context "when there are no matched jobs" do
+ let(:config) do
+ {
+ test: {
+ stage: 'test',
+ script: 'echo',
+ except: ['external_pull_requests']
+ }
+ }
+ end
+
+ it 'does not create a detached merge request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."])
+ end
+ end
+ end
+
+ context 'when external pull request is not specified' do
+ let(:pull_request) { nil }
+
+ it 'does not create an external pull request pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.errors[:external_pull_request]).to eq(["can't be blank"])
+ end
+ end
+ end
+
+ context "when config does not have external_pull_requests keywords" do
+ let(:config) do
+ {
+ build: {
+ stage: 'build',
+ script: 'echo'
+ },
+ test: {
+ stage: 'test',
+ script: 'echo'
+ },
+ pages: {
+ stage: 'deploy',
+ script: 'echo'
+ }
+ }
+ end
+
+ context 'when external pull request is specified' do
+ let(:pull_request) do
+ create(:external_pull_request,
+ project: project,
+ source_branch: Gitlab::Git.ref_name(ref_name),
+ target_branch: 'master')
+ end
+
+ it 'creates an external pull request pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_external_pull_request_event
+ expect(pipeline.external_pull_request).to eq(pull_request)
+ expect(pipeline.source_sha).to eq(source_sha)
+ expect(pipeline.builds.order(:stage_id)
+ .map(&:name))
+ .to eq(%w[build test pages])
+ end
+ end
+
+ context 'when external pull request is not specified' do
+ let(:pull_request) { nil }
+
+ it 'does not create an external pull request pipeline' do
+ expect(pipeline).not_to be_persisted
+
+ expect(pipeline.errors[:base])
+ .to eq(['Failed to build the pipeline!'])
+ end
+ end
+ end
+ end
+ end
+
describe 'Pipelines for merge requests' do
let(:pipeline) do
execute_service(source: source,
@@ -1024,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
@@ -1289,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
@@ -1329,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
@@ -1364,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/external_pull_requests/create_pipeline_service_spec.rb b/spec/services/external_pull_requests/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..a4da5b38b97
--- /dev/null
+++ b/spec/services/external_pull_requests/create_pipeline_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExternalPullRequests::CreatePipelineService do
+ describe '#execute' do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:pull_request) { create(:external_pull_request, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ subject { described_class.new(project, user).execute(pull_request) }
+
+ context 'when pull request is open' do
+ before do
+ pull_request.update!(status: :open)
+ end
+
+ context 'when source sha is the head of the source branch' do
+ let(:source_branch) { project.repository.branches.last }
+ let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
+
+ before do
+ pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target)
+ end
+
+ it 'creates a pipeline for external pull request' do
+ expect(subject).to be_valid
+ expect(subject).to be_persisted
+ expect(subject).to be_external_pull_request_event
+ expect(subject).to eq(project.ci_pipelines.last)
+ expect(subject.external_pull_request).to eq(pull_request)
+ expect(subject.user).to eq(user)
+ expect(subject.status).to eq('pending')
+ expect(subject.ref).to eq(pull_request.source_branch)
+ expect(subject.sha).to eq(pull_request.source_sha)
+ expect(subject.source_sha).to eq(pull_request.source_sha)
+ end
+ end
+
+ context 'when source sha is not the head of the source branch (force push upon rebase)' do
+ let(:source_branch) { project.repository.branches.first }
+ let(:commit) { project.repository.commits(source_branch.name, limit: 2).last }
+
+ before do
+ pull_request.update!(source_branch: source_branch.name, source_sha: commit.sha)
+ end
+
+ it 'does nothing' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ context 'when pull request is not opened' do
+ before do
+ pull_request.update!(status: :closed)
+ end
+
+ it 'does nothing' do
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+
+ expect(subject).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index d9e607cd251..c3a4f3dbe3f 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -99,6 +99,20 @@ describe Git::BranchPushService, services: true do
expect(pipeline).to be_push
expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref)
end
+
+ context 'when pipeline has errors' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'reports an error' do
+ allow(Sidekiq).to receive(:server?).and_return(true)
+ expect(Sidekiq.logger).to receive(:warn)
+
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ end
+ end
end
describe "Updates merge requests" do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 6ca0a3fa448..b65ee16c189 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1140,6 +1140,19 @@ describe QuickActions::InterpretService do
let(:todo_label) { create(:label, project: project, title: 'To Do') }
let(:inreview_label) { create(:label, project: project, title: 'In Review') }
+ it 'is available when the user is a developer' do
+ expect(service.available_commands(issue)).to include(a_hash_including(name: :copy_metadata))
+ end
+
+ context 'when the user does not have permission' do
+ let(:guest) { create(:user) }
+ let(:service) { described_class.new(project, guest) }
+
+ it 'is not available' do
+ expect(service.available_commands(issue)).not_to include(a_hash_including(name: :copy_metadata))
+ end
+ end
+
it_behaves_like 'empty command' do
let(:content) { '/copy_metadata' }
let(:issuable) { issue }
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/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 4488e5f227e..fdbfe53fa39 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -17,7 +17,36 @@ module WorkhorseHelpers
end
def workhorse_internal_api_request_header
- jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
{ 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
end
+
+ # workhorse_post_with_file will transform file_key inside params as if it was disk accelerated by workhorse
+ def workhorse_post_with_file(url, file_key:, params:)
+ workhorse_params = params.dup
+ file = workhorse_params.delete(file_key)
+
+ workhorse_params.merge!(workhorse_disk_accelerated_file_params(file_key, file))
+
+ post(url,
+ params: workhorse_params,
+ headers: workhorse_rewritten_fields_header('file' => file.path)
+ )
+ end
+
+ private
+
+ def jwt_token(data = {})
+ JWT.encode({ 'iss' => 'gitlab-workhorse' }.merge(data), Gitlab::Workhorse.secret, 'HS256')
+ end
+
+ def workhorse_rewritten_fields_header(fields)
+ { Gitlab::Middleware::Multipart::RACK_ENV_KEY => jwt_token('rewritten_fields' => fields) }
+ end
+
+ def workhorse_disk_accelerated_file_params(key, file)
+ {
+ "#{key}.name" => file.original_filename,
+ "#{key}.path" => file.path
+ }
+ end
end
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 734d6838f4d..116bc8d0b9c 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -1,15 +1,22 @@
# Class relationships to be included in the project import/export
-project_tree:
- - :issues
- - :labels
- - merge_requests:
- - :merge_request_diff
- - :merge_request_test
- - commit_statuses:
- - :commit
- - project_members:
+tree:
+ project:
+ - :issues
+ - :labels
+ - merge_requests:
+ - :merge_request_diff
+ - :merge_request_test
+ - commit_statuses:
+ - :commit
+ - project_members:
+ - :user
+ group_members:
- :user
+preloads:
+ merge_request_diff:
+ source_project:
+
included_attributes:
merge_requests:
- :id
diff --git a/spec/validators/named_ecdsa_key_validator_spec.rb b/spec/validators/named_ecdsa_key_validator_spec.rb
new file mode 100644
index 00000000000..044c5b84a56
--- /dev/null
+++ b/spec/validators/named_ecdsa_key_validator_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe NamedEcdsaKeyValidator do
+ let(:validator) { described_class.new(attributes: [:key]) }
+ let!(:domain) { build(:pages_domain) }
+
+ subject { validator.validate_each(domain, :key, value) }
+
+ context 'with empty value' do
+ let(:value) { nil }
+
+ it 'does not add any error if value is empty' do
+ subject
+
+ expect(domain.errors).to be_empty
+ end
+ end
+
+ shared_examples 'does not add any error' do
+ it 'does not add any error' do
+ expect(value).to be_present
+
+ subject
+
+ expect(domain.errors).to be_empty
+ end
+ end
+
+ context 'when key is not EC' do
+ let(:value) { attributes_for(:pages_domain)[:key] }
+
+ include_examples 'does not add any error'
+ end
+
+ context 'with ECDSA certificate with named curve' do
+ let(:value) { attributes_for(:pages_domain, :ecdsa)[:key] }
+
+ include_examples 'does not add any error'
+ end
+
+ context 'with ECDSA certificate with explicit curve params' do
+ let(:value) { attributes_for(:pages_domain, :explicit_ecdsa)[:key] }
+
+ it 'adds errors' do
+ expect(value).to be_present
+
+ subject
+
+ expect(domain.errors[:key]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/workers/update_external_pull_requests_worker_spec.rb b/spec/workers/update_external_pull_requests_worker_spec.rb
new file mode 100644
index 00000000000..f3956bb3514
--- /dev/null
+++ b/spec/workers/update_external_pull_requests_worker_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe UpdateExternalPullRequestsWorker do
+ describe '#perform' do
+ set(:project) { create(:project, import_source: 'tanuki/repository') }
+ set(:user) { create(:user) }
+ let(:worker) { described_class.new }
+
+ before do
+ create(:external_pull_request,
+ project: project,
+ source_repository: project.import_source,
+ target_repository: project.import_source,
+ source_branch: 'feature-1',
+ target_branch: 'master')
+
+ create(:external_pull_request,
+ project: project,
+ source_repository: project.import_source,
+ target_repository: project.import_source,
+ source_branch: 'feature-1',
+ target_branch: 'develop')
+ end
+
+ subject { worker.perform(project.id, user.id, ref) }
+
+ context 'when ref is a branch' do
+ let(:ref) { 'refs/heads/feature-1' }
+ let(:create_pipeline_service) { instance_double(ExternalPullRequests::CreatePipelineService) }
+
+ it 'runs CreatePipelineService for each pull request matching the source branch and repository' do
+ expect(ExternalPullRequests::CreatePipelineService)
+ .to receive(:new)
+ .and_return(create_pipeline_service)
+ .twice
+ expect(create_pipeline_service).to receive(:execute).twice
+
+ subject
+ end
+ end
+
+ context 'when ref is not a branch' do
+ let(:ref) { 'refs/tags/v1.2.3' }
+
+ it 'does nothing' do
+ expect(ExternalPullRequests::CreatePipelineService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 92da409f544..c64c3a6acaa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -991,15 +991,15 @@
dependencies:
vue-eslint-parser "^6.0.4"
-"@gitlab/svgs@^1.71.0":
- version "1.71.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.71.0.tgz#c8e6e8f500ea91e5cbba4ac08df533fb2e622a00"
- integrity sha512-kkeNic/FFwaqKnzwio4NE7whBOZ/toRJ8cS0587DBotajAzSYhph5ij4TCY2GTjPa33zIJ5OUr/k90C0Kr71hQ==
+"@gitlab/svgs@^1.72.0":
+ version "1.72.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.72.0.tgz#78a29fd383a5a2b31ef91670068a6fea05ba234e"
+ integrity sha512-EVKRqrXsCY6tUiVAh+lpFMJAyNXZwfEqv5NeKH5ginhALMlOunRkY5rsDllyNvgZ0DWHJS1KEKJj2oVU1ouwAg==
-"@gitlab/ui@5.21.0":
- version "5.21.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.21.0.tgz#975cf0bca3d16dd080d67ed392b9d24cd64695ac"
- integrity sha512-8TMVM+pJXf7omHgKMMZ1FiltuyMOTwfQ3iFgorQzcuhio9u35DJpWi45S2TF7m6CrlpJi7dMX3BsXLbF7ViSUw==
+"@gitlab/ui@5.21.1":
+ version "5.21.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.21.1.tgz#8215ab3eae4296845596d5b3a987d5460b030569"
+ integrity sha512-TjPVhex9sQGUVwebaJK5XuopDHWx4+Sh9N7yH5u8eXSFWa8vk11voR4qYVt7DZB7powAO/+iiXxYMLLNtXmC/g==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -11815,6 +11815,11 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
+timezone-mock@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.0.8.tgz#1b9f7af13f2bf84b7aa3d3d6e24aa17255b6037d"
+ integrity sha512-7dgx34HJPY8O/c5dbqG+I9S3TVDjrfssXmS8BNqiy8sdYvYDfM7shHpNA6VTDQWcDGyv43bE3El6YuFDQf1X3g==
+
tiny-emitter@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"