summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--.gitlab/merge_request_templates/Documentation.md1
-rw-r--r--Dangerfile23
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock41
-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/error_tracking/components/error_tracking_list.vue20
-rw-r--r--app/assets/javascripts/error_tracking/index.js4
-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/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/clusters/base_controller.rb4
-rw-r--r--app/controllers/projects/settings/operations_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb1
-rw-r--r--app/finders/issuable_finder.rb23
-rw-r--r--app/graphql/types/permission_types/project.rb2
-rw-r--r--app/helpers/projects/error_tracking_helper.rb3
-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/pages_domain.rb4
-rw-r--r--app/models/project.rb2
-rw-r--r--app/policies/clusters/instance_policy.rb1
-rw-r--r--app/policies/project_policy.rb1
-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/services/issuable_base_service.rb15
-rw-r--r--app/services/labels/available_labels_service.rb6
-rw-r--r--app/services/labels/find_or_create_service.rb8
-rw-r--r--app/services/merge_requests/build_service.rb12
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb10
-rw-r--r--app/validators/certificate_key_validator.rb4
-rw-r--r--app/validators/named_ecdsa_key_validator.rb34
-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/error_tracking/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml17
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml1
-rw-r--r--app/views/shared/_import_form.html.haml1
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml2
-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/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml5
-rw-r--r--changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.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/add-label-push-opts.yml5
-rw-r--r--changelogs/unreleased/ecdsa_pages_certificates.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/keyboard-shortcuts-2.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/initializers/chronic_duration.rb4
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--danger/database/Dangerfile18
-rw-r--r--danger/documentation/Dangerfile22
-rw-r--r--danger/duplicate_yarn_dependencies/Dangerfile24
-rw-r--r--danger/eslint/Dangerfile22
-rw-r--r--danger/frozen_string/Dangerfile12
-rw-r--r--danger/gemfile/Dangerfile19
-rw-r--r--danger/prettier/Dangerfile26
-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.md35
-rw-r--r--doc/administration/integration/plantuml.md17
-rw-r--r--doc/administration/logs.md4
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md6
-rw-r--r--doc/api/merge_request_approvals.md71
-rw-r--r--doc/api/v3_to_v4.md4
-rw-r--r--doc/ci/examples/README.md2
-rw-r--r--doc/ci/examples/browser_performance.md157
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md14
-rw-r--r--doc/ci/multi_project_pipelines.md2
-rw-r--r--doc/development/api_graphql_styleguide.md45
-rw-r--r--doc/development/architecture.md4
-rw-r--r--doc/development/background_migrations.md8
-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/security/reset_root_password.md2
-rw-r--r--doc/user/admin_area/diff_limits.md2
-rw-r--r--doc/user/application_security/sast/index.md1
-rw-r--r--doc/user/clusters/applications.md9
-rw-r--r--doc/user/project/description_templates.md2
-rw-r--r--doc/user/project/import/img/import_projects_from_repo_url.pngbin142329 -> 142284 bytes
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md173
-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.md50
-rw-r--r--doc/user/project/web_ide/index.md2
-rw-r--r--doc/workflow/img/repository_mirroring_push_settings.pngbin50526 -> 72515 bytes
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md3
-rw-r--r--doc/workflow/repository_mirroring.md12
-rw-r--r--doc/workflow/shortcuts.md191
-rw-r--r--lib/api/validations/types/workhorse_file.rb20
-rw-r--r--lib/api/wikis.rb22
-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/danger/helper.rb11
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb54
-rw-r--r--lib/gitlab/import_export/config.rb81
-rw-r--r--lib/gitlab/import_export/import_export.yml168
-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.rb21
-rw-r--r--lib/gitlab/import_export/reader.rb30
-rw-r--r--lib/gitlab/middleware/multipart.rb18
-rw-r--r--lib/gitlab/patch/chronic_duration.rb35
-rw-r--r--lib/gitlab/push_options.rb21
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb2
-rw-r--r--lib/gitlab_danger.rb54
-rw-r--r--lib/tasks/gitlab_danger.rake17
-rw-r--r--locale/gitlab.pot138
-rw-r--r--package.json5
-rw-r--r--plugins/.gitignore5
-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--shared/.gitignore4
-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/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/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/frontend/clusters/components/applications_spec.js4
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js32
-rw-r--r--spec/helpers/projects/error_tracking_helper_spec.rb28
-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/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.rb195
-rw-r--r--spec/lib/gitlab/import_export/config_spec.rb266
-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/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/patch/chronic_duration_spec.rb27
-rw-r--r--spec/lib/gitlab/time_tracking_formatter_spec.rb8
-rw-r--r--spec/lib/gitlab_danger_spec.rb76
-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/requests/api/wikis_spec.rb13
-rw-r--r--spec/requests/projects/uploads_spec.rb38
-rw-r--r--spec/requests/rack_attack_global_spec.rb222
-rw-r--r--spec/serializers/merge_request_serializer_spec.rb5
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb154
-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/merge_requests/push_options_handler_service_spec.rb145
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb13
-rw-r--r--spec/support/helpers/workhorse_helpers.rb31
-rw-r--r--spec/support/import_export/import_export.yml21
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb221
-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--tmp/.gitignore5
-rw-r--r--yarn.lock21
195 files changed, 4210 insertions, 1793 deletions
diff --git a/.gitignore b/.gitignore
index 3ffe4263c4f..fcbb4c352a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,11 +61,9 @@ eslint-report.html
/shared/artifacts/
/rails_best_practices_output.html
/tags
-/tmp/*
/vendor/bundle/*
/vendor/gitaly-ruby
/builds*
-/shared/*
/.gitlab_workhorse_secret
/webpack-report/
/knapsack/
@@ -73,7 +71,6 @@ eslint-report.html
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
-/plugins/*
/.gitlab_pages_secret
/.gitlab_smime_key
/.gitlab_smime_cert
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/Dangerfile b/Dangerfile
index 094d55e8652..228190cd530 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,23 +1,12 @@
# frozen_string_literal: true
+
+require_relative 'lib/gitlab_danger'
+
danger.import_plugin('danger/plugins/helper.rb')
danger.import_plugin('danger/plugins/roulette.rb')
unless helper.release_automation?
- danger.import_dangerfile(path: 'danger/metadata')
- danger.import_dangerfile(path: 'danger/changes_size')
- danger.import_dangerfile(path: 'danger/changelog')
- danger.import_dangerfile(path: 'danger/specs')
- danger.import_dangerfile(path: 'danger/gemfile')
- danger.import_dangerfile(path: 'danger/database')
- danger.import_dangerfile(path: 'danger/documentation')
- danger.import_dangerfile(path: 'danger/frozen_string')
- danger.import_dangerfile(path: 'danger/commit_messages')
- danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
- danger.import_dangerfile(path: 'danger/prettier')
- danger.import_dangerfile(path: 'danger/eslint')
- danger.import_dangerfile(path: 'danger/roulette')
- danger.import_dangerfile(path: 'danger/single_codebase')
- danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
- danger.import_dangerfile(path: 'danger/ce_ee_vue_templates')
- danger.import_dangerfile(path: 'danger/only_documentation')
+ GitlabDanger.new(helper.gitlab_helper).rule_names.each do |file|
+ danger.import_dangerfile(path: File.join('danger', file))
+ end
end
diff --git a/Gemfile b/Gemfile
index 3e3a8c5dde2..9f9d8f487fe 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'
@@ -318,6 +318,7 @@ end
group :development do
gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false
+ gem 'danger', '~> 6.0', require: false
gem 'letter_opener_web', '~> 1.3.4'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 6add217bc32..011a365110c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -140,9 +140,15 @@ GEM
numerizer (~> 0.1.1)
chunky_png (1.3.5)
citrus (3.0.2)
+ claide (1.0.3)
+ claide-plugins (0.9.2)
+ cork
+ nap
+ open4 (~> 1.3)
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
+ colored2 (3.1.2)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concord (0.1.5)
@@ -151,6 +157,8 @@ GEM
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
contracts (0.11.0)
+ cork (0.3.0)
+ colored2 (~> 3.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
@@ -158,6 +166,19 @@ GEM
css_parser (1.5.0)
addressable
daemons (1.2.6)
+ danger (6.0.9)
+ claide (~> 1.0)
+ claide-plugins (>= 0.9.2)
+ colored2 (~> 3.1)
+ cork (~> 0.1)
+ faraday (~> 0.9)
+ faraday-http-cache (~> 2.0)
+ git (~> 1.5)
+ kramdown (~> 2.0)
+ kramdown-parser-gfm (~> 1.0)
+ no_proxy_fix
+ octokit (~> 4.7)
+ terminal-table (~> 1)
database_cleaner (1.7.0)
debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
@@ -227,6 +248,8 @@ GEM
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
+ faraday-http-cache (2.0.0)
+ faraday (~> 0.8)
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
@@ -261,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)
@@ -308,6 +331,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
+ git (1.5.0)
gitaly (1.58.0)
grpc (~> 1.0)
github-markup (1.7.0)
@@ -478,6 +502,9 @@ GEM
kgio (2.11.2)
knapsack (1.17.0)
rake
+ kramdown (2.1.0)
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
kubeclient (4.2.2)
http (~> 3.0)
recursive-open-struct (~> 1.0, >= 1.0.4)
@@ -535,10 +562,12 @@ GEM
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
nakayoshi_fork (0.0.4)
+ nap (1.1.0)
net-ldap (0.16.0)
net-ssh (5.2.0)
netrc (0.11.0)
nio4r (2.3.1)
+ no_proxy_fix (0.1.2)
nokogiri (1.10.4)
mini_portile2 (~> 2.4.0)
nokogumbo (1.5.0)
@@ -615,6 +644,7 @@ GEM
addressable (~> 2.5)
omniauth (~> 1.3)
openid_connect (~> 1.1)
+ open4 (1.3.4)
openid_connect (1.1.6)
activemodel
attr_required (>= 1.0.0)
@@ -769,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)
@@ -926,6 +956,8 @@ GEM
ffi
sysexits (1.2.0)
temple (0.8.0)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
test-prof (0.2.5)
text (1.3.1)
thin (1.7.2)
@@ -1055,6 +1087,7 @@ DEPENDENCIES
concurrent-ruby (~> 1.1)
connection_pool (~> 2.0)
creole (~> 0.5.0)
+ danger (~> 6.0)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.2.0)
default_value_for (~> 3.2.0)
@@ -1081,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)
@@ -1196,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/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/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 43ae54133af..b1d568532a6 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -38,6 +38,10 @@ export default {
type: String,
required: true,
},
+ userCanEnableErrorTracking: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapState(['errors', 'externalUrl', 'loading']),
@@ -111,14 +115,26 @@ export default {
</gl-table>
</div>
</div>
- <div v-else>
+ <div v-else-if="userCanEnableErrorTracking">
<gl-empty-state
:title="__('Get started with error tracking')"
- :description="__('Monitor your errors by integrating with Sentry')"
+ :description="__('Monitor your errors by integrating with Sentry.')"
:primary-button-text="__('Enable error tracking')"
:primary-button-link="enableErrorTrackingLink"
:svg-path="illustrationPath"
/>
</div>
+ <div v-else>
+ <gl-empty-state :title="__('Get started with error tracking')" :svg-path="illustrationPath">
+ <template #description>
+ <div>
+ <span>{{ __('Monitor your errors by integrating with Sentry.') }}</span>
+ <a href="/help/user/project/operations/error_tracking.html">
+ {{ __('More information') }}
+ </a>
+ </div>
+ </template>
+ </gl-empty-state>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js
index 3d609448efe..073e2c8f1c7 100644
--- a/app/assets/javascripts/error_tracking/index.js
+++ b/app/assets/javascripts/error_tracking/index.js
@@ -14,9 +14,10 @@ export default () => {
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset;
- let { errorTrackingEnabled } = domEl.dataset;
+ let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset;
errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
+ userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking);
return createElement('error-tracking-list', {
props: {
@@ -24,6 +25,7 @@ export default () => {
enableErrorTrackingLink,
errorTrackingEnabled,
illustrationPath,
+ userCanEnableErrorTracking,
},
});
},
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/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/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/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 5cfb0ac307d..ec89bb89edc 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -3,7 +3,7 @@
module Projects
module Settings
class OperationsController < Projects::ApplicationController
- before_action :authorize_update_environment!
+ before_action :authorize_admin_operations!
helper_method :error_tracking_setting
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/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index e9a4ea9157b..993d33c4fc2 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -16,7 +16,7 @@ module Types
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
- :create_pages, :destroy_pages, :read_pages_content
+ :create_pages, :destroy_pages, :read_pages_content, :admin_operations
end
end
end
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
index 6daf2e21ca2..fd1222a1dfb 100644
--- a/app/helpers/projects/error_tracking_helper.rb
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -1,12 +1,13 @@
# frozen_string_literal: true
module Projects::ErrorTrackingHelper
- def error_tracking_data(project)
+ def error_tracking_data(current_user, project)
error_tracking_enabled = !!project.error_tracking_setting&.enabled?
{
'index-path' => project_error_tracking_index_path(project,
format: :json),
+ 'user-can-enable-error-tracking' => can?(current_user, :admin_operations, project).to_s,
'enable-error-tracking-link' => project_settings_operations_path(project),
'error-tracking-enabled' => error_tracking_enabled.to_s,
'illustration-path' => image_path('illustrations/cluster_popover.svg')
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/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/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/policies/project_policy.rb b/app/policies/project_policy.rb
index e2634692dc7..5c36b59f07b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -294,6 +294,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_release
enable :destroy_artifacts
enable :daily_statistics
+ enable :admin_operations
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
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/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3555864f834..900e5063621 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -76,13 +76,16 @@ class IssuableBaseService < BaseService
end
def filter_labels
- params[:add_label_ids] = labels_service.filter_labels_ids_in_param(:add_label_ids) if params[:add_label_ids]
- params[:remove_label_ids] = labels_service.filter_labels_ids_in_param(:remove_label_ids) if params[:remove_label_ids]
+ label_ids_to_filter(:add_label_ids, :add_labels, false)
+ label_ids_to_filter(:remove_label_ids, :remove_labels, true)
+ label_ids_to_filter(:label_ids, :labels, false)
+ end
- if params[:label_ids]
- params[:label_ids] = labels_service.filter_labels_ids_in_param(:label_ids)
- elsif params[:labels]
- params[:label_ids] = labels_service.find_or_create_by_titles.map(&:id)
+ def label_ids_to_filter(label_id_key, label_key, find_only)
+ if params[label_id_key]
+ params[label_id_key] = labels_service.filter_labels_ids_in_param(label_id_key)
+ elsif params[label_key]
+ params[label_id_key] = labels_service.find_or_create_by_titles(label_key, find_only: find_only).map(&:id)
end
end
diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb
index fe477d96970..8886e58d6ef 100644
--- a/app/services/labels/available_labels_service.rb
+++ b/app/services/labels/available_labels_service.rb
@@ -9,8 +9,8 @@ module Labels
@params = params
end
- def find_or_create_by_titles
- labels = params.delete(:labels)
+ def find_or_create_by_titles(key = :labels, find_only: false)
+ labels = params.delete(key)
return [] unless labels
@@ -23,7 +23,7 @@ module Labels
include_ancestor_groups: true,
title: label_name.strip,
available_labels: available_labels
- ).execute
+ ).execute(find_only: find_only)
label
end.compact
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index 628873519d7..a47dd42aea0 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -9,9 +9,9 @@ module Labels
@params = params.dup.with_indifferent_access
end
- def execute(skip_authorization: false)
+ def execute(skip_authorization: false, find_only: false)
@skip_authorization = skip_authorization
- find_or_create_label
+ find_or_create_label(find_only: find_only)
end
private
@@ -30,9 +30,11 @@ module Labels
# Only creates the label if current_user can do so, if the label does not exist
# and the user can not create the label, nil is returned
# rubocop: disable CodeReuse/ActiveRecord
- def find_or_create_label
+ def find_or_create_label(find_only: false)
new_label = available_labels.find_by(title: title)
+ return new_label if find_only
+
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
create_params = params.except(:include_ancestor_groups)
new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index b28f80939ae..308a3a10d1a 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -18,6 +18,18 @@ module MergeRequests
merge_request.target_project = find_target_project
filter_params(merge_request)
+
+ # merge_request.assign_attributes(...) below is a Rails
+ # method that only work if all the params it is passed have
+ # corresponding fields in the database. As there are no fields
+ # in the database for :add_label_ids and :remove_label_ids, we
+ # need to remove them from the params before the call to
+ # merge_request.assign_attributes(...)
+ #
+ # IssuableBaseService#process_label_ids takes care
+ # of the removal.
+ params[:label_ids] = process_label_ids(params, extra_label_ids: merge_request.label_ids.to_a)
+
merge_request.assign_attributes(params.to_h.compact)
merge_request.compare_commits = []
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index b210004e6e1..0168b31005e 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -100,7 +100,8 @@ module MergeRequests
merge_request = ::MergeRequests::CreateService.new(
project,
current_user,
- merge_request.attributes.merge(assignees: merge_request.assignees)
+ merge_request.attributes.merge(assignees: merge_request.assignees,
+ label_ids: merge_request.label_ids)
).execute
end
@@ -122,7 +123,9 @@ module MergeRequests
title: push_options[:title],
description: push_options[:description],
target_branch: push_options[:target],
- force_remove_source_branch: push_options[:remove_source_branch]
+ force_remove_source_branch: push_options[:remove_source_branch],
+ label: push_options[:label],
+ unlabel: push_options[:unlabel]
}
params.compact!
@@ -134,6 +137,9 @@ module MergeRequests
)
end
+ params[:add_labels] = params.delete(:label).keys if params.has_key?(:label)
+ params[:remove_labels] = params.delete(:unlabel).keys if params.has_key?(:unlabel)
+
params
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/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/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml
index bc02c5f0e5a..96f61584a99 100644
--- a/app/views/projects/error_tracking/index.html.haml
+++ b/app/views/projects/error_tracking/index.html.haml
@@ -1,3 +1,3 @@
- page_title _('Errors')
-#js-error_tracking{ data: error_tracking_data(@project) }
+#js-error_tracking{ data: error_tracking_data(@current_user, @project) }
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/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 1a163cc4a54..7ff6c0a2019 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -3,6 +3,7 @@
%li
= _('The repository must be accessible over <code>http://</code>,
<code>https://</code>, <code>ssh://</code> or <code>git://</code>.').html_safe
+ %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe
%li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index d0f9374e832..b2ea45d6f1a 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -27,6 +27,7 @@
%ul
%li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
+ %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe
%li
= _('If your HTTP repository is not publicly accessible, add your credentials.')
%li
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/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/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml b/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml
new file mode 100644
index 00000000000..1962a662179
--- /dev/null
+++ b/changelogs/unreleased/59729-estimate-quick-action-does-not-produce-correct-time-for-1mo.yml
@@ -0,0 +1,5 @@
+---
+title: Fix parsing of months in time tracking commands
+merge_request: 32165
+author:
+type: fixed
diff --git a/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml b/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml
new file mode 100644
index 00000000000..b152943942f
--- /dev/null
+++ b/changelogs/unreleased/66467-enable-error-tracking-only-user-can-read-sentry-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Display `more information` docs link on error tracking page when users do not have permissions to enable that feature
+merge_request: 32365
+author: Romain Maneschi
+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/add-label-push-opts.yml b/changelogs/unreleased/add-label-push-opts.yml
new file mode 100644
index 00000000000..1289020e4e5
--- /dev/null
+++ b/changelogs/unreleased/add-label-push-opts.yml
@@ -0,0 +1,5 @@
+---
+title: Support adding and removing labels w/ push opts
+merge_request: 31831
+author:
+type: added
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/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/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/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/initializers/chronic_duration.rb b/config/initializers/chronic_duration.rb
index b65b06c813a..aa43eef434c 100644
--- a/config/initializers/chronic_duration.rb
+++ b/config/initializers/chronic_duration.rb
@@ -1 +1,5 @@
+# frozen_string_literal: true
+
ChronicDuration.raise_exceptions = true
+
+ChronicDuration.prepend Gitlab::Patch::ChronicDuration
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/danger/database/Dangerfile b/danger/database/Dangerfile
index 3550cb7eabf..5cdad09db6e 100644
--- a/danger/database/Dangerfile
+++ b/danger/database/Dangerfile
@@ -1,7 +1,13 @@
# frozen_string_literal: true
-SCHEMA_NOT_UPDATED_MESSAGE = <<~MSG
-**New %<migrations>s added but %<schema>s wasn't updated.**
+gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
+
+SCHEMA_NOT_UPDATED_MESSAGE_SHORT = <<~MSG
+New %<migrations>s added but %<schema>s wasn't updated.
+MSG
+
+SCHEMA_NOT_UPDATED_MESSAGE_FULL = <<~MSG
+**#{SCHEMA_NOT_UPDATED_MESSAGE_SHORT}**
Usually, when adding new %<migrations>s, %<schema>s should be
updated too (unless the migration isn't changing the DB schema
@@ -29,14 +35,18 @@ geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb}).emp
non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty?
geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty?
+format_str = gitlab_danger.ci? ? SCHEMA_NOT_UPDATED_MESSAGE_FULL : SCHEMA_NOT_UPDATED_MESSAGE_SHORT
+
if non_geo_migration_created && !non_geo_db_schema_updated
- warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'migrations', schema: gitlab.html_link("db/schema.rb"))
+ warn format(format_str, migrations: 'migrations', schema: gitlab_danger.html_link("db/schema.rb"))
end
if geo_migration_created && !geo_db_schema_updated
- warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'Geo migrations', schema: gitlab.html_link("ee/db/geo/schema.rb"))
+ warn format(format_str, migrations: 'Geo migrations', schema: gitlab_danger.html_link("ee/db/geo/schema.rb"))
end
+return unless gitlab_danger.ci?
+
db_paths_to_review = helper.changes_by_category[:database]
if gitlab.mr_labels.include?('database') || db_paths_to_review.any?
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 96c0d9b7478..ad64c3a6c60 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -6,20 +6,22 @@ unless docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a review ' \
'from the Technical Writing team.'
- markdown(<<~MARKDOWN)
-## Documentation review
+ if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Documentation review
-The following files require a review from a technical writer:
+ The following files require a review from a technical writer:
-* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
-The review does not need to block merging this merge request. See the:
+ The review does not need to block merging this merge request. See the:
-- [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review.
-- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
- MARKDOWN
+ - [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review.
+ - [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
+ MARKDOWN
- unless gitlab.mr_labels.include?('Documentation')
- warn 'This merge request is missing the ~Documentation label.'
+ unless gitlab.mr_labels.include?('Documentation')
+ warn 'This merge request is missing the ~Documentation label.'
+ end
end
end
diff --git a/danger/duplicate_yarn_dependencies/Dangerfile b/danger/duplicate_yarn_dependencies/Dangerfile
index 25f81ec86a4..492b888d00e 100644
--- a/danger/duplicate_yarn_dependencies/Dangerfile
+++ b/danger/duplicate_yarn_dependencies/Dangerfile
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-return unless helper.all_changed_files.include? 'yarn.lock'
+return unless helper.all_changed_files.include?('yarn.lock')
duplicate = `node_modules/.bin/yarn-deduplicate --list --strategy fewer yarn.lock`
.split(/$/)
@@ -11,17 +11,19 @@ return if duplicate.empty?
warn 'This merge request has introduced duplicated yarn dependencies.'
-markdown(<<~MARKDOWN)
- ## Duplicate yarn dependencies
+if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Duplicate yarn dependencies
- The following dependencies should be de-duplicated:
+ The following dependencies should be de-duplicated:
- * #{duplicate.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{duplicate.map { |path| "`#{path}`" }.join("\n* ")}
- Please run the following command and commit the changes to `yarn.lock`:
+ Please run the following command and commit the changes to `yarn.lock`:
- ```
- node_modules/.bin/yarn-deduplicate --strategy fewer yarn.lock \\
- && yarn install
- ```
-MARKDOWN
+ ```
+ node_modules/.bin/yarn-deduplicate --strategy fewer yarn.lock \\
+ && yarn install
+ ```
+ MARKDOWN
+end
diff --git a/danger/eslint/Dangerfile b/danger/eslint/Dangerfile
index 4916cacfd7e..92830bd7706 100644
--- a/danger/eslint/Dangerfile
+++ b/danger/eslint/Dangerfile
@@ -13,17 +13,19 @@ return if eslint_candidates.empty?
warn 'This merge request changed files with disabled eslint rules. Please consider fixing them.'
-markdown(<<~MARKDOWN)
- ## Disabled eslint rules
+if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Disabled eslint rules
- The following files have disabled `eslint` rules. Please consider fixing them:
+ The following files have disabled `eslint` rules. Please consider fixing them:
- * #{eslint_candidates.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{eslint_candidates.map { |path| "`#{path}`" }.join("\n* ")}
- Run the following command for more details
+ Run the following command for more details
- ```
- node_modules/.bin/eslint --report-unused-disable-directives --no-inline-config \\
- #{eslint_candidates.map { |path| " '#{path}'" }.join(" \\\n")}
- ```
-MARKDOWN
+ ```
+ node_modules/.bin/eslint --report-unused-disable-directives --no-inline-config \\
+ #{eslint_candidates.map { |path| " '#{path}'" }.join(" \\\n")}
+ ```
+ MARKDOWN
+end
diff --git a/danger/frozen_string/Dangerfile b/danger/frozen_string/Dangerfile
index b9687ef6b83..8d3ac3dee68 100644
--- a/danger/frozen_string/Dangerfile
+++ b/danger/frozen_string/Dangerfile
@@ -16,11 +16,13 @@ if files_to_fix.any?
warn 'This merge request adds files that do not enforce frozen string literal. ' \
'See https://gitlab.com/gitlab-org/gitlab-ce/issues/47424 for more information.'
- markdown(<<~MARKDOWN)
- ## Enable Frozen String Literal
+ if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Enable Frozen String Literal
- The following files should have `#{MAGIC_COMMENT}` on the first line:
+ The following files should have `#{MAGIC_COMMENT}` on the first line:
- * #{files_to_fix.map { |path| "`#{path}`" }.join("\n* ")}
- MARKDOWN
+ * #{files_to_fix.map { |path| "`#{path}`" }.join("\n* ")}
+ MARKDOWN
+ end
end
diff --git a/danger/gemfile/Dangerfile b/danger/gemfile/Dangerfile
index dfe64f79d7b..07c4c07cfe8 100644
--- a/danger/gemfile/Dangerfile
+++ b/danger/gemfile/Dangerfile
@@ -1,5 +1,9 @@
-GEMFILE_LOCK_NOT_UPDATED_MESSAGE = <<~MSG.freeze
-**%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.**
+GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT = <<~MSG.freeze
+%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.
+MSG
+
+GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL = <<~MSG.freeze
+**#{GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT}**
Usually, when %<gemfile>s is updated, you should run
```
@@ -19,5 +23,14 @@ gemfile_modified = git.modified_files.include?("Gemfile")
gemfile_lock_modified = git.modified_files.include?("Gemfile.lock")
if gemfile_modified && !gemfile_lock_modified
- warn format(GEMFILE_LOCK_NOT_UPDATED_MESSAGE, gemfile: gitlab.html_link("Gemfile"), gemfile_lock: gitlab.html_link("Gemfile.lock"))
+ gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
+
+ format_str = gitlab_danger.ci? ? GEMFILE_LOCK_NOT_UPDATED_MESSAGE_FULL : GEMFILE_LOCK_NOT_UPDATED_MESSAGE_SHORT
+
+ message = format(format_str,
+ gemfile: gitlab_danger.html_link("Gemfile"),
+ gemfile_lock: gitlab_danger.html_link("Gemfile.lock")
+ )
+
+ warn(message)
end
diff --git a/danger/prettier/Dangerfile b/danger/prettier/Dangerfile
index 37c4b78a213..0be75db8baa 100644
--- a/danger/prettier/Dangerfile
+++ b/danger/prettier/Dangerfile
@@ -19,21 +19,23 @@ return if unpretty.empty?
warn 'This merge request changed frontend files without pretty printing them.'
-markdown(<<~MARKDOWN)
- ## Pretty print Frontend files
+if GitlabDanger.new(helper.gitlab_helper).ci?
+ markdown(<<~MARKDOWN)
+ ## Pretty print Frontend files
- The following files should have been pretty printed with `prettier`:
+ The following files should have been pretty printed with `prettier`:
- * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")}
+ * #{unpretty.map { |path| "`#{path}`" }.join("\n* ")}
- Please run
+ Please run
- ```
- node_modules/.bin/prettier --write \\
- #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")}
- ```
+ ```
+ node_modules/.bin/prettier --write \\
+ #{unpretty.map { |path| " '#{path}'" }.join(" \\\n")}
+ ```
- Also consider auto-formatting [on-save].
+ Also consider auto-formatting [on-save].
- [on-save]: https://docs.gitlab.com/ee/development/new_fe_guide/style/prettier.html
-MARKDOWN
+ [on-save]: https://docs.gitlab.com/ee/development/new_fe_guide/style/prettier.html
+ MARKDOWN
+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 64eca0b00f6..f643d853d10 100644
--- a/doc/administration/database_load_balancing.md
+++ b/doc/administration/database_load_balancing.md
@@ -146,7 +146,11 @@ The following options can be set:
| `use_tcp` | Lookup DNS resources using TCP instead of UDP | false |
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.
+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.
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
@@ -212,28 +216,25 @@ without it immediately leading to errors being presented to the users.
## Logging
-The load balancer logs various messages, such as:
+The load balancer logs various events in
+[`database_load_balancing.log`](logs.md#database_load_balancinglog-premium-only), such as
- When a host is marked as offline
- When a host comes back online
- When all secondaries are offline
+- When a read is retried on a different host due to a query conflict
-Each log message contains the tag `[DB-LB]` to make searching/filtering of such
-log entries easier. For example:
+The log is structured with each entry a JSON object containing at least:
-```
-[DB-LB] Host 10.123.2.5 came back online
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Host 10.123.2.6 came back online
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Marking host 10.123.2.7 as offline
-[DB-LB] Host 10.123.2.7 came back online
-[DB-LB] Host 10.123.2.7 came back online
+- An `event` field useful for filtering.
+- A human-readable `message` field.
+- Some event-specific metadata. For example, `db_host`
+- Contextual information that is always logged. For example, `severity` and `time`.
+
+For example:
+
+```json
+{"severity":"INFO","time":"2019-09-02T12:12:01.728Z","correlation_id":"abcdefg","event":"host_online","message":"Host came back online","db_host":"111.222.333.444","db_port":null,"tag":"rails.database_load_balancing","environment":"production","hostname":"web-example-1","fqdn":"gitlab.example.com","path":null,"params":null}
```
## Handling Stale Reads
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/logs.md b/doc/administration/logs.md
index 4c43a434817..c51b53c596e 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -339,11 +339,11 @@ installations from source.
[Rack Attack]: ../security/rack_attack.md
[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md
-## `database_load_balancing.log`
+## `database_load_balancing.log` **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15442) in GitLab 12.3.
-This log is used for observability of [Database Load Balancing](database_load_balancing.md).
+Contains details of GitLab's [Database Load Balancing](database_load_balancing.md).
It is stored at:
- `/var/log/gitlab/gitlab-rails/database_load_balancing.log` for Omnibus GitLab packages.
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/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/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/examples/README.md b/doc/ci/examples/README.md
index 00995f881da..f9612d0c53d 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -22,7 +22,7 @@ The following table lists examples with step-by-step tutorials that are containe
| Use case | Resource |
|:----------------------------|:---------------------------------------------------------------------------------------------------------------------------|
-| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). |
+| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](../../user/project/merge_requests/browser_performance_testing.md). |
| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
| Deployment with Dpl | [Using `dpl` as deployment tool](deployment/README.md). |
| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index 3266e5dc62e..4a73fe2e62c 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -1,158 +1,5 @@
---
-type: howto
+redirect_to: '../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing'
---
-# Browser Performance Testing with the sitespeed.io container
-
-NOTE: **Note:**
-The job definition shown below is supported on GitLab 11.5 and later versions.
-It also requires the GitLab Runner 11.5 or later.
-For earlier versions, use the [previous job definitions](#previous-job-definitions).
-
-This example shows how to run the
-[sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on
-your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
-using Docker-in-Docker.
-
-First, you need GitLab Runner with
-[docker-in-docker build](../docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
-generates the expected report:
-
-```yaml
-performance:
- stage: performance
- image: docker:git
- variables:
- URL: https://example.com
- services:
- - docker:stable-dind
- script:
- - mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
- - mv sitespeed-results/data/performance.json performance.json
- artifacts:
- paths:
- - sitespeed-results/
- reports:
- performance: performance.json
-```
-
-The above example will create a `performance` job in your CI/CD pipeline and will run
-sitespeed.io against the webpage you defined in `URL` to gather key metrics.
-The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for
-sitespeed.io is downloaded in order to save the report as a
-[Performance report artifact](../yaml/README.md#artifactsreportsperformance-premium)
-that you can later download and analyze.
-Due to implementation limitations we always take the latest Performance artifact available.
-
-The full HTML sitespeed.io report will also be saved as an artifact, and if you have
-[GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed
-directly in your browser.
-
-For further customization options for sitespeed.io, including the ability to
-provide a list of URLs to test, please see the
-[Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) documentation.
-
-TIP: **Tip:**
-For [GitLab Premium](https://about.gitlab.com/pricing/) users, key metrics are automatically
-extracted and shown right in the merge request widget.
-[Learn more on Browser Performance Testing in merge requests](../../user/project/merge_requests/browser_performance_testing.md).
-
-## Performance testing on Review Apps
-
-The above CI YML is great for testing against static environments, and it can
-be extended for dynamic environments. There are a few extra steps to take to
-set this up:
-
-1. The `performance` job should run after the dynamic environment has started.
-1. In the `review` job, persist the hostname and upload it as an artifact so
- it's available to the `performance` job (the same can be done for static
- environments like staging and production to unify the code path). Saving it
- as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
- in your job's `script`.
-1. In the `performance` job, read the previous artifact into an environment
- variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
- URLs.
-1. You can now run the sitespeed.io container against the desired hostname and
- paths.
-
-Your `.gitlab-ci.yml` file would look like:
-
-```yaml
-stages:
- - deploy
- - performance
-
-review:
- stage: deploy
- environment:
- name: review/$CI_COMMIT_REF_SLUG
- url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN
- script:
- - run_deploy_script
- - echo $CI_ENVIRONMENT_URL > environment_url.txt
- artifacts:
- paths:
- - environment_url.txt
- only:
- - branches
- except:
- - master
-
-performance:
- stage: performance
- image: docker:git
- services:
- - docker:stable-dind
- dependencies:
- - review
- script:
- - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
- - mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
- - mv sitespeed-results/data/performance.json performance.json
- artifacts:
- paths:
- - sitespeed-results/
- reports:
- performance: performance.json
-```
-
-A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml).
-
-## Previous job definitions
-
-CAUTION: **Caution:**
-Before GitLab 11.5, Performance job and artifact had to be named specifically
-to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
-and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
-
-For GitLab 11.4 and earlier, the job should look like:
-
-```yaml
-performance:
- stage: performance
- image: docker:git
- variables:
- URL: https://example.com
- services:
- - docker:stable-dind
- script:
- - mkdir gitlab-exporter
- - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
- - mv sitespeed-results/data/performance.json performance.json
- artifacts:
- paths:
- - performance.json
- - sitespeed-results/
-```
+This document was moved to [another location](../../user/project/merge_requests/browser_performance_testing.md#configuring-browser-performance-testing).
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/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index 377ae9717b2..9e18d1d883c 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -178,6 +178,8 @@ the ones defined in the upstream project will take precedence.
### Mirroring status from triggered pipeline
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+
You can mirror the pipeline status from the triggered pipeline to the source
bridge job by using `strategy: depend`. For example:
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 7569ccc04c1..c3165dc2e21 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -191,6 +191,51 @@ end
policies at once. The fields for these will all have be non-nullable
booleans with a default description.
+## Enums
+
+GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the
+following rules apply:
+
+- Values must be uppercase.
+- Class names must end with the string `Enum`.
+- The `graphql_name` must not contain the string `Enum`.
+
+For example:
+
+```ruby
+module Types
+ class TrafficLightStateEnum < BaseEnum
+ graphql_name 'TrafficLightState'
+ description 'State of a traffic light'
+
+ value 'RED', description: 'Drivers must stop'
+ value 'YELLOW', description: 'Drivers must stop when it is safe to'
+ value 'GREEN', description: 'Drivers can start or keep driving'
+ end
+end
+```
+
+If the enum will be used for a class property in Ruby that is not an uppercase string,
+you can provide a `value:` option that will adapt the uppercase value.
+
+In the following example:
+
+- GraphQL inputs of `OPENED` will be converted to `'opened'`.
+- Ruby values of `'opened'` will be converted to `"OPENED"` in GraphQL responses.
+
+```ruby
+module Types
+ class EpicStateEnum < BaseEnum
+ graphql_name 'EpicState'
+ description 'State of a GitLab epic'
+
+ value 'OPENED', value: 'opened', description: 'An open Epic'
+ value 'CLOSED', value: 'closed', description: 'An closed Epic'
+ end
+end
+
+```
+
## Authorization
Authorizations can be applied to both types and fields using the same
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index ee5fc553e27..147bd21e6c7 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -61,8 +61,8 @@ graph TB
Unicorn --> PgBouncer[PgBouncer]
Unicorn --> Redis
Unicorn --> Gitaly
- Redis --> Sidekiq
- Sidekiq["Sidekiq (GitLab Rails, ES Indexer)"] --> PgBouncer
+ Sidekiq --> Redis
+ Sidekiq --> PgBouncer
GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis
GitLabWorkhorse --> Gitaly
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/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/security/reset_root_password.md b/doc/security/reset_root_password.md
index ec360e2d338..00c9dc1407d 100644
--- a/doc/security/reset_root_password.md
+++ b/doc/security/reset_root_password.md
@@ -9,7 +9,7 @@ To reset your root password, first log into your server with root privileges.
Start a Ruby on Rails console with this command:
```bash
-gitlab-rails console production
+gitlab-rails console -e production
```
Wait until the console has loaded.
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/sast/index.md b/doc/user/application_security/sast/index.md
index fbc130689e0..15a21bb82e0 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -205,6 +205,7 @@ Some analyzers can be customized with environment variables.
| `GRADLE_PATH` | spotbugs | Path to the `gradle` executable. |
| `JAVA_OPTS` | spotbugs | Additional arguments for the `java` executable. |
| `JAVA_PATH` | spotbugs | Path to the `java` executable. |
+| `SAST_JAVA_VERSION` | spotbugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. |
| `MAVEN_CLI_OPTS` | spotbugs | Additional arguments for the `mvn` or `mvnw` executable. |
| `MAVEN_PATH` | spotbugs | Path to the `mvn` executable. |
| `MAVEN_REPO_PATH` | spotbugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). |
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 7169dfdf1e1..65b654d1553 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -82,10 +82,12 @@ certificates are valid and up-to-date.
NOTE: **Note:**
The
-[stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
+[jetstack/cert-manager](https://github.com/jetstack/cert-manager)
chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/cert_manager/values.yaml)
-file.
+file. Prior to GitLab 12.3,
+the [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
+chart was used.
### GitLab Runner
@@ -188,7 +190,8 @@ You can clone repositories from the files tab in Jupyter:
### Knative
-> Available for project-level clusters since GitLab 11.5.
+> - Available for project-level clusters since GitLab 11.5.
+> - Available for group-level and instance-level clusters since GitLab 12.3.
[Knative](https://cloud.google.com/knative) provides a platform to
create, deploy, and manage serverless workloads from a Kubernetes
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/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png
index c453c7e558a..90bcff5d31b 100644
--- a/doc/user/project/import/img/import_projects_from_repo_url.png
+++ b/doc/user/project/import/img/import_projects_from_repo_url.png
Binary files differ
diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md
index 49b9826a52a..2339cfa0db8 100644
--- a/doc/user/project/merge_requests/browser_performance_testing.md
+++ b/doc/user/project/merge_requests/browser_performance_testing.md
@@ -4,8 +4,7 @@ type: reference, howto
# Browser Performance Testing **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507)
-in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
If your application offers a web interface and you are using
[GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance
@@ -25,18 +24,20 @@ for [additional metrics](https://gitlab.com/gitlab-org/gitlab-ee/issues/4370)
in a future release.
Going a step further, GitLab can show the Performance report right
-in the merge request widget area:
+in the merge request widget area (see below).
## Use cases
For instance, consider the following workflow:
-1. A member of the marketing team is attempting to track engagement by adding a new tool
-1. With browser performance metrics, they see how their changes are impacting the usability of the page for end users
-1. The metrics show that after their changes the performance score of the page has gone down
-1. When looking at the detailed report, they see that the new Javascript library was included in `<head>` which affects loading page speed
-1. They ask a front end developer to help them, who sets the library to load asynchronously
-1. The frontend developer approves the merge request and authorizes its deployment to production
+1. A member of the marketing team is attempting to track engagement by adding a new tool.
+1. With browser performance metrics, they see how their changes are impacting the usability
+ of the page for end users.
+1. The metrics show that after their changes the performance score of the page has gone down.
+1. When looking at the detailed report, they see that the new JavaScript library was
+ included in `<head>` which affects loading page speed.
+1. They ask a front end developer to help them, who sets the library to load asynchronously.
+1. The frontend developer approves the merge request and authorizes its deployment to production.
## How it works
@@ -48,15 +49,165 @@ example on [Testing Browser Performance](../../../ci/examples/browser_performanc
GitLab then checks this report, compares key performance metrics for each page
between the source and target branches, and shows the information right on the merge request.
->**Note:**
+NOTE: **Note:**
If the Performance report doesn't have anything to compare to, no information
will be displayed in the merge request area. That is the case when you add the
Performance job in your `.gitlab-ci.yml` for the very first time.
-Consecutive merge requests will have something to compare to and the Performance
+Consecutive merge requests will have something to compare to, and the Performance
report will be shown properly.
![Performance Widget](img/browser_performance_testing.png)
+## Configuring Browser Performance Testing
+
+NOTE: **Note:**
+The job definition shown below is supported in GitLab 11.5 and later versions.
+It also requires GitLab Runner 11.5 or later. For earlier versions, use the
+[previous job definitions](#previous-job-definitions).
+
+This example shows how to run the [sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/)
+on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
+using Docker-in-Docker.
+
+First, you need GitLab Runner with
+[docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+
+Once you set up the Runner, add a new job to `.gitlab-ci.yml` that generates the
+expected report:
+
+```yaml
+performance:
+ stage: performance
+ image: docker:git
+ variables:
+ URL: https://example.com
+ services:
+ - docker:stable-dind
+ script:
+ - mkdir gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - sitespeed-results/
+ reports:
+ performance: performance.json
+```
+
+The above example will create a `performance` job in your CI/CD pipeline and will run
+sitespeed.io against the webpage you defined in `URL` to gather key metrics.
+The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance)
+is downloaded in order to save the report as a [Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium)
+that you can later download and analyze. Due to implementation limitations we always
+take the latest Performance artifact available.
+
+The full HTML sitespeed.io report will also be saved as an artifact, and if you have
+[GitLab Pages](../pages/index.md) enabled, it can be viewed directly in your browser.
+
+For further customization options for sitespeed.io, including the ability to provide a
+list of URLs to test, please see the [Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/)
+documentation.
+
+TIP: **Tip:**
+Key metrics are automatically extracted and shown in the merge request widget.
+
+### Performance testing on Review Apps
+
+The above CI YML is great for testing against static environments, and it can
+be extended for dynamic environments. There are a few extra steps to take to
+set this up:
+
+1. The `performance` job should run after the dynamic environment has started.
+1. In the `review` job, persist the hostname and upload it as an artifact so
+ it's available to the `performance` job (the same can be done for static
+ environments like staging and production to unify the code path). Saving it
+ as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
+ in your job's `script`.
+1. In the `performance` job, read the previous artifact into an environment
+ variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
+ URLs.
+1. You can now run the sitespeed.io container against the desired hostname and
+ paths.
+
+Your `.gitlab-ci.yml` file would look like:
+
+```yaml
+stages:
+ - deploy
+ - performance
+
+review:
+ stage: deploy
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN
+ script:
+ - run_deploy_script
+ - echo $CI_ENVIRONMENT_URL > environment_url.txt
+ artifacts:
+ paths:
+ - environment_url.txt
+ only:
+ - branches
+ except:
+ - master
+
+performance:
+ stage: performance
+ image: docker:git
+ services:
+ - docker:stable-dind
+ dependencies:
+ - review
+ script:
+ - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+ - mkdir gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - sitespeed-results/
+ reports:
+ performance: performance.json
+```
+
+A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml).
+
+### Previous job definitions
+
+CAUTION: **Caution:**
+Before GitLab 11.5, Performance job and artifact had to be named specifically
+to automatically extract report data and show it in the merge request widget.
+While these old job definitions are still maintained they have been deprecated
+and may be removed in next major release, GitLab 12.0.
+You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+performance:
+ stage: performance
+ image: docker:git
+ variables:
+ URL: https://example.com
+ services:
+ - docker:stable-dind
+ script:
+ - mkdir gitlab-exporter
+ - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
+ - mv sitespeed-results/data/performance.json performance.json
+ artifacts:
+ paths:
+ - performance.json
+ - sitespeed-results/
+```
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
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 d6da8cb99c7..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.
@@ -289,6 +309,7 @@ as pushing changes:
- Set the merge request to remove the source branch when it's merged.
- Set the title of the merge request to a particular title.
- Set the description of the merge request to a particular description.
+- Add or remove labels from the merge request.
### Create a new merge request using git push options
@@ -375,6 +396,35 @@ git push -o merge_request.description="The description I want"
You can also use this push option in addition to the
`merge_request.create` push option.
+### 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
+`merge_request.label` push option:
+
+```sh
+git push -o merge_request.label="label1" -o merge_request.label="label2"
+```
+
+To remove two labels from an existing merge request, use
+the `merge_request.unlabel` push option:
+
+```sh
+git push -o merge_request.unlabel="label1" -o merge_request.unlabel="label2"
+```
+
+You can also use these push options in addition to the
+`merge_request.create` push option.
+
+To create a merge request and add two labels to it, use:
+
+```sh
+git push -o merge_request.create -o merge_request.label="label1" -o merge_request.label="label2"
+```
+
## Find the merge request that introduced a change
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5.
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/img/repository_mirroring_push_settings.png b/doc/workflow/img/repository_mirroring_push_settings.png
index 21a6aca4526..3c0eacaa2df 100644
--- a/doc/workflow/img/repository_mirroring_push_settings.png
+++ b/doc/workflow/img/repository_mirroring_push_settings.png
Binary files differ
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index f5593927cc2..36db4f73885 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -62,9 +62,8 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
-NOTE: **Note:**
**Make sure** that `.gitattributes` is tracked by Git. Otherwise Git
-LFS will not be working properly for people cloning the project.
+LFS will not be working properly for people cloning the project:
```bash
git add .gitattributes
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 753518d0424..5bf1f484106 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -37,7 +37,8 @@ The following are some possible use cases for repository mirroring:
## Pushing to a remote repository **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7.
+> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
For an existing project, you can set up push mirroring as follows:
@@ -66,7 +67,8 @@ section.
### Push only protected branches **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
You can choose to only push your protected branches from GitLab to your remote repository.
@@ -96,7 +98,8 @@ The repository will push soon. To force a push, click the appropriate button.
## Pulling from a remote repository **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2.
+> - [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.
@@ -154,7 +157,8 @@ Repository mirrors are updated as Sidekiq becomes available to process them. If
### SSH authentication
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
SSH authentication is mutual:
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/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/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/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 17ad07bfc0c..e2911b4e6c8 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -38,8 +38,17 @@ module Gitlab
ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md')
end
+ def gitlab_helper
+ # Unfortunately the following does not work:
+ # - respond_to?(:gitlab)
+ # - respond_to?(:gitlab, true)
+ gitlab
+ rescue NoMethodError
+ nil
+ end
+
def release_automation?
- gitlab.mr_author == RELEASE_TOOLS_BOT
+ gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
end
def project_name
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index 42cd94add79..13883ca7f3d 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -3,35 +3,19 @@
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] || {}
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?
- end
-
- def find_included(value)
- key = key_from_hash(value)
- @included_attributes[key].nil? ? {} : { only: @included_attributes[key] }
- end
-
- def find_excluded(value)
- key = key_from_hash(value)
- @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] }
- end
-
- def find_method(value)
- key = key_from_hash(value)
- @methods[key].nil? ? {} : { methods: @methods[key] }
+ def find_relations_tree(model_key)
+ @tree[model_key]
end
def find_excluded_keys(klass_name)
@@ -40,12 +24,24 @@ module Gitlab
private
- def find_attributes_only(value)
- find_included(value).merge(find_excluded(value)).merge(find_method(value))
+ 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)
+ }.compact
+ end
+
+ def resolve_model_tree(model_tree)
+ return unless model_tree
+
+ 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/import_export.yml b/lib/gitlab/import_export/import_export.yml
index bd0f3e70749..06c94beead8 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,15 @@ methods:
- :type
lists:
- :list_type
+ ci_pipelines:
+ - :notes
# 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..f1b3db6b208 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,25 @@ 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
+ @project.as_json(reader.project_tree)
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/patch/chronic_duration.rb b/lib/gitlab/patch/chronic_duration.rb
new file mode 100644
index 00000000000..ab3cba3657f
--- /dev/null
+++ b/lib/gitlab/patch/chronic_duration.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# Fixes a bug where parsing months doesn't take into account
+# the ChronicDuration.days_per_week setting
+#
+# We can remove this when we do a refactor and push upstream in
+# https://gitlab.com/gitlab-org/gitlab-ce/issues/66637
+
+module Gitlab
+ module Patch
+ module ChronicDuration
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def duration_units_seconds_multiplier(unit)
+ return 0 unless duration_units_list.include?(unit)
+
+ case unit
+ when 'months'
+ 3600 * ::ChronicDuration.hours_per_day * ::ChronicDuration.days_per_month
+ else
+ super
+ end
+ end
+
+ # ChronicDuration#output uses 1mo = 4w as the conversion so we do the same here.
+ # We do need to add a special case for the default days_per_week value because
+ # we want to retain existing behavior for the default case
+ def days_per_month
+ ::ChronicDuration.days_per_week == 7 ? 30 : ::ChronicDuration.days_per_week * 4
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
index 682edfc4259..a2296d265cd 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -7,10 +7,12 @@ module Gitlab
keys: [
:create,
:description,
+ :label,
:merge_when_pipeline_succeeds,
:remove_source_branch,
:target,
- :title
+ :title,
+ :unlabel
]
},
ci: {
@@ -18,6 +20,11 @@ module Gitlab
}
}).freeze
+ MULTI_VALUE_OPTIONS = [
+ %w[merge_request label],
+ %w[merge_request unlabel]
+ ].freeze
+
NAMESPACE_ALIASES = HashWithIndifferentAccess.new({
mr: :merge_request
}).freeze
@@ -50,12 +57,22 @@ module Gitlab
next if [namespace, key].any?(&:nil?)
options[namespace] ||= HashWithIndifferentAccess.new
- options[namespace][key] = value
+
+ if option_multi_value?(namespace, key)
+ options[namespace][key] ||= HashWithIndifferentAccess.new(0)
+ options[namespace][key][value] += 1
+ else
+ options[namespace][key] = value
+ end
end
options
end
+ def option_multi_value?(namespace, key)
+ MULTI_VALUE_OPTIONS.any? { |arr| arr == [namespace, key] }
+ end
+
def parse_option(option)
parts = OPTION_MATCHER.match(option)
return unless parts
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_danger.rb b/lib/gitlab_danger.rb
new file mode 100644
index 00000000000..b4768a9546d
--- /dev/null
+++ b/lib/gitlab_danger.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class GitlabDanger
+ LOCAL_RULES ||= %w[
+ changes_size
+ gemfile
+ documentation
+ frozen_string
+ duplicate_yarn_dependencies
+ prettier
+ eslint
+ database
+ ].freeze
+
+ CI_ONLY_RULES ||= %w[
+ metadata
+ changelog
+ specs
+ commit_messages
+ roulette
+ single_codebase
+ gitlab_ui_wg
+ ce_ee_vue_templates
+ only_documentation
+ ].freeze
+
+ MESSAGE_PREFIX = '==>'.freeze
+
+ attr_reader :gitlab_danger_helper
+
+ def initialize(gitlab_danger_helper)
+ @gitlab_danger_helper = gitlab_danger_helper
+ end
+
+ def self.local_warning_message
+ "#{MESSAGE_PREFIX} Only the following Danger rules can be run locally: #{LOCAL_RULES.join(', ')}"
+ end
+
+ def self.success_message
+ "#{MESSAGE_PREFIX} No Danger rule violations!"
+ end
+
+ def rule_names
+ ci? ? LOCAL_RULES | CI_ONLY_RULES : LOCAL_RULES
+ end
+
+ def html_link(str)
+ self.ci? ? gitlab_danger_helper.html_link(str) : str
+ end
+
+ def ci?
+ !gitlab_danger_helper.nil?
+ end
+end
diff --git a/lib/tasks/gitlab_danger.rake b/lib/tasks/gitlab_danger.rake
new file mode 100644
index 00000000000..e75539f048c
--- /dev/null
+++ b/lib/tasks/gitlab_danger.rake
@@ -0,0 +1,17 @@
+desc 'Run local Danger rules'
+task :danger_local do
+ require 'gitlab_danger'
+ require 'gitlab/popen'
+
+ puts("#{GitlabDanger.local_warning_message}\n")
+
+ # _status will _always_ be 0, regardless of failure or success :(
+ output, _status = Gitlab::Popen.popen(%w{danger dry_run})
+
+ if output.empty?
+ puts(GitlabDanger.success_message)
+ else
+ puts(output)
+ exit(1)
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e15000b5184..f2277dc3446 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2035,10 +2035,10 @@ msgstr ""
msgid "Certificate (PEM)"
msgstr ""
-msgid "Change Label"
+msgid "Change assignee"
msgstr ""
-msgid "Change assignee"
+msgid "Change label"
msgstr ""
msgid "Change milestone"
@@ -3019,6 +3019,9 @@ msgstr ""
msgid "Comment is being updated"
msgstr ""
+msgid "Comment/Reply (quoting selected text)"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -4189,9 +4192,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 +4213,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 +4549,9 @@ msgstr ""
msgid "Epic"
msgstr ""
+msgid "Epics (Ultimate / Gold license only)"
+msgstr ""
+
msgid "Error"
msgstr ""
@@ -5100,9 +5109,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 +5133,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 +5376,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 +5400,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,45 +5431,60 @@ 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 ""
@@ -6233,6 +6252,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 ""
@@ -7209,7 +7231,7 @@ msgstr ""
msgid "Monday"
msgstr ""
-msgid "Monitor your errors by integrating with Sentry"
+msgid "Monitor your errors by integrating with Sentry."
msgstr ""
msgid "Monitoring"
@@ -7254,18 +7276,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 +7336,6 @@ msgstr ""
msgid "Network"
msgstr ""
-msgid "Network Graph"
-msgstr ""
-
msgid "Never"
msgstr ""
@@ -7451,6 +7458,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 +8459,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 +8843,7 @@ msgstr ""
msgid "Project Badges"
msgstr ""
-msgid "Project File"
-msgstr ""
-
-msgid "Project Files browsing"
+msgid "Project Files"
msgstr ""
msgid "Project ID"
@@ -9621,9 +9637,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 +9685,9 @@ msgstr ""
msgid "Repository"
msgstr ""
+msgid "Repository Graph"
+msgstr ""
+
msgid "Repository Settings"
msgstr ""
@@ -10499,9 +10515,6 @@ msgstr ""
msgid "Show whitespace changes"
msgstr ""
-msgid "Show/hide this dialog"
-msgstr ""
-
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
@@ -10936,6 +10949,9 @@ msgstr ""
msgid "Start date"
msgstr ""
+msgid "Start search"
+msgstr ""
+
msgid "Start the Runner!"
msgstr ""
@@ -12274,6 +12290,9 @@ msgstr ""
msgid "Toggle the Performance Bar"
msgstr ""
+msgid "Toggle this dialog"
+msgstr ""
+
msgid "Toggle thread"
msgstr ""
@@ -13023,6 +13042,9 @@ msgid_plural "When these merge requests are accepted"
msgstr[0] ""
msgstr[1] ""
+msgid "When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed."
+msgstr ""
+
msgid "When:"
msgstr ""
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/plugins/.gitignore b/plugins/.gitignore
new file mode 100644
index 00000000000..e4ccdc9e2ec
--- /dev/null
+++ b/plugins/.gitignore
@@ -0,0 +1,5 @@
+*
+!*/
+!.gitignore
+!.gitkeep
+!examples/*
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/shared/.gitignore b/shared/.gitignore
new file mode 100644
index 00000000000..5c0276adafd
--- /dev/null
+++ b/shared/.gitignore
@@ -0,0 +1,4 @@
+*
+!*/
+!.gitignore
+!.gitkeep
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/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/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/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/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 67e5dc399ac..ce8b8908026 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -11,19 +11,24 @@ describe('ErrorTrackingList', () => {
let wrapper;
let actions;
- function mountComponent({ errorTrackingEnabled = true } = {}) {
+ function mountComponent({
+ errorTrackingEnabled = true,
+ userCanEnableErrorTracking = true,
+ stubs = {
+ 'gl-link': GlLink,
+ },
+ } = {}) {
wrapper = shallowMount(ErrorTrackingList, {
localVue,
store,
propsData: {
indexPath: '/path',
enableErrorTrackingLink: '/link',
+ userCanEnableErrorTracking,
errorTrackingEnabled,
illustrationPath: 'illustration/path',
},
- stubs: {
- 'gl-link': GlLink,
- },
+ stubs,
});
}
@@ -115,4 +120,23 @@ describe('ErrorTrackingList', () => {
expect(wrapper.find(GlButton).exists()).toBeFalsy();
});
});
+
+ describe('When error tracking is disabled and user is not allowed to enable it', () => {
+ beforeEach(() => {
+ mountComponent({
+ errorTrackingEnabled: false,
+ userCanEnableErrorTracking: false,
+ stubs: {
+ 'gl-link': GlLink,
+ 'gl-empty-state': GlEmptyState,
+ },
+ });
+ });
+
+ it('shows empty state', () => {
+ expect(wrapper.find('a').attributes('href')).toBe(
+ '/help/user/project/operations/error_tracking.html',
+ );
+ });
+ });
});
diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb
index 7516a636c93..064b3ad21cb 100644
--- a/spec/helpers/projects/error_tracking_helper_spec.rb
+++ b/spec/helpers/projects/error_tracking_helper_spec.rb
@@ -6,21 +6,31 @@ describe Projects::ErrorTrackingHelper do
include Gitlab::Routing.url_helpers
set(:project) { create(:project) }
+ set(:current_user) { create(:user) }
describe '#error_tracking_data' do
+ let(:can_enable_error_tracking) { true }
let(:setting_path) { project_settings_operations_path(project) }
let(:index_path) do
project_error_tracking_index_path(project, format: :json)
end
+ before do
+ allow(helper)
+ .to receive(:can?)
+ .with(current_user, :admin_operations, project)
+ .and_return(can_enable_error_tracking)
+ end
+
context 'without error_tracking_setting' do
it 'returns frontend configuration' do
- expect(error_tracking_data(project)).to eq(
+ expect(helper.error_tracking_data(current_user, project)).to match(
'index-path' => index_path,
+ 'user-can-enable-error-tracking' => 'true',
'enable-error-tracking-link' => setting_path,
'error-tracking-enabled' => 'false',
- "illustration-path" => "/images/illustrations/cluster_popover.svg"
+ 'illustration-path' => match_asset_path('/assets/illustrations/cluster_popover.svg')
)
end
end
@@ -36,7 +46,7 @@ describe Projects::ErrorTrackingHelper do
end
it 'show error tracking enabled' do
- expect(error_tracking_data(project)).to include(
+ expect(helper.error_tracking_data(current_user, project)).to include(
'error-tracking-enabled' => 'true'
)
end
@@ -48,11 +58,21 @@ describe Projects::ErrorTrackingHelper do
end
it 'show error tracking not enabled' do
- expect(error_tracking_data(project)).to include(
+ expect(helper.error_tracking_data(current_user, project)).to include(
'error-tracking-enabled' => 'false'
)
end
end
end
+
+ context 'when user is not maintainer' do
+ let(:can_enable_error_tracking) { false }
+
+ it 'shows error tracking enablement as disabled' do
+ expect(helper.error_tracking_data(current_user, project)).to include(
+ 'user-can-enable-error-tracking' => 'false'
+ )
+ end
+ end
end
end
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/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..208b60844e3
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb
@@ -0,0 +1,195 @@
+# 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: []
+ } },
+ { merge_request_test: { include: [] } }
+ ],
+ only: [:id]
+ } },
+ { commit_statuses: {
+ include: [{ commit: { include: [] } }]
+ } },
+ { project_members: {
+ include: [{ user: { include: [],
+ 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
+ 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: [] } }]
+ )
+ 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: [] } }]
+ )
+ 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: [] } }]
+ )
+ 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: [] } }] } }]
+ )
+ 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: [] } }] } }]
+ )
+ 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: [] } }] } }]
+ } }]
+ )
+ 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] } }]
+ )
+ 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: [] } }]
+ )
+ 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] } }]
+ )
+ 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] } }]
+ )
+ 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..e53db37def4 100644
--- a/spec/lib/gitlab/import_export/config_spec.rb
+++ b/spec/lib/gitlab/import_export/config_spec.rb
@@ -1,163 +1,159 @@
# 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)
+ 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
+
+ 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
+ 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]
}
}
- })
-
- 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]
}
}
- })
-
- 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/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/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/patch/chronic_duration_spec.rb b/spec/lib/gitlab/patch/chronic_duration_spec.rb
new file mode 100644
index 00000000000..541037ec1a2
--- /dev/null
+++ b/spec/lib/gitlab/patch/chronic_duration_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Patch::ChronicDuration do
+ subject { ChronicDuration.parse('1mo') }
+
+ it 'uses default conversions' do
+ expect(subject).to eq(2_592_000)
+ end
+
+ context 'with custom conversions' do
+ before do
+ ChronicDuration.hours_per_day = 8
+ ChronicDuration.days_per_week = 5
+ end
+
+ after do
+ ChronicDuration.hours_per_day = 24
+ ChronicDuration.days_per_week = 7
+ end
+
+ it 'uses custom conversions' do
+ expect(subject).to eq(576_000)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb
index a85d418777f..cfc804c13a7 100644
--- a/spec/lib/gitlab/time_tracking_formatter_spec.rb
+++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb
@@ -17,6 +17,14 @@ describe Gitlab::TimeTrackingFormatter do
it { expect(subject).to eq(-12_000) }
end
+
+ context 'durations with months' do
+ let(:duration_string) { '1mo' }
+
+ it 'uses our custom conversions' do
+ expect(subject).to eq(576_000)
+ end
+ end
end
describe '#output' do
diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb
new file mode 100644
index 00000000000..623ac20fa7c
--- /dev/null
+++ b/spec/lib/gitlab_danger_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabDanger do
+ let(:gitlab_danger_helper) { nil }
+
+ subject { described_class.new(gitlab_danger_helper) }
+
+ describe '.local_warning_message' do
+ it 'returns an informational message with rules that can run' do
+ expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database')
+ end
+ end
+
+ describe '.success_message' do
+ it 'returns an informational success message' do
+ expect(described_class.success_message).to eq('==> No Danger rule violations!')
+ end
+ end
+
+ describe '#rule_names' do
+ context 'when running locally' do
+ it 'returns local only rules' do
+ expect(subject.rule_names).to eq(described_class::LOCAL_RULES)
+ end
+ end
+
+ context 'when running under CI' do
+ let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
+
+ it 'returns all rules' do
+ expect(subject.rule_names).to eq(described_class::LOCAL_RULES | described_class::CI_ONLY_RULES)
+ end
+ end
+ end
+
+ describe '#html_link' do
+ context 'when running locally' do
+ it 'returns the same string' do
+ str = 'something'
+
+ expect(subject.html_link(str)).to eq(str)
+ end
+ end
+
+ context 'when running under CI' do
+ let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
+
+ it 'returns a HTML link formatted version of the string' do
+ str = 'something'
+ html_formatted_str = %Q{<a href="#{str}">#{str}</a>}
+
+ expect(gitlab_danger_helper).to receive(:html_link).with(str).and_return(html_formatted_str)
+
+ expect(subject.html_link(str)).to eq(html_formatted_str)
+ end
+ end
+ end
+
+ describe '#ci?' do
+ context 'when gitlab_danger_helper is not available' do
+ it 'returns false' do
+ expect(subject.ci?).to be_falsey
+ end
+ end
+
+ context 'when gitlab_danger_helper is available' do
+ let(:gitlab_danger_helper) { double('danger_gitlab_helper') }
+
+ it 'returns true' do
+ expect(subject.ci?).to be_truthy
+ end
+ end
+ end
+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/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/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 478f09a7881..cf459ba99c1 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -20,11 +20,6 @@ describe 'Rack Attack global throttles' do
let(:period_in_seconds) { 10000 }
let(:period) { period_in_seconds.seconds }
- let(:url_that_does_not_require_authentication) { '/users/sign_in' }
- let(:url_that_requires_authentication) { '/dashboard/snippets' }
- let(:url_api_internal) { '/api/v4/internal/check' }
- let(:api_partial_url) { '/todos' }
-
around do |example|
# Instead of test environment's :null_store so the throttles can increment
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
@@ -35,112 +30,10 @@ describe 'Rack Attack global throttles' do
Rack::Attack.cache.store = Rails.cache
end
- # Requires let variables:
- # * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
- # * get_args
- # * other_user_get_args
- shared_examples_for 'rate-limited token-authenticated requests' do
- before do
- # Set low limits
- settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
- settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
- end
-
- context 'when the throttle is enabled' do
- before do
- settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
- stub_application_setting(settings_to_set)
- end
-
- it 'rejects requests over the rate limit' do
- # At first, allow requests under the rate limit.
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- # the last straw
- expect_rejection { get(*get_args) }
- end
-
- it 'allows requests after throttling and then waiting for the next period' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get(*get_args) }
-
- Timecop.travel(period.from_now) do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get(*get_args) }
- end
- end
-
- it 'counts requests from different users separately, even from the same IP' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- # would be over the limit if this wasn't a different user
- get(*other_user_get_args)
- expect(response).to have_http_status 200
- end
-
- it 'counts all requests from the same user, even via different IPs' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
-
- expect_rejection { get(*get_args) }
- end
-
- it 'logs RackAttack info into structured logs' do
- requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
-
- arguments = {
- message: 'Rack_Attack',
- env: :throttle,
- remote_ip: '127.0.0.1',
- request_method: 'GET',
- path: get_args.first,
- user_id: user.id,
- username: user.username
- }
-
- expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
-
- expect_rejection { get(*get_args) }
- end
- end
-
- context 'when the throttle is disabled' do
- before do
- settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
- stub_application_setting(settings_to_set)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- get(*get_args)
- expect(response).to have_http_status 200
- end
- end
- end
- end
-
describe 'unauthenticated requests' do
+ let(:url_that_does_not_require_authentication) { '/users/sign_in' }
+ let(:url_api_internal) { '/api/v4/internal/check' }
+
before do
# Set low limits
settings_to_set[:throttle_unauthenticated_requests_per_period] = requests_per_period
@@ -245,6 +138,7 @@ describe 'Rack Attack global throttles' do
let(:other_user) { create(:user) }
let(:other_user_token) { create(:personal_access_token, user: other_user) }
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+ let(:api_partial_url) { '/todos' }
context 'with the token in the query string' do
let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
@@ -265,10 +159,13 @@ describe 'Rack Attack global throttles' do
let(:user) { create(:user) }
let(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let(:token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") }
+
let(:other_user) { create(:user) }
let(:other_user_application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: other_user) }
let(:other_user_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: other_user.id, scopes: "api") }
+
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
+ let(:api_partial_url) { '/todos' }
context 'with the token in the query string' do
let(:get_args) { [api(api_partial_url, oauth_access_token: token)] }
@@ -299,110 +196,11 @@ describe 'Rack Attack global throttles' do
end
describe 'web requests authenticated with regular login' do
+ let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
let(:user) { create(:user) }
+ let(:url_that_requires_authentication) { '/dashboard/snippets' }
- before do
- login_as(user)
-
- # Set low limits
- settings_to_set[:throttle_authenticated_web_requests_per_period] = requests_per_period
- settings_to_set[:throttle_authenticated_web_period_in_seconds] = period_in_seconds
- end
-
- context 'when the throttle is enabled' do
- before do
- settings_to_set[:throttle_authenticated_web_enabled] = true
- stub_application_setting(settings_to_set)
- end
-
- it 'rejects requests over the rate limit' do
- # At first, allow requests under the rate limit.
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- # the last straw
- expect_rejection { get url_that_requires_authentication }
- end
-
- it 'allows requests after throttling and then waiting for the next period' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get url_that_requires_authentication }
-
- Timecop.travel(period.from_now) do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- expect_rejection { get url_that_requires_authentication }
- end
- end
-
- it 'counts requests from different users separately, even from the same IP' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- # would be over the limit if this wasn't a different user
- login_as(create(:user))
-
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- it 'counts all requests from the same user, even via different IPs' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
-
- expect_rejection { get url_that_requires_authentication }
- end
-
- it 'logs RackAttack info into structured logs' do
- requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
-
- arguments = {
- message: 'Rack_Attack',
- env: :throttle,
- remote_ip: '127.0.0.1',
- request_method: 'GET',
- path: '/dashboard/snippets',
- user_id: user.id,
- username: user.username
- }
-
- expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
-
- get url_that_requires_authentication
- end
- end
-
- context 'when the throttle is disabled' do
- before do
- settings_to_set[:throttle_authenticated_web_enabled] = false
- stub_application_setting(settings_to_set)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
- end
- end
- end
+ it_behaves_like 'rate-limited web authenticated requests'
end
def api_get_args_with_token_headers(partial_url, token_headers)
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..d8880819d9f 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
@@ -969,6 +973,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,
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/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index a27fea0c90f..ff4cdd3e7e2 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -13,6 +13,9 @@ describe MergeRequests::PushOptionsHandlerService do
let(:target_branch) { 'feature' }
let(:title) { 'my title' }
let(:description) { 'my description' }
+ let(:label1) { 'mylabel1' }
+ let(:label2) { 'mylabel2' }
+ let(:label3) { 'mylabel3' }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
@@ -122,6 +125,16 @@ describe MergeRequests::PushOptionsHandlerService do
end
end
+ shared_examples_for 'a service that can change labels of a merge request' do |count|
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'changes label count' do
+ service.execute
+
+ expect(last_mr.label_ids.count).to eq(count)
+ end
+ end
+
shared_examples_for 'a service that does not create a merge request' do
it do
expect { service.execute }.not_to change { MergeRequest.count }
@@ -504,6 +517,138 @@ describe MergeRequests::PushOptionsHandlerService do
end
end
+ describe '`label` push option' do
+ let(:push_options) { { label: { label1 => 1, label2 => 1 } } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 2
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 2
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 2
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`unlabel` push option' do
+ let(:push_options) { { label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 1
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, label: { label1 => 1, label2 => 1 }, unlabel: { label1 => 1, label3 => 1 } } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 1
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can change labels of a merge request', 1
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
describe 'multiple pushed branches' do
let(:push_options) { { create: true } }
let(:changes) 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/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..ed2a3243f0d 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -1,13 +1,16 @@
# 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
included_attributes:
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
new file mode 100644
index 00000000000..afc6f59b773
--- /dev/null
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+#
+# Requires let variables:
+# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths"
+# * get_args
+# * other_user_get_args
+# * requests_per_period
+# * period_in_seconds
+# * period
+shared_examples_for 'rate-limited token-authenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get(*get_args) }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ get(*other_user_get_args)
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get(*get_args) }
+ end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ remote_ip: '127.0.0.1',
+ request_method: 'GET',
+ path: get_args.first,
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
+
+ expect_rejection { get(*get_args) }
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get(*get_args)
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+end
+
+# Requires let variables:
+# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths"
+# * user
+# * url_that_requires_authentication
+# * requests_per_period
+# * period_in_seconds
+# * period
+shared_examples_for 'rate-limited web authenticated requests' do
+ before do
+ login_as(user)
+
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # the last straw
+ expect_rejection { get url_that_requires_authentication }
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+
+ Timecop.travel(period.from_now) do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+ end
+
+ it 'counts requests from different users separately, even from the same IP' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ # would be over the limit if this wasn't a different user
+ login_as(create(:user))
+
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ it 'counts all requests from the same user, even via different IPs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ expect_rejection { get url_that_requires_authentication }
+ end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ arguments = {
+ message: 'Rack_Attack',
+ env: :throttle,
+ remote_ip: '127.0.0.1',
+ request_method: 'GET',
+ path: '/dashboard/snippets',
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
+
+ get url_that_requires_authentication
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+ end
+ end
+end
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/tmp/.gitignore b/tmp/.gitignore
new file mode 100644
index 00000000000..a383b23fb78
--- /dev/null
+++ b/tmp/.gitignore
@@ -0,0 +1,5 @@
+*
+!*/
+!.gitignore
+!.gitkeep
+/tests/
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"