summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-13 15:07:43 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-13 15:07:43 +0000
commit7eca3f56625526ffa7f263c1fef0fcea34de8ca6 (patch)
treefec87c2a902e3c44f89963f4b28e6de32c0806f3
parent988424215cf104d9ee24bb1751141424cffb32d1 (diff)
downloadgitlab-ce-7eca3f56625526ffa7f263c1fef0fcea34de8ca6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo.yml27
-rw-r--r--.rubocop_todo/database/multiple_databases.yml5
-rw-r--r--.rubocop_todo/performance/constant_regexp.yml32
-rw-r--r--.rubocop_todo/rails/index_with.yml52
-rw-r--r--.rubocop_todo/rspec/scattered_let.yml285
-rw-r--r--.rubocop_todo/style/bare_percent_literals.yml115
-rw-r--r--.rubocop_todo/style/rescue_modifier.yml51
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js29
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar.vue6
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_button.vue74
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js53
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js108
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js98
-rw-r--r--app/assets/javascripts/editor/graphql/add_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/editor/graphql/get_item.query.graphql9
-rw-r--r--app/assets/javascripts/editor/graphql/remove_items.mutation.graphql3
-rw-r--r--app/assets/javascripts/editor/graphql/typedefs.graphql23
-rw-r--r--app/assets/javascripts/editor/graphql/update_item.mutation.graphql2
-rw-r--r--app/assets/javascripts/flash.js3
-rw-r--r--app/assets/javascripts/group.js37
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue16
-rw-r--r--app/assets/javascripts/notes/components/comment_type_dropdown.vue48
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue25
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue15
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue20
-rw-r--r--app/assets/javascripts/notes/i18n.js14
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js2
-rw-r--r--app/controllers/admin/labels_controller.rb1
-rw-r--r--app/controllers/autocomplete_controller.rb1
-rw-r--r--app/controllers/boards/issues_controller.rb1
-rw-r--r--app/controllers/boards/lists_controller.rb1
-rw-r--r--app/controllers/dashboard/labels_controller.rb1
-rw-r--r--app/controllers/dashboard/milestones_controller.rb1
-rw-r--r--app/controllers/dashboard/todos_controller.rb1
-rw-r--r--app/controllers/dashboard_controller.rb1
-rw-r--r--app/controllers/groups/autocomplete_sources_controller.rb1
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/groups/crm/contacts_controller.rb1
-rw-r--r--app/controllers/groups/crm/organizations_controller.rb1
-rw-r--r--app/controllers/groups/labels_controller.rb1
-rw-r--r--app/controllers/groups/milestones_controller.rb1
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/profiles/notifications_controller.rb1
-rw-r--r--app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb1
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb1
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb1
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb1
-rw-r--r--app/controllers/projects/discussions_controller.rb1
-rw-r--r--app/controllers/projects/issue_links_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb7
-rw-r--r--app/controllers/projects/labels_controller.rb1
-rw-r--r--app/controllers/projects/milestones_controller.rb1
-rw-r--r--app/controllers/projects/notes_controller.rb1
-rw-r--r--app/controllers/projects/todos_controller.rb1
-rw-r--r--app/controllers/projects/work_items_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/controllers/sent_notifications_controller.rb1
-rw-r--r--app/models/group_group_link.rb2
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/services/ci/job_artifacts/create_service.rb2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--config/feature_flags/development/ci_fix_rules_if_comparison_with_regexp_variable.yml8
-rw-r--r--config/feature_flags/development/omit_epic_subscribed.yml2
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--db/migrate/20220511151646_add_exclude_from_free_user_cap_to_namespace_settings.rb13
-rw-r--r--db/schema_migrations/202205111516461
-rw-r--r--db/structure.sql1
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt6
-rw-r--r--doc/administration/geo/glossary.md4
-rw-r--r--doc/administration/geo/replication/troubleshooting.md4
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md4
-rw-r--r--doc/administration/gitaly/praefect.md2
-rw-r--r--doc/administration/nfs.md2
-rw-r--r--doc/administration/operations/extra_sidekiq_processes.md2
-rw-r--r--doc/administration/package_information/licensing.md2
-rw-r--r--doc/administration/troubleshooting/elasticsearch.md2
-rw-r--r--doc/administration/troubleshooting/sidekiq.md2
-rw-r--r--doc/api/index.md2
-rw-r--r--doc/api/issues.md2
-rw-r--r--doc/api/merge_requests.md8
-rw-r--r--doc/api/pages_domains.md2
-rw-r--r--doc/api/settings.md6
-rw-r--r--doc/architecture/blueprints/database/scalability/patterns/time_decay.md6
-rw-r--r--doc/ci/examples/semantic-release.md2
-rw-r--r--doc/ci/services/index.md2
-rw-r--r--doc/ci/yaml/index.md2
-rw-r--r--doc/development/adding_database_indexes.md9
-rw-r--r--doc/development/code_intelligence/index.md2
-rw-r--r--doc/development/code_review.md8
-rw-r--r--doc/development/contributing/design.md4
-rw-r--r--doc/development/contributing/merge_request_workflow.md4
-rw-r--r--doc/development/database/avoiding_downtime_in_migrations.md2
-rw-r--r--doc/development/database/loose_foreign_keys.md4
-rw-r--r--doc/development/database/table_partitioning.md2
-rw-r--r--doc/development/fe_guide/graphql.md2
-rw-r--r--doc/development/migration_style_guide.md5
-rw-r--r--doc/development/workhorse/index.md2
-rw-r--r--doc/install/docker.md2
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/update/index.md11
-rw-r--r--doc/user/search/img/issue_search_by_id.pngbin20577 -> 0 bytes
-rw-r--r--doc/user/search/img/issue_search_by_id_v15_0.pngbin0 -> 15890 bytes
-rw-r--r--doc/user/search/index.md2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/boards.rb1
-rw-r--r--lib/api/ci/runner.rb2
-rw-r--r--lib/api/group_boards.rb1
-rw-r--r--lib/api/group_labels.rb1
-rw-r--r--lib/api/group_milestones.rb1
-rw-r--r--lib/api/integrations/jira_connect/subscriptions.rb51
-rw-r--r--lib/api/issue_links.rb1
-rw-r--r--lib/api/issues.rb1
-rw-r--r--lib/api/labels.rb1
-rw-r--r--lib/api/notification_settings.rb1
-rw-r--r--lib/api/project_milestones.rb1
-rw-r--r--lib/api/sidekiq_metrics.rb3
-rw-r--r--lib/api/todos.rb1
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb7
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb7
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb12
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/value.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml30
-rw-r--r--lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml20
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml24
-rw-r--r--lib/gitlab/database.rb17
-rw-r--r--lib/gitlab/database/load_balancing/load_balancer.rb2
-rw-r--r--lib/gitlab/database/migrations/observers/query_log.rb2
-rw-r--r--lib/gitlab/sidekiq_config.rb14
-rw-r--r--lib/gitlab/sidekiq_config/dummy_worker.rb4
-rw-r--r--lib/gitlab/sidekiq_config/worker.rb2
-rw-r--r--lib/tasks/gitlab/db/validate_config.rake4
-rw-r--r--locale/gitlab.pot54
-rw-r--r--spec/features/groups_spec.rb18
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js37
-rw-r--r--spec/frontend/editor/components/helpers.js18
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js116
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js112
-rw-r--r--spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js156
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js111
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js34
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js32
-rw-r--r--spec/frontend/notes/components/note_body_spec.js90
-rw-r--r--spec/frontend/notes/components/note_form_spec.js19
-rw-r--r--spec/frontend/notes/components/note_header_spec.js4
-rw-r--r--spec/lib/gitlab/application_context_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb45
-rw-r--r--spec/lib/gitlab/database_spec.rb34
-rw-r--r--spec/lib/gitlab/sidekiq_config_spec.rb36
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb9
-rw-r--r--spec/requests/api/integrations/jira_connect/subscriptions_spec.rb86
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb13
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb11
-rw-r--r--spec/services/groups/group_links/destroy_service_spec.rb85
163 files changed, 2289 insertions, 615 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 4d2477c199d..05b96a6b88a 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -11,11 +11,6 @@ Gitlab/PolicyRuleBoolean:
Exclude:
- 'ee/app/policies/ee/identity_provider_policy.rb'
-# Offense count: 41
-# Cop supports --auto-correct.
-Performance/ConstantRegexp:
- Enabled: false
-
# Offense count: 1428
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
@@ -34,11 +29,6 @@ RSpec/PredicateMatcher:
RSpec/RepeatedExampleGroupBody:
Enabled: false
-# Offense count: 610
-# Cop supports --auto-correct.
-RSpec/ScatteredLet:
- Enabled: false
-
# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: Include.
@@ -78,11 +68,6 @@ Rails/CreateTableWithTimestamps:
Rails/HasManyOrHasOneDependent:
Enabled: false
-# Offense count: 47
-# Cop supports --auto-correct.
-Rails/IndexWith:
- Enabled: false
-
# Offense count: 118
# Configuration parameters: Include.
# Include: app/models/**/*.rb
@@ -116,13 +101,6 @@ Rails/WhereExists:
Style/AccessorGrouping:
Enabled: false
-# Offense count: 188
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: percent_q, bare_percent
-Style/BarePercentLiterals:
- Enabled: false
-
# Offense count: 42
# Cop supports --auto-correct.
Style/CaseLikeIf:
@@ -158,8 +136,3 @@ Style/Lambda:
# Cop supports --auto-correct.
Style/RedundantRegexpEscape:
Enabled: false
-
-# Offense count: 53
-# Cop supports --auto-correct.
-Style/RescueModifier:
- Enabled: false
diff --git a/.rubocop_todo/database/multiple_databases.yml b/.rubocop_todo/database/multiple_databases.yml
index 08ce72f7f51..1d0085e1ba6 100644
--- a/.rubocop_todo/database/multiple_databases.yml
+++ b/.rubocop_todo/database/multiple_databases.yml
@@ -2,7 +2,6 @@
Database/MultipleDatabases:
Exclude:
- 'config/initializers/active_record_data_types.rb'
- - 'config/initializers/sidekiq.rb'
- 'db/post_migrate/20210317104032_set_iteration_cadence_automatic_to_false.rb'
- 'db/post_migrate/20210811122206_update_external_project_bots.rb'
- 'db/post_migrate/20210812013042_remove_duplicate_project_authorizations.rb'
@@ -10,10 +9,6 @@ Database/MultipleDatabases:
- 'lib/backup/manager.rb'
- 'lib/gitlab/background_migration/backfill_projects_with_coverage.rb'
- 'lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans.rb'
- - 'lib/gitlab/database.rb'
- - 'lib/gitlab/database/load_balancing/load_balancer.rb'
- - 'lib/gitlab/database/migrations/observers/query_log.rb'
- - 'lib/tasks/gitlab/db/validate_config.rake'
- 'spec/db/schema_spec.rb'
- 'spec/initializers/database_config_spec.rb'
- 'spec/lib/backup/manager_spec.rb'
diff --git a/.rubocop_todo/performance/constant_regexp.yml b/.rubocop_todo/performance/constant_regexp.yml
new file mode 100644
index 00000000000..fb2b7d6ef49
--- /dev/null
+++ b/.rubocop_todo/performance/constant_regexp.yml
@@ -0,0 +1,32 @@
+---
+# Cop supports --auto-correct.
+Performance/ConstantRegexp:
+ # Offense count: 46
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'app/models/commit.rb'
+ - 'app/models/commit_range.rb'
+ - 'app/models/custom_emoji.rb'
+ - 'app/models/gpg_key.rb'
+ - 'app/models/merge_request.rb'
+ - 'app/models/project.rb'
+ - 'app/models/wiki.rb'
+ - 'ee/app/models/ee/epic.rb'
+ - 'lib/banzai/filter/custom_emoji_filter.rb'
+ - 'lib/gitlab/cleanup/project_uploads.rb'
+ - 'lib/gitlab/database/reindexing/reindex_concurrently.rb'
+ - 'lib/gitlab/dependency_linker/base_linker.rb'
+ - 'lib/gitlab/dependency_linker/composer_json_linker.rb'
+ - 'lib/gitlab/dependency_linker/godeps_json_linker.rb'
+ - 'lib/gitlab/dependency_linker/podspec_linker.rb'
+ - 'lib/gitlab/git.rb'
+ - 'lib/gitlab/job_waiter.rb'
+ - 'lib/gitlab/metrics/dashboard/url.rb'
+ - 'lib/gitlab/path_regex.rb'
+ - 'lib/gitlab/regex.rb'
+ - 'scripts/perf/query_limiting_report.rb'
+ - 'scripts/validate_migration_schema'
+ - 'spec/models/concerns/token_authenticatable_spec.rb'
+ - 'spec/scripts/lib/glfm/update_specification_spec.rb'
+ - 'spec/services/notes/copy_service_spec.rb'
diff --git a/.rubocop_todo/rails/index_with.yml b/.rubocop_todo/rails/index_with.yml
new file mode 100644
index 00000000000..09339d3fd56
--- /dev/null
+++ b/.rubocop_todo/rails/index_with.yml
@@ -0,0 +1,52 @@
+---
+# Cop supports --auto-correct.
+Rails/IndexWith:
+ # Offense count: 54
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'app/helpers/ci/jobs_helper.rb'
+ - 'app/models/ci/build_trace_chunk.rb'
+ - 'app/models/ci/processable.rb'
+ - 'app/models/concerns/cached_commit.rb'
+ - 'app/models/environment.rb'
+ - 'app/services/concerns/rate_limited_service.rb'
+ - 'db/post_migrate/20210731132939_backfill_stage_event_hash.rb'
+ - 'ee/app/models/vulnerabilities/projects_grade.rb'
+ - 'ee/lib/ee/gitlab/usage_data.rb'
+ - 'ee/lib/gitlab/auth/group_saml/auth_hash.rb'
+ - 'ee/lib/gitlab/custom_file_templates.rb'
+ - 'ee/lib/gitlab/insights/reducers/count_per_label_reducer.rb'
+ - 'ee/spec/lib/ee/gitlab/application_context_spec.rb'
+ - 'ee/spec/models/ee/namespace_spec.rb'
+ - 'ee/spec/models/sca/license_compliance_spec.rb'
+ - 'ee/spec/views/admin/dashboard/index.html.haml_spec.rb'
+ - 'lib/api/entities/project_integration.rb'
+ - 'lib/api/helpers/packages/conan/api_helpers.rb'
+ - 'lib/banzai/filter/repository_link_filter.rb'
+ - 'lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb'
+ - 'lib/gitlab/ci/ansi2html.rb'
+ - 'lib/gitlab/ci/reports/security/finding.rb'
+ - 'lib/gitlab/ci/reports/security/identifier.rb'
+ - 'lib/gitlab/ci/reports/test_suite.rb'
+ - 'lib/gitlab/database/count/exact_count_strategy.rb'
+ - 'lib/gitlab/database/migration_helpers.rb'
+ - 'lib/gitlab/database/obsolete_ignored_columns.rb'
+ - 'lib/gitlab/issuable_metadata.rb'
+ - 'lib/gitlab/template/base_template.rb'
+ - 'lib/gitlab/usage_data.rb'
+ - 'lib/google_api/cloud_platform/client.rb'
+ - 'qa/qa/resource/reusable.rb'
+ - 'scripts/trigger-build.rb'
+ - 'spec/lib/gitlab/api_authentication/sent_through_builder_spec.rb'
+ - 'spec/lib/gitlab/conflict/file_spec.rb'
+ - 'spec/lib/gitlab/import_export/model_configuration_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/tree_restorer_spec.rb'
+ - 'spec/lib/google_api/cloud_platform/client_spec.rb'
+ - 'spec/lib/learn_gitlab/onboarding_spec.rb'
+ - 'spec/models/event_spec.rb'
+ - 'spec/presenters/projects/security/configuration_presenter_spec.rb'
+ - 'spec/support/database/multiple_databases.rb'
+ - 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
+ - 'spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb'
+ - 'spec/views/admin/dashboard/index.html.haml_spec.rb'
diff --git a/.rubocop_todo/rspec/scattered_let.yml b/.rubocop_todo/rspec/scattered_let.yml
new file mode 100644
index 00000000000..52f19bf0768
--- /dev/null
+++ b/.rubocop_todo/rspec/scattered_let.yml
@@ -0,0 +1,285 @@
+---
+# Cop supports --auto-correct.
+RSpec/ScatteredLet:
+ # Offense count: 720
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'ee/spec/features/groups/group_roadmap_spec.rb'
+ - 'ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb'
+ - 'ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb'
+ - 'ee/spec/finders/security/vulnerability_reads_finder_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+ - 'ee/spec/graphql/mutations/boards/epics/create_spec.rb'
+ - 'ee/spec/graphql/mutations/dast_site_profiles/create_spec.rb'
+ - 'ee/spec/graphql/mutations/merge_requests/accept_spec.rb'
+ - 'ee/spec/graphql/mutations/releases/update_spec.rb'
+ - 'ee/spec/graphql/resolvers/security_orchestration/scan_execution_policy_resolver_spec.rb'
+ - 'ee/spec/graphql/types/boards/board_epic_type_spec.rb'
+ - 'ee/spec/graphql/types/instance_security_dashboard_type_spec.rb'
+ - 'ee/spec/helpers/ee/subscribable_banner_helper_spec.rb'
+ - 'ee/spec/helpers/trial_status_widget_helper_spec.rb'
+ - 'ee/spec/lib/banzai/reference_parser/iteration_parser_spec.rb'
+ - 'ee/spec/lib/ee/audit/compliance_framework_changes_auditor_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/ci/config_spec.rb'
+ - 'ee/spec/lib/ee/gitlab/email/handler/service_desk_handler_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/parsers/security/formatters/dependency_list_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/dast_api_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/ci/templates/dast_api_latest_gitlab_ci_yaml_spec.rb'
+ - 'ee/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
+ - 'ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/events/repositories_changed_event_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
+ - 'ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb'
+ - 'ee/spec/lib/gitlab/git_access_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/security_orchestration_policies/lazy_dast_profile_aggregate_spec.rb'
+ - 'ee/spec/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/reducers/count_per_label_reducer_spec.rb'
+ - 'ee/spec/lib/gitlab/insights/reducers/label_count_per_period_reducer_spec.rb'
+ - 'ee/spec/lib/gitlab/usage_data_metrics_spec.rb'
+ - 'ee/spec/models/analytics/cycle_analytics/group_level_spec.rb'
+ - 'ee/spec/models/approval_wrapped_any_approver_rule_spec.rb'
+ - 'ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb'
+ - 'ee/spec/models/ci/minutes/notification_spec.rb'
+ - 'ee/spec/models/ci/minutes/quota_spec.rb'
+ - 'ee/spec/models/ci/pipeline_spec.rb'
+ - 'ee/spec/models/ee/ci/build_dependencies_spec.rb'
+ - 'ee/spec/models/ee/namespace/root_storage_size_spec.rb'
+ - 'ee/spec/models/label_note_spec.rb'
+ - 'ee/spec/models/sca/license_compliance_spec.rb'
+ - 'ee/spec/policies/merge_request_policy_spec.rb'
+ - 'ee/spec/requests/api/graphql/compliance_management/merge_requests/compliance_violations_spec.rb'
+ - 'ee/spec/requests/api/graphql/group/ci_cd_settings_spec.rb'
+ - 'ee/spec/requests/api/graphql/group/epic/notes_spec.rb'
+ - 'ee/spec/requests/api/graphql/group_query_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/boards/epic_boards/epic_move_list_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/incident_management/oncall_rotation/update_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/issues/promote_to_epic_spec.rb'
+ - 'ee/spec/requests/api/graphql/mutations/security_policy/commit_scan_execution_policy_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/alert_management/http_integrations_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/dast_profile_schedule_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/pipeline/dast_profile_spec.rb'
+ - 'ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb'
+ - 'ee/spec/requests/api/internal/base_spec.rb'
+ - 'ee/spec/requests/api/projects_spec.rb'
+ - 'ee/spec/requests/api/vulnerability_findings_spec.rb'
+ - 'ee/spec/requests/git_http_geo_spec.rb'
+ - 'ee/spec/serializers/status_page/incident_serializer_spec.rb'
+ - 'ee/spec/services/app_sec/dast/scanner_profiles/update_service_spec.rb'
+ - 'ee/spec/services/arkose/blocked_users_report_service_spec.rb'
+ - 'ee/spec/services/arkose/user_verification_service_spec.rb'
+ - 'ee/spec/services/audit_event_service_spec.rb'
+ - 'ee/spec/services/audit_events/protected_branch_audit_event_service_spec.rb'
+ - 'ee/spec/services/ee/issue_links/create_service_spec.rb'
+ - 'ee/spec/services/epic_issues/create_service_spec.rb'
+ - 'ee/spec/services/epics/issue_promote_service_spec.rb'
+ - 'ee/spec/services/gitlab_subscriptions/activate_service_spec.rb'
+ - 'ee/spec/services/group_saml/saml_provider/create_service_spec.rb'
+ - 'ee/spec/services/group_saml/saml_provider/update_service_spec.rb'
+ - 'ee/spec/services/groups/memberships/export_service_spec.rb'
+ - 'ee/spec/services/incident_management/escalation_policies/create_service_spec.rb'
+ - 'ee/spec/services/incident_management/oncall_rotations/remove_participant_service_spec.rb'
+ - 'ee/spec/services/merge_request_approval_settings/update_service_spec.rb'
+ - 'ee/spec/services/merge_trains/refresh_service_spec.rb'
+ - 'ee/spec/services/projects/destroy_service_spec.rb'
+ - 'ee/spec/services/projects/prometheus/alerts/notify_service_spec.rb'
+ - 'ee/spec/services/quality_management/test_cases/create_service_spec.rb'
+ - 'ee/spec/services/quick_actions/interpret_service_spec.rb'
+ - 'ee/spec/services/requirements_management/update_requirement_service_spec.rb'
+ - 'ee/spec/services/search/group_service_spec.rb'
+ - 'ee/spec/services/search/project_service_spec.rb'
+ - 'ee/spec/services/todo_service_spec.rb'
+ - 'ee/spec/views/shared/_mirror_update_button.html.haml_spec.rb'
+ - 'ee/spec/views/subscriptions/groups/edit.html.haml_spec.rb'
+ - 'ee/spec/workers/compliance_management/merge_requests/compliance_violations_worker_spec.rb'
+ - 'ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_1_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/1_manage/project/project_audit_logs_spec.rb'
+ - 'spec/controllers/projects/artifacts_controller_spec.rb'
+ - 'spec/controllers/projects/deploy_keys_controller_spec.rb'
+ - 'spec/controllers/projects/environments_controller_spec.rb'
+ - 'spec/controllers/projects/issues_controller_spec.rb'
+ - 'spec/controllers/projects/releases/evidences_controller_spec.rb'
+ - 'spec/controllers/projects/releases_controller_spec.rb'
+ - 'spec/finders/ci/daily_build_group_report_results_finder_spec.rb'
+ - 'spec/finders/concerns/finder_with_cross_project_access_spec.rb'
+ - 'spec/finders/concerns/finder_with_group_hierarchy_spec.rb'
+ - 'spec/finders/events_finder_spec.rb'
+ - 'spec/finders/group_projects_finder_spec.rb'
+ - 'spec/finders/license_template_finder_spec.rb'
+ - 'spec/frontend/fixtures/pipelines.rb'
+ - 'spec/graphql/resolvers/commit_pipelines_resolver_spec.rb'
+ - 'spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb'
+ - 'spec/graphql/resolvers/design_management/versions_resolver_spec.rb'
+ - 'spec/graphql/types/ci/job_token_scope_type_spec.rb'
+ - 'spec/helpers/merge_requests_helper_spec.rb'
+ - 'spec/lib/banzai/filter/references/project_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/references/user_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/upload_link_filter_spec.rb'
+ - 'spec/lib/banzai/reference_parser/alert_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/commit_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/commit_range_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/external_issue_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/label_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/merge_request_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/milestone_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/project_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/snippet_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/user_parser_spec.rb'
+ - 'spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb'
+ - 'spec/lib/gitlab/asciidoc/include_processor_spec.rb'
+ - 'spec/lib/gitlab/auth/ldap/person_spec.rb'
+ - 'spec/lib/gitlab/auth/saml/auth_hash_spec.rb'
+ - 'spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb'
+ - 'spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb'
+ - 'spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb'
+ - 'spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb'
+ - 'spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
+ - 'spec/lib/gitlab/ci/config/external/file/artifact_spec.rb'
+ - 'spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb'
+ - 'spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb'
+ - 'spec/lib/gitlab/ci/status/stage/factory_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder/group_spec.rb'
+ - 'spec/lib/gitlab/ci/variables/builder/project_spec.rb'
+ - 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
+ - 'spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb'
+ - 'spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/test_batched_background_runner_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning/partition_manager_spec.rb'
+ - 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb'
+ - 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb'
+ - 'spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb'
+ - 'spec/lib/gitlab/diff/formatters/text_formatter_spec.rb'
+ - 'spec/lib/gitlab/diff/position_spec.rb'
+ - 'spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb'
+ - 'spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb'
+ - 'spec/lib/gitlab/diff/suggestion_diff_spec.rb'
+ - 'spec/lib/gitlab/diff/suggestion_spec.rb'
+ - 'spec/lib/gitlab/elasticsearch/logs/lines_spec.rb'
+ - 'spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
+ - 'spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb'
+ - 'spec/lib/gitlab/error_tracking_spec.rb'
+ - 'spec/lib/gitlab/git/blame_spec.rb'
+ - 'spec/lib/gitlab/git/diff_collection_spec.rb'
+ - 'spec/lib/gitlab/git_access_spec.rb'
+ - 'spec/lib/gitlab/github_import/parallel_scheduling_spec.rb'
+ - 'spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/export_task_spec.rb'
+ - 'spec/lib/gitlab/jira_import/issue_serializer_spec.rb'
+ - 'spec/lib/gitlab/lets_encrypt/client_spec.rb'
+ - 'spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb'
+ - 'spec/lib/gitlab/metrics/subscribers/external_http_spec.rb'
+ - 'spec/lib/gitlab/middleware/memory_report_spec.rb'
+ - 'spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb'
+ - 'spec/lib/gitlab/pagination/keyset/page_spec.rb'
+ - 'spec/lib/gitlab/pagination/offset_pagination_spec.rb'
+ - 'spec/lib/gitlab/patch/database_config_spec.rb'
+ - 'spec/lib/gitlab/path_regex_spec.rb'
+ - 'spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb'
+ - 'spec/lib/gitlab/serializer/pagination_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb'
+ - 'spec/lib/gitlab/spamcheck/client_spec.rb'
+ - 'spec/lib/gitlab/template/finders/global_template_finder_spec.rb'
+ - 'spec/lib/gitlab/tree_summary_spec.rb'
+ - 'spec/lib/gitlab/usage/service_ping_report_spec.rb'
+ - 'spec/lib/gitlab/usage_data_metrics_spec.rb'
+ - 'spec/lib/gitlab/utils/measuring_spec.rb'
+ - 'spec/lib/gitlab/zentao/client_spec.rb'
+ - 'spec/lib/peek/views/external_http_spec.rb'
+ - 'spec/mailers/emails/in_product_marketing_spec.rb'
+ - 'spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb'
+ - 'spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb'
+ - 'spec/models/application_record_spec.rb'
+ - 'spec/models/ci/build_dependencies_spec.rb'
+ - 'spec/models/ci/pipeline_spec.rb'
+ - 'spec/models/concerns/issuable_spec.rb'
+ - 'spec/models/concerns/manual_inverse_association_spec.rb'
+ - 'spec/models/concerns/noteable_spec.rb'
+ - 'spec/models/deploy_keys_project_spec.rb'
+ - 'spec/models/design_management/design_at_version_spec.rb'
+ - 'spec/models/diff_note_spec.rb'
+ - 'spec/models/environment_spec.rb'
+ - 'spec/models/integration_spec.rb'
+ - 'spec/models/merge_request_diff_spec.rb'
+ - 'spec/models/merge_request_spec.rb'
+ - 'spec/models/milestone_spec.rb'
+ - 'spec/models/push_event_spec.rb'
+ - 'spec/models/ssh_host_key_spec.rb'
+ - 'spec/models/user_spec.rb'
+ - 'spec/models/users/credit_card_validation_spec.rb'
+ - 'spec/models/wiki_page_spec.rb'
+ - 'spec/policies/ci/build_policy_spec.rb'
+ - 'spec/policies/design_management/design_policy_spec.rb'
+ - 'spec/policies/group_member_policy_spec.rb'
+ - 'spec/requests/admin/background_migrations_controller_spec.rb'
+ - 'spec/requests/api/ci/pipeline_schedules_spec.rb'
+ - 'spec/requests/api/ci/pipelines_spec.rb'
+ - 'spec/requests/api/commit_statuses_spec.rb'
+ - 'spec/requests/api/graphql/ci/groups_spec.rb'
+ - 'spec/requests/api/graphql/ci/pipelines_spec.rb'
+ - 'spec/requests/api/graphql/mutations/boards/create_spec.rb'
+ - 'spec/requests/api/graphql/namespace/projects_spec.rb'
+ - 'spec/requests/api/graphql/project/issue/design_collection/version_spec.rb'
+ - 'spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb'
+ - 'spec/requests/api/graphql/project/issue_spec.rb'
+ - 'spec/requests/api/graphql/project/milestones_spec.rb'
+ - 'spec/requests/api/graphql/project/pipeline_spec.rb'
+ - 'spec/requests/api/graphql/project/release_spec.rb'
+ - 'spec/requests/api/graphql/user/starred_projects_query_spec.rb'
+ - 'spec/requests/api/issues/get_group_issues_spec.rb'
+ - 'spec/requests/api/issues/get_project_issues_spec.rb'
+ - 'spec/requests/api/issues/post_projects_issues_spec.rb'
+ - 'spec/requests/api/issues/put_projects_issues_spec.rb'
+ - 'spec/requests/api/merge_requests_spec.rb'
+ - 'spec/requests/api/notes_spec.rb'
+ - 'spec/requests/api/project_clusters_spec.rb'
+ - 'spec/requests/api/project_export_spec.rb'
+ - 'spec/requests/api/rubygem_packages_spec.rb'
+ - 'spec/requests/projects/releases_controller_spec.rb'
+ - 'spec/rubocop/cop/migration/update_column_in_batches_spec.rb'
+ - 'spec/scripts/pipeline_test_report_builder_spec.rb'
+ - 'spec/serializers/build_details_entity_spec.rb'
+ - 'spec/serializers/ci/job_entity_spec.rb'
+ - 'spec/serializers/merge_requests/pipeline_entity_spec.rb'
+ - 'spec/services/ci/change_variable_service_spec.rb'
+ - 'spec/services/ci/change_variables_service_spec.rb'
+ - 'spec/services/ci/create_downstream_pipeline_service_spec.rb'
+ - 'spec/services/ci/create_pipeline_service/logger_spec.rb'
+ - 'spec/services/ci/create_pipeline_service_spec.rb'
+ - 'spec/services/ci/destroy_pipeline_service_spec.rb'
+ - 'spec/services/ci/find_exposed_artifacts_service_spec.rb'
+ - 'spec/services/ci/pipeline_bridge_status_service_spec.rb'
+ - 'spec/services/commits/cherry_pick_service_spec.rb'
+ - 'spec/services/design_management/delete_designs_service_spec.rb'
+ - 'spec/services/design_management/save_designs_service_spec.rb'
+ - 'spec/services/discussions/capture_diff_note_positions_service_spec.rb'
+ - 'spec/services/events/destroy_service_spec.rb'
+ - 'spec/services/git/base_hooks_service_spec.rb'
+ - 'spec/services/groups/group_links/update_service_spec.rb'
+ - 'spec/services/jira_import/cloud_users_mapper_service_spec.rb'
+ - 'spec/services/jira_import/server_users_mapper_service_spec.rb'
+ - 'spec/services/lfs/push_service_spec.rb'
+ - 'spec/services/metrics/dashboard/custom_metric_embed_service_spec.rb'
+ - 'spec/services/metrics/dashboard/dynamic_embed_service_spec.rb'
+ - 'spec/services/metrics/dashboard/gitlab_alert_embed_service_spec.rb'
+ - 'spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb'
+ - 'spec/services/notification_service_spec.rb'
+ - 'spec/services/packages/composer/create_package_service_spec.rb'
+ - 'spec/services/packages/conan/create_package_file_service_spec.rb'
+ - 'spec/services/packages/debian/create_package_file_service_spec.rb'
+ - 'spec/services/packages/debian/generate_distribution_key_service_spec.rb'
+ - 'spec/services/packages/debian/update_distribution_service_spec.rb'
+ - 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb'
+ - 'spec/services/snippets/create_service_spec.rb'
+ - 'spec/services/spam/spam_verdict_service_spec.rb'
+ - 'spec/services/suggestions/create_service_spec.rb'
+ - 'spec/services/system_notes/design_management_service_spec.rb'
+ - 'spec/services/system_notes/merge_requests_service_spec.rb'
+ - 'spec/services/todo_service_spec.rb'
+ - 'spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb'
+ - 'spec/tasks/gitlab/artifacts/migrate_rake_spec.rb'
+ - 'spec/workers/concerns/gitlab/github_import/object_importer_spec.rb'
+ - 'spec/workers/packages/debian/generate_distribution_worker_spec.rb'
diff --git a/.rubocop_todo/style/bare_percent_literals.yml b/.rubocop_todo/style/bare_percent_literals.yml
new file mode 100644
index 00000000000..658c6c22baa
--- /dev/null
+++ b/.rubocop_todo/style/bare_percent_literals.yml
@@ -0,0 +1,115 @@
+---
+# Cop supports --auto-correct.
+Style/BarePercentLiterals:
+ # Offense count: 220
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'app/models/commit.rb'
+ - 'app/models/concerns/storage/legacy_namespace.rb'
+ - 'app/models/integrations/datadog.rb'
+ - 'app/services/feature_flags/base_service.rb'
+ - 'app/services/repositories/base_service.rb'
+ - 'app/services/repositories/destroy_rollback_service.rb'
+ - 'app/services/repositories/destroy_service.rb'
+ - 'ee/app/services/jira/jql_builder_service.rb'
+ - 'ee/lib/ee/gitlab/checks/push_rules/file_size_check.rb'
+ - 'ee/spec/features/projects/environments/environments_spec.rb'
+ - 'ee/spec/helpers/subscriptions_helper_spec.rb'
+ - 'ee/spec/lib/banzai/filter/references/iteration_reference_filter_spec.rb'
+ - 'ee/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb'
+ - 'ee/spec/lib/gitlab/status_page/filter/image_filter_spec.rb'
+ - 'ee/spec/requests/api/ci/jobs_spec.rb'
+ - 'ee/spec/services/geo/container_repository_sync_spec.rb'
+ - 'lib/banzai/filter/autolink_filter.rb'
+ - 'lib/banzai/filter/references/reference_filter.rb'
+ - 'lib/banzai/filter/spaced_link_filter.rb'
+ - 'lib/banzai/filter/table_of_contents_filter.rb'
+ - 'lib/banzai/issuable_extractor.rb'
+ - 'lib/gitlab/authorized_keys.rb'
+ - 'lib/gitlab/etag_caching/middleware.rb'
+ - 'lib/gitlab/etag_caching/router/rails.rb'
+ - 'lib/gitlab/gl_repository/identifier.rb'
+ - 'lib/gitlab/import_export/repo_restorer.rb'
+ - 'lib/kramdown/parser/atlassian_document_format.rb'
+ - 'lib/tasks/tanuki_emoji.rake'
+ - 'qa/qa/ee/page/dashboard/projects.rb'
+ - 'qa/qa/ee/page/group/settings/general.rb'
+ - 'qa/qa/ee/page/project/issue/show.rb'
+ - 'qa/qa/ee/page/project/job/show.rb'
+ - 'qa/qa/ee/page/project/packages/index.rb'
+ - 'qa/qa/ee/page/project/pipeline/show.rb'
+ - 'qa/qa/ee/page/project/show.rb'
+ - 'qa/qa/ee/page/project/snippet/index.rb'
+ - 'qa/qa/ee/page/project/wiki/show.rb'
+ - 'qa/qa/page/component/design_management.rb'
+ - 'qa/qa/page/component/select2.rb'
+ - 'qa/qa/page/element.rb'
+ - 'qa/qa/page/file/form.rb'
+ - 'qa/qa/page/project/web_ide/edit.rb'
+ - 'qa/qa/resource/events/project.rb'
+ - 'qa/qa/resource/members.rb'
+ - 'qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_new_account_spec.rb'
+ - 'qa/qa/specs/features/ee/browser_ui/3_create/repository/push_rules_spec.rb'
+ - 'qa/qa/support/page/logging.rb'
+ - 'qa/spec/runtime/feature_spec.rb'
+ - 'scripts/regenerate-schema'
+ - 'scripts/trigger-build.rb'
+ - 'spec/controllers/import/fogbugz_controller_spec.rb'
+ - 'spec/controllers/projects/artifacts_controller_spec.rb'
+ - 'spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb'
+ - 'spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb'
+ - 'spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb'
+ - 'spec/features/projects/artifacts/user_downloads_artifacts_spec.rb'
+ - 'spec/features/projects/badges/coverage_spec.rb'
+ - 'spec/features/projects/badges/pipeline_badge_spec.rb'
+ - 'spec/features/projects/issuable_templates_spec.rb'
+ - 'spec/features/projects/jobs_spec.rb'
+ - 'spec/features/projects/pipelines/legacy_pipeline_spec.rb'
+ - 'spec/features/projects/pipelines/legacy_pipelines_spec.rb'
+ - 'spec/features/projects/pipelines/pipeline_spec.rb'
+ - 'spec/features/projects/pipelines/pipelines_spec.rb'
+ - 'spec/features/uploads/user_uploads_avatar_to_group_spec.rb'
+ - 'spec/features/uploads/user_uploads_avatar_to_profile_spec.rb'
+ - 'spec/graphql/resolvers/echo_resolver_spec.rb'
+ - 'spec/helpers/markup_helper_spec.rb'
+ - 'spec/lib/banzai/filter/autolink_filter_spec.rb'
+ - 'spec/lib/banzai/filter/external_link_filter_spec.rb'
+ - 'spec/lib/banzai/filter/image_link_filter_spec.rb'
+ - 'spec/lib/banzai/filter/references/label_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb'
+ - 'spec/lib/banzai/pipeline/full_pipeline_spec.rb'
+ - 'spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb'
+ - 'spec/lib/banzai/reference_parser/commit_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/issue_parser_spec.rb'
+ - 'spec/lib/banzai/reference_parser/merge_request_parser_spec.rb'
+ - 'spec/lib/gitlab/diff/highlight_spec.rb'
+ - 'spec/lib/gitlab/error_tracking/stack_trace_highlight_decorator_spec.rb'
+ - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
+ - 'spec/lib/gitlab/highlight_spec.rb'
+ - 'spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb'
+ - 'spec/lib/gitlab/pagination/offset_pagination_spec.rb'
+ - 'spec/lib/gitlab/prometheus/query_variables_spec.rb'
+ - 'spec/lib/gitlab/reference_extractor_spec.rb'
+ - 'spec/lib/gitlab/url_sanitizer_spec.rb'
+ - 'spec/mailers/emails/releases_spec.rb'
+ - 'spec/mailers/emails/service_desk_spec.rb'
+ - 'spec/models/deployment_spec.rb'
+ - 'spec/models/integrations/drone_ci_spec.rb'
+ - 'spec/models/integrations/teamcity_spec.rb'
+ - 'spec/models/project_label_spec.rb'
+ - 'spec/presenters/snippet_blob_presenter_spec.rb'
+ - 'spec/requests/api/ci/job_artifacts_spec.rb'
+ - 'spec/requests/api/deployments_spec.rb'
+ - 'spec/requests/api/graphql/mutations/snippets/destroy_spec.rb'
+ - 'spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb'
+ - 'spec/services/prometheus/proxy_variable_substitution_service_spec.rb'
+ - 'spec/support/banzai/reference_filter_shared_examples.rb'
+ - 'spec/support/helpers/graphql_helpers.rb'
+ - 'spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb'
+ - 'spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb'
+ - 'spec/support/shared_examples/graphql/label_fields.rb'
+ - 'spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb'
+ - 'spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
+ - 'spec/views/layouts/_head.html.haml_spec.rb'
diff --git a/.rubocop_todo/style/rescue_modifier.yml b/.rubocop_todo/style/rescue_modifier.yml
new file mode 100644
index 00000000000..298ef3aece9
--- /dev/null
+++ b/.rubocop_todo/style/rescue_modifier.yml
@@ -0,0 +1,51 @@
+---
+# Cop supports --auto-correct.
+Style/RescueModifier:
+ # Offense count: 59
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'app/controllers/admin/system_info_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/finders/ci/daily_build_group_report_results_finder.rb'
+ - 'app/helpers/blob_helper.rb'
+ - 'app/models/concerns/diff_positionable_note.rb'
+ - 'app/models/integrations/jira.rb'
+ - 'app/models/sent_notification.rb'
+ - 'app/models/todo.rb'
+ - 'app/services/security/ci_configuration/sast_parser_service.rb'
+ - 'config/initializers/active_record_data_types.rb'
+ - 'config/settings.rb'
+ - 'ee/app/models/license.rb'
+ - 'ee/lib/ee/gitlab/background_migration/drop_invalid_remediations.rb'
+ - 'ee/lib/gitlab/geo.rb'
+ - 'ee/lib/gitlab/geo/health_check.rb'
+ - 'lib/api/helpers.rb'
+ - 'lib/feature.rb'
+ - 'lib/gitlab/current_settings.rb'
+ - 'lib/gitlab/dependency_linker/cargo_toml_linker.rb'
+ - 'lib/gitlab/dependency_linker/json_linker.rb'
+ - 'lib/gitlab/diff/parser.rb'
+ - 'lib/gitlab/import_export/after_export_strategy_builder.rb'
+ - 'lib/gitlab/kubernetes.rb'
+ - 'lib/gitlab/middleware/read_only/controller.rb'
+ - 'lib/gitlab/process_management.rb'
+ - 'lib/gitlab/query_limiting/middleware.rb'
+ - 'lib/gitlab/quick_actions/command_definition.rb'
+ - 'lib/gitlab/quick_actions/spend_time_and_date_separator.rb'
+ - 'lib/gitlab/word_diff/segments/diff_hunk.rb'
+ - 'lib/gitlab/zentao/client.rb'
+ - 'lib/tasks/gitlab/praefect.rake'
+ - 'qa/qa/page/component/access_tokens.rb'
+ - 'qa/qa/page/profile/ssh_keys.rb'
+ - 'spec/finders/concerns/finder_with_cross_project_access_spec.rb'
+ - 'spec/lib/gitlab/database/load_balancing/session_spec.rb'
+ - 'spec/lib/gitlab/database/migrations/instrumentation_spec.rb'
+ - 'spec/lib/gitlab/email/handler/service_desk_handler_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb'
+ - 'spec/models/concerns/bulk_insert_safe_spec.rb'
+ - 'spec/models/design_management/version_spec.rb'
+ - 'spec/models/group_spec.rb'
+ - 'spec/models/namespace/traversal_hierarchy_spec.rb'
+ - 'spec/services/design_management/delete_designs_service_spec.rb'
+ - 'spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index f7c29cb018f..235e115267d 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-60a7383d965aa6a8e69aa2e33a84792cde486cd3
+42fab8fc526215f9426bc9f459f9e6da0951c574
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index ee2f6cfb46c..2ee2e199358 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
-import { getBlobLanguage } from '~/editor/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
@@ -36,7 +36,7 @@ export default class EditBlob {
import('~/editor/extensions/source_editor_markdown_ext'),
import('~/editor/extensions/source_editor_markdown_livepreview_ext'),
]);
- this.editor.use([
+ this.markdownExtensions = this.editor.use([
{ definition: MarkdownExtension },
{
definition: MarkdownLivePreview,
@@ -48,7 +48,6 @@ export default class EditBlob {
message: `${BLOB_EDITOR_ERROR}: ${e}`,
});
}
- this.hasMarkdownExtension = true;
addEditorMarkdownListeners(this.editor);
}
@@ -58,8 +57,6 @@ export default class EditBlob {
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
- this.hasMarkdownExtension = false;
-
const rootEditor = new SourceEditor();
this.editor = rootEditor.createInstance({
@@ -67,21 +64,29 @@ export default class EditBlob {
blobPath: fileNameEl.value,
blobContent: editorEl.innerText,
});
- this.editor.use([{ definition: SourceEditorExtension }, { definition: FileTemplateExtension }]);
+ this.editor.use([
+ { definition: SourceEditorExtension },
+ { definition: FileTemplateExtension },
+ { definition: ToolbarExtension },
+ ]);
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
- const newLang = getBlobLanguage(fileNameEl.value);
- if (newLang === 'markdown') {
- if (!this.hasMarkdownExtension) {
- this.fetchMarkdownExtension();
- }
- }
});
form.addEventListener('submit', () => {
fileContentEl.value = insertFinalNewline(this.editor.getValue());
});
+
+ // onDidChangeModelLanguage is part of the native Monaco API
+ // https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneCodeEditor.html#onDidChangeModelLanguage
+ this.editor.onDidChangeModelLanguage(({ newLanguage = '', oldLanguage = '' }) => {
+ if (newLanguage === 'markdown') {
+ this.fetchMarkdownExtension();
+ } else if (oldLanguage === 'markdown') {
+ this.editor.unuse(this.markdownExtensions);
+ }
+ });
}
initFileSelectors() {
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
index 1427f2df461..2c177634bbe 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
@@ -55,8 +55,8 @@ export default {
id="se-toolbar"
class="gl-py-3 gl-px-5 gl-bg-white gl-border-t gl-border-b gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
- <template v-for="group in $options.groups">
- <gl-button-group v-if="hasGroupItems(group)" :key="group">
+ <div v-for="group in $options.groups" :key="group">
+ <gl-button-group v-if="hasGroupItems(group)">
<template v-for="item in getGroupItems(group)">
<source-editor-toolbar-button
:key="item.id"
@@ -65,6 +65,6 @@ export default {
/>
</template>
</gl-button-group>
- </template>
+ </div>
</section>
</template>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
index 2595d67af34..194b482c12e 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
@@ -1,7 +1,5 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
-import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql';
export default {
name: 'SourceEditorToolbarButton',
@@ -20,70 +18,40 @@ export default {
},
},
},
- data() {
- return {
- buttonItem: this.button,
- };
- },
- apollo: {
- buttonItem: {
- query: getToolbarItemQuery,
- variables() {
- return {
- id: this.button.id,
- };
- },
- update({ item }) {
- return item;
- },
- skip() {
- return !this.button.id;
- },
- },
- },
computed: {
icon() {
- return this.buttonItem.selected
- ? this.buttonItem.selectedIcon || this.buttonItem.icon
- : this.buttonItem.icon;
+ return this.button.selected ? this.button.selectedIcon || this.button.icon : this.button.icon;
},
label() {
- return this.buttonItem.selected
- ? this.buttonItem.selectedLabel || this.buttonItem.label
- : this.buttonItem.label;
+ return this.button.selected
+ ? this.button.selectedLabel || this.button.label
+ : this.button.label;
+ },
+ showButton() {
+ return Object.entries(this.button).length > 0;
},
},
methods: {
clickHandler() {
- if (this.buttonItem.onClick) {
- this.buttonItem.onClick();
+ if (this.button.onClick) {
+ this.button.onClick();
}
- this.$apollo.mutate({
- mutation: updateToolbarItemMutation,
- variables: {
- id: this.buttonItem.id,
- propsToUpdate: {
- selected: !this.buttonItem.selected,
- },
- },
- });
this.$emit('click');
},
},
};
</script>
<template>
- <div>
- <gl-button
- v-gl-tooltip.hover
- :category="buttonItem.category"
- :variant="buttonItem.variant"
- type="button"
- :selected="buttonItem.selected"
- :icon="icon"
- :title="label"
- :aria-label="label"
- @click="clickHandler"
- />
- </div>
+ <gl-button
+ v-if="showButton"
+ v-gl-tooltip.hover
+ :category="button.category"
+ :variant="button.variant"
+ type="button"
+ :selected="button.selected"
+ :icon="icon"
+ :title="label"
+ :aria-label="label"
+ @click="clickHandler"
+ />
</template>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js b/app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js
new file mode 100644
index 00000000000..603ba26f22e
--- /dev/null
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_graphql.js
@@ -0,0 +1,53 @@
+import produce from 'immer';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import typeDefs from '~/editor/graphql/typedefs.graphql';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const resolvers = {
+ Mutation: {
+ addToolbarItems: (_, { items = [] }, { cache }) => {
+ const itemsSourceData = cache.readQuery({ query: getToolbarItemsQuery });
+ const data = produce(itemsSourceData, (draftData) => {
+ const existingNodes = draftData?.items?.nodes || [];
+ draftData.items = {
+ nodes: Array.isArray(items) ? [...existingNodes, ...items] : [...existingNodes, items],
+ };
+ });
+ cache.writeQuery({ query: getToolbarItemsQuery, data });
+ },
+
+ removeToolbarItems: (_, { ids }, { cache }) => {
+ const sourceData = cache.readQuery({ query: getToolbarItemsQuery });
+ const {
+ items: { nodes },
+ } = sourceData;
+ const data = produce(sourceData, (draftData) => {
+ draftData.items.nodes = nodes.filter((item) => !ids.includes(item.id));
+ });
+ cache.writeQuery({ query: getToolbarItemsQuery, data });
+ },
+
+ updateToolbarItem: (_, { id, propsToUpdate }, { cache }) => {
+ const itemSourceData = cache.readQuery({ query: getToolbarItemsQuery });
+ const data = produce(itemSourceData, (draftData) => {
+ const existingNodes = draftData?.items?.nodes || [];
+ draftData.items = {
+ nodes: existingNodes.map((item) => {
+ return item.id === id ? { ...item, ...propsToUpdate } : item;
+ }),
+ };
+ });
+ cache.writeQuery({ query: getToolbarItemsQuery, data });
+ },
+ },
+};
+
+const defaultClient = createDefaultClient(resolvers, { typeDefs });
+
+export const apolloProvider = new VueApollo({
+ defaultClient,
+});
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 361122d8890..83cfdd25757 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,5 +1,5 @@
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
@@ -57,5 +57,8 @@ export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview';
export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview';
+export const EXTENSION_MARKDOWN_PREVIEW_HIDE_ACTION_ID = 'markdown-preview-hide';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH = 0.5; // 50% of the width
export const EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY = 250; // ms
+export const EXTENSION_MARKDOWN_PREVIEW_LABEL = __('Preview Markdown');
+export const EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL = __('Hide Live Preview');
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index 9d53268c340..11cc85c659d 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -3,14 +3,17 @@ import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
import createFlash from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import syntaxHighlight from '~/syntax_highlight';
import {
EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ EXTENSION_MARKDOWN_PREVIEW_HIDE_ACTION_ID,
EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+ EXTENSION_MARKDOWN_PREVIEW_LABEL,
+ EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
+ EDITOR_TOOLBAR_RIGHT_GROUP,
} from '../constants';
const fetchPreview = (text, previewMarkdownPath) => {
@@ -41,31 +44,58 @@ export class EditorMarkdownPreviewExtension {
onSetup(instance, setupOptions) {
this.preview = {
el: undefined,
- action: undefined,
+ actions: {
+ preview: undefined,
+ hide: undefined,
+ },
shown: false,
modelChangeListener: undefined,
path: setupOptions.previewMarkdownPath,
+ actionShowPreviewCondition: instance.createContextKey('toggleLivePreview', true),
};
+ this.toolbarButtons = [];
+
this.setupPreviewAction(instance);
+ if (instance.toolbar) {
+ this.setupToolbar(instance);
+ }
+ }
- instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => {
- if (newLanguage === 'markdown' && oldLanguage !== newLanguage) {
- instance.setupPreviewAction();
- } else {
- instance.cleanup();
- }
- });
+ onBeforeUnuse(instance) {
+ this.cleanup(instance);
+ const ids = this.toolbarButtons.map((item) => item.id);
+ if (instance.toolbar) {
+ instance.toolbar.removeItems(ids);
+ }
+ }
- instance.onDidChangeModel(() => {
- const model = instance.getModel();
- if (model) {
- const { language } = model.getLanguageIdentifier();
- instance.cleanup();
- if (language === 'markdown') {
- instance.setupPreviewAction();
- }
- }
- });
+ cleanup(instance) {
+ if (this.preview.modelChangeListener) {
+ this.preview.modelChangeListener.dispose();
+ }
+ this.preview.actions.preview.dispose();
+ this.preview.actions.hide.dispose();
+ if (this.preview.shown) {
+ this.togglePreviewPanel(instance);
+ this.togglePreviewLayout(instance);
+ }
+ this.preview.shown = false;
+ }
+
+ setupToolbar(instance) {
+ this.toolbarButtons = [
+ {
+ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ label: EXTENSION_MARKDOWN_PREVIEW_LABEL,
+ icon: 'live-preview',
+ selected: false,
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ category: 'primary',
+ selectedLabel: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
+ onClick: () => instance.togglePreview(),
+ },
+ ];
+ instance.toolbar.addItems(this.toolbarButtons);
}
togglePreviewLayout(instance) {
@@ -103,22 +133,33 @@ export class EditorMarkdownPreviewExtension {
setupPreviewAction(instance) {
if (instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return;
-
- this.preview.action = instance.addAction({
- id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
- label: __('Preview Markdown'),
+ const actionBasis = {
keybindings: [
// eslint-disable-next-line no-bitwise,no-undef
monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_P),
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
-
// Method that will be executed when the action is triggered.
// @param ed The editor instance is passed in as a convenience
run(inst) {
inst.togglePreview();
},
+ };
+
+ this.preview.actions.preview = instance.addAction({
+ ...actionBasis,
+ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ label: EXTENSION_MARKDOWN_PREVIEW_LABEL,
+
+ precondition: 'toggleLivePreview',
+ });
+ this.preview.actions.hide = instance.addAction({
+ ...actionBasis,
+ id: EXTENSION_MARKDOWN_PREVIEW_HIDE_ACTION_ID,
+ label: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
+
+ precondition: '!toggleLivePreview',
});
}
@@ -126,18 +167,6 @@ export class EditorMarkdownPreviewExtension {
return {
markdownPreview: this.preview,
- cleanup: (instance) => {
- if (this.preview.modelChangeListener) {
- this.preview.modelChangeListener.dispose();
- }
- this.preview.action.dispose();
- if (this.preview.shown) {
- this.togglePreviewPanel(instance);
- this.togglePreviewLayout(instance);
- }
- this.preview.shown = false;
- },
-
fetchPreview: (instance) => this.fetchPreview(instance),
setupPreviewAction: (instance) => this.setupPreviewAction(instance),
@@ -149,6 +178,8 @@ export class EditorMarkdownPreviewExtension {
this.togglePreviewLayout(instance);
this.togglePreviewPanel(instance);
+ this.preview.actionShowPreviewCondition.set(!this.preview.actionShowPreviewCondition.get());
+
if (!this.preview?.shown) {
this.preview.modelChangeListener = instance.onDidChangeModelContent(
debounce(
@@ -161,6 +192,11 @@ export class EditorMarkdownPreviewExtension {
}
this.preview.shown = !this.preview?.shown;
+ if (instance.toolbar) {
+ instance.toolbar.updateItem(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, {
+ selected: this.preview.shown,
+ });
+ }
},
};
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js b/app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js
new file mode 100644
index 00000000000..9655c8ae76a
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_toolbar_ext.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import removeToolbarItemsMutation from '~/editor/graphql/remove_items.mutation.graphql';
+import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
+import addToolbarItemsMutation from '~/editor/graphql/add_items.mutation.graphql';
+import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+
+const client = apolloProvider.defaultClient;
+
+export class ToolbarExtension {
+ /**
+ * A required getter returning the extension's name
+ * We have to provide it for every extension instead of relying on the built-in
+ * `name` prop because the prop does not survive the webpack's minification
+ * and the name mangling.
+ * @returns {string}
+ */
+ static get extensionName() {
+ return 'ToolbarExtension';
+ }
+ /**
+ * THE LIFE-CYCLE CALLBACKS
+ */
+
+ /**
+ * Is called before the extension gets used by an instance,
+ * Use `onSetup` to setup Monaco directly:
+ * actions, keystrokes, update options, etc.
+ * Is called only once before the extension gets registered
+ *
+ * @param { Object } [instance] The Source Editor instance
+ * @param { Object } [setupOptions] The setupOptions object
+ */
+ // eslint-disable-next-line class-methods-use-this
+ onSetup(instance, setupOptions) {
+ const el = setupOptions?.el || document.getElementById('editor-toolbar');
+ ToolbarExtension.setupVue(el);
+ }
+
+ static setupVue(el) {
+ client.cache.writeQuery({ query: getToolbarItemsQuery, data: { items: { nodes: [] } } });
+ const ToolbarComponent = Vue.extend(SourceEditorToolbar);
+
+ const toolbar = new ToolbarComponent({
+ el,
+ apolloProvider,
+ });
+ toolbar.$mount();
+ }
+
+ /**
+ * The public API of the extension: these are the methods that will be exposed
+ * to the end user
+ * @returns {Object}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ toolbar: {
+ getItem: (id) => {
+ const items = client.readQuery({ query: getToolbarItemsQuery })?.items?.nodes || [];
+ return items.find((item) => item.id === id);
+ },
+ getAllItems: () => {
+ return client.readQuery({ query: getToolbarItemsQuery })?.items?.nodes || [];
+ },
+ addItems: (items = []) => {
+ return client.mutate({
+ mutation: addToolbarItemsMutation,
+ variables: {
+ items,
+ },
+ });
+ },
+ removeItems: (ids = []) => {
+ client.mutate({
+ mutation: removeToolbarItemsMutation,
+ variables: {
+ ids,
+ },
+ });
+ },
+ updateItem: (id = '', propsToUpdate = {}) => {
+ if (id) {
+ client.mutate({
+ mutation: updateToolbarItemMutation,
+ variables: {
+ id,
+ propsToUpdate,
+ },
+ });
+ }
+ },
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/editor/graphql/add_items.mutation.graphql b/app/assets/javascripts/editor/graphql/add_items.mutation.graphql
new file mode 100644
index 00000000000..13afcc04a48
--- /dev/null
+++ b/app/assets/javascripts/editor/graphql/add_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation addItems($items: [Item]) {
+ addToolbarItems(items: $items) @client
+}
diff --git a/app/assets/javascripts/editor/graphql/get_item.query.graphql b/app/assets/javascripts/editor/graphql/get_item.query.graphql
deleted file mode 100644
index 7c8bc09f7b0..00000000000
--- a/app/assets/javascripts/editor/graphql/get_item.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query ToolbarItem($id: String!) {
- item(id: $id) @client {
- id
- label
- icon
- selected
- group
- }
-}
diff --git a/app/assets/javascripts/editor/graphql/remove_items.mutation.graphql b/app/assets/javascripts/editor/graphql/remove_items.mutation.graphql
new file mode 100644
index 00000000000..627f105b0ec
--- /dev/null
+++ b/app/assets/javascripts/editor/graphql/remove_items.mutation.graphql
@@ -0,0 +1,3 @@
+mutation removeToolbarItems($ids: [ID!]) {
+ removeToolbarItems(ids: $ids) @client
+}
diff --git a/app/assets/javascripts/editor/graphql/typedefs.graphql b/app/assets/javascripts/editor/graphql/typedefs.graphql
new file mode 100644
index 00000000000..2433ebf6c66
--- /dev/null
+++ b/app/assets/javascripts/editor/graphql/typedefs.graphql
@@ -0,0 +1,23 @@
+type Item {
+ id: ID!
+ label: String!
+ icon: String
+ selected: Boolean
+ group: Int!
+ category: String
+ selectedLabel: String
+}
+
+type Items {
+ nodes: [Item]!
+}
+
+extend type Query {
+ items: Items
+}
+
+extend type Mutation {
+ updateToolbarItem(id: ID!, propsToUpdate: Item!): LocalErrors
+ removeToolbarItems(ids: [ID!]): LocalErrors
+ addToolbarItems(items: [Item]): LocalErrors
+}
diff --git a/app/assets/javascripts/editor/graphql/update_item.mutation.graphql b/app/assets/javascripts/editor/graphql/update_item.mutation.graphql
index f8424c65181..05c18988c87 100644
--- a/app/assets/javascripts/editor/graphql/update_item.mutation.graphql
+++ b/app/assets/javascripts/editor/graphql/update_item.mutation.graphql
@@ -1,3 +1,3 @@
-mutation updateItem($id: String!, $propsToUpdate: Item!) {
+mutation updateItem($id: ID!, $propsToUpdate: Item!) {
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
}
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 24ec16bf20e..c9b6a4f9913 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -207,7 +207,7 @@ const createAlert = function createAlert({
});
};
-/*
+/**
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
@@ -223,6 +223,7 @@ const createAlert = function createAlert({
* @param {Boolean} options.fadeTransition Boolean to determine whether to fade the alert out
* @param {Boolean} options.captureError Boolean to determine whether to send error to Sentry
* @param {Object} options.error Error to be captured in Sentry
+ * @deprecated Use `createAlert` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/362334.
*/
const createFlash = function createFlash({
message,
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index b6a6720e7a1..49e7dd28ff6 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,8 +1,13 @@
+import { debounce } from 'lodash';
+
import createFlash from '~/flash';
import { __ } from '~/locale';
import { getGroupPathAvailability } from '~/rest_api';
+import axios from '~/lib/utils/axios_utils';
import { slugify } from './lib/utils/text_utility';
+const DEBOUNCE_TIMEOUT_DURATION = 1000;
+
export default class Group {
constructor() {
this.groupPaths = Array.from(document.querySelectorAll('.js-autofill-group-path'));
@@ -10,7 +15,11 @@ export default class Group {
this.parentId = document.getElementById('group_parent_id');
this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this);
- this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this);
+ this.updateGroupPathSlugHandler = debounce(
+ this.updateGroupPathSlug.bind(this),
+ DEBOUNCE_TIMEOUT_DURATION,
+ );
+ this.currentApiRequestController = null;
this.groupNames.forEach((groupName) => {
groupName.addEventListener('keyup', this.updateHandler);
@@ -44,13 +53,23 @@ export default class Group {
});
}
- updateGroupPathSlug({ currentTarget: { value } = '' } = {}) {
- const slug = this.groupPaths[0]?.value || slugify(value);
+ updateGroupPathSlug({ target: { value } = '' } = {}) {
+ if (this.currentApiRequestController !== null) {
+ this.currentApiRequestController.abort();
+ }
+
+ this.currentApiRequestController = new AbortController();
+
+ const slug = slugify(value);
if (!slug) return;
- getGroupPathAvailability(slug, this.parentId?.value)
+ getGroupPathAvailability(slug, this.parentId?.value, {
+ signal: this.currentApiRequestController.signal,
+ })
.then(({ data }) => data)
.then(({ exists, suggests }) => {
+ this.currentApiRequestController = null;
+
if (exists && suggests.length) {
const [suggestedSlug] = suggests;
@@ -63,10 +82,14 @@ export default class Group {
});
}
})
- .catch(() =>
+ .catch((error) => {
+ if (axios.isCancel(error)) {
+ return;
+ }
+
createFlash({
message: __('An error occurred while checking group path. Please refresh and try again.'),
- }),
- );
+ });
+ });
}
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 4a15b810d38..02a815db09c 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -90,9 +90,16 @@ export default {
return this.getUserData.id;
},
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? this.$options.i18n.comment
- : this.$options.i18n.startThread;
+ const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
+ if (this.getNoteableData.confidential || this.noteIsConfidential) {
+ return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
+ }
+ return this.noteType === constants.COMMENT ? comment : startThread;
+ },
+ textareaPlaceholder() {
+ return this.getNoteableData.confidential || this.noteIsConfidential
+ ? this.$options.i18n.bodyPlaceholderInternal
+ : this.$options.i18n.bodyPlaceholder;
},
discussionsRequireResolution() {
return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE;
@@ -371,7 +378,7 @@ export default {
data-testid="comment-field"
data-supports-quick-actions="true"
:aria-label="$options.i18n.comment"
- :placeholder="$options.i18n.bodyPlaceholder"
+ :placeholder="textareaPlaceholder"
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleEnter()"
@keydown.ctrl.enter="handleEnter()"
@@ -419,6 +426,7 @@ export default {
class="gl-mr-3"
:disabled="disableSubmitButton"
:tracking-label="trackingLabel"
+ :is-internal-note="noteIsConfidential"
:noteable-display-name="noteableDisplayName"
:discussions-require-resolution="discussionsRequireResolution"
@click="handleSave"
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
index 30ea5d3532e..543be838920 100644
--- a/app/assets/javascripts/notes/components/comment_type_dropdown.vue
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ isInternalNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
noteableDisplayName: {
type: String,
required: true,
@@ -48,18 +53,43 @@ export default {
isNoteTypeDiscussion() {
return this.noteType === constants.DISCUSSION;
},
+ dropdownCommentButtonTitle() {
+ const { comment, internalComment } = this.$options.i18n.submitButton;
+
+ return this.isInternalNote ? internalComment : comment;
+ },
+ dropdownStartThreadButtonTitle() {
+ const { startThread, startInternalThread } = this.$options.i18n.submitButton;
+
+ return this.isInternalNote ? startInternalThread : startThread;
+ },
commentButtonTitle() {
- return this.noteType === constants.COMMENT
- ? this.$options.i18n.comment
- : this.$options.i18n.startThread;
+ const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
+
+ if (this.isInternalNote) {
+ return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
+ }
+ return this.noteType === constants.COMMENT ? comment : startThread;
},
startDiscussionDescription() {
- return this.discussionsRequireResolution
- ? this.$options.i18n.discussionThatNeedsResolution
- : this.$options.i18n.discussion;
+ const {
+ discussionThatNeedsResolution,
+ internalDiscussionThatNeedsResolution,
+ discussion,
+ internalDiscussion,
+ } = this.$options.i18n;
+
+ if (this.isInternalNote) {
+ return this.discussionsRequireResolution
+ ? internalDiscussionThatNeedsResolution
+ : internalDiscussion;
+ }
+ return this.discussionsRequireResolution ? discussionThatNeedsResolution : discussion;
},
commentDescription() {
- return sprintf(this.$options.i18n.submitButton.commentHelp, {
+ const { commentHelp, internalCommentHelp } = this.$options.i18n.submitButton;
+
+ return sprintf(this.isInternalNote ? internalCommentHelp : commentHelp, {
noteableDisplayName: this.noteableDisplayName,
});
},
@@ -101,7 +131,7 @@ export default {
:is-checked="isNoteTypeComment"
@click.stop.prevent="setNoteTypeToComment"
>
- <strong>{{ $options.i18n.submitButton.comment }}</strong>
+ <strong>{{ dropdownCommentButtonTitle }}</strong>
<p class="gl-m-0">{{ commentDescription }}</p>
</gl-dropdown-item>
<gl-dropdown-divider />
@@ -111,7 +141,7 @@ export default {
data-qa-selector="discussion_menu_item"
@click.stop.prevent="setNoteTypeToDiscussion"
>
- <strong>{{ $options.i18n.submitButton.startThread }}</strong>
+ <strong>{{ dropdownStartThreadButtonTitle }}</strong>
<p class="gl-m-0">{{ startDiscussionDescription }}</p>
</gl-dropdown-item>
</gl-dropdown>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index fe17a061c0a..6c9bc4461c2 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -4,6 +4,7 @@ import { GlSafeHtmlDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import { __ } from '~/locale';
import '~/behaviors/markdown/render_gfm';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import autosave from '../mixins/autosave';
@@ -69,6 +70,9 @@ export default {
noteBody() {
return this.note.note;
},
+ saveButtonTitle() {
+ return this.note.confidential ? __('Save internal note') : __('Save comment');
+ },
hasSuggestion() {
return this.note.suggestions && this.note.suggestions.length;
},
@@ -180,6 +184,7 @@ export default {
:note-id="note.id"
:line="line"
:note="note"
+ :save-button-title="saveButtonTitle"
:help-page-path="helpPagePath"
:discussion="discussion"
:resolve-discussion="note.resolve_discussion"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index c1e763d81ee..5dd032abd72 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -8,9 +8,11 @@ import markdownField from '~/vue_shared/components/markdown/field.vue';
import eventHub from '../event_hub';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
+import { COMMENT_FORM } from '../i18n';
import CommentFieldLayout from './comment_field_layout.vue';
export default {
+ i18n: COMMENT_FORM,
name: 'NoteForm',
components: {
markdownField,
@@ -133,6 +135,11 @@ export default {
.some((n) => n.current_user?.can_resolve_discussion) || this.isDraft
);
},
+ textareaPlaceholder() {
+ return this.discussionNote?.confidential
+ ? this.$options.i18n.bodyPlaceholderInternal
+ : this.$options.i18n.bodyPlaceholder;
+ },
noteHash() {
if (this.noteId) {
return `#note_${this.noteId}`;
@@ -350,7 +357,7 @@ export default {
data-qa-selector="reply_field"
dir="auto"
:aria-label="__('Reply to comment')"
- :placeholder="__('Write a comment or drag your files here…')"
+ :placeholder="textareaPlaceholder"
@keydown.meta.enter="handleKeySubmit()"
@keydown.ctrl.enter="handleKeySubmit()"
@keydown.exact.up="editMyLastNote()"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 11b427b9346..1ad9d593ccc 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,6 +1,7 @@
<script>
import {
GlIcon,
+ GlBadge,
GlLoadingIcon,
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
@@ -10,8 +11,6 @@ import { __, s__ } from '~/locale';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-import { NOTEABLE_TYPE_MAPPING } from '../constants';
-
export default {
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
components: {
@@ -19,6 +18,7 @@ export default {
GitlabTeamMemberBadge: () =>
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
+ GlBadge,
GlLoadingIcon,
UserNameWithStatus,
},
@@ -111,13 +111,7 @@ export default {
return this.author.name;
},
noteConfidentialityTooltip() {
- if (
- this.noteableType === NOTEABLE_TYPE_MAPPING.Issue ||
- this.noteableType === NOTEABLE_TYPE_MAPPING.MergeRequest
- ) {
- return s__('Notes|This comment is confidential and only visible to project members');
- }
- return s__('Notes|This comment is confidential and only visible to group members');
+ return s__('Notes|This internal note will always remain confidential');
},
},
mounted() {
@@ -236,15 +230,16 @@ export default {
</a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template>
- <gl-icon
+ <gl-badge
v-if="isConfidential"
v-gl-tooltip:tooltipcontainer.bottom
- data-testid="confidentialIndicator"
- name="eye-slash"
- :size="16"
+ data-testid="internalNoteIndicator"
+ variant="warning"
+ size="sm"
:title="noteConfidentialityTooltip"
- class="gl-ml-1 gl-text-orange-700 align-middle"
- />
+ >
+ {{ __('Internal note') }}
+ </gl-badge>
<slot name="extra-controls"></slot>
<gl-loading-icon
v-if="showSpinner"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 3f1dfea6b6a..0f5a517a4c5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -7,7 +7,7 @@ import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -95,6 +95,9 @@ export default {
isLoggedIn() {
return isLoggedIn();
},
+ commentType() {
+ return this.discussion.confidential ? __('internal note') : __('comment');
+ },
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
},
@@ -104,6 +107,9 @@ export default {
firstNote() {
return this.discussion.notes.slice(0, 1)[0];
},
+ saveButtonTitle() {
+ return this.discussion.confidential ? __('Reply internally') : __('Comment');
+ },
shouldShowJumpToNextDiscussion() {
return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion');
},
@@ -174,7 +180,10 @@ export default {
},
cancelReplyForm: ignoreWhilePending(async function cancelReplyForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
- const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
+ const msg = sprintf(
+ s__('Notes|Are you sure you want to cancel creating this %{commentType}?'),
+ { commentType: this.commentType },
+ );
const confirmed = await confirmAction(msg, {
primaryBtnText: __('Discard changes'),
@@ -311,7 +320,7 @@ export default {
ref="noteForm"
:discussion="discussion"
:line="diffLine"
- save-button-title="Comment"
+ :save-button-title="saveButtonTitle"
:autosave-key="autosaveKey"
@handleFormUpdateAddToReview="addReplyToReview"
@handleFormUpdate="saveReply"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index a2fbb242222..cda22b58c5b 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -108,6 +108,9 @@ export default {
author() {
return this.note.author;
},
+ commentType() {
+ return this.note.confidential ? __('internal note') : __('comment');
+ },
classNameBindings() {
return {
[`note-row-${this.note.id}`]: true,
@@ -246,14 +249,19 @@ export default {
this.$emit('handleEdit');
},
async deleteHandler() {
- const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment');
+ let { commentType } = this;
+
+ if (this.note.isDraft) {
+ // Draft internal notes (i.e. MR review comments) are not supported.
+ commentType = __('pending comment');
+ }
- const msg = sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), {
- typeOfComment,
+ const msg = sprintf(__('Are you sure you want to delete this %{commentType}?'), {
+ commentType,
});
const confirmed = await confirmAction(msg, {
primaryBtnVariant: 'danger',
- primaryBtnText: __('Delete Comment'),
+ primaryBtnText: this.note.confidential ? __('Delete Internal Note') : __('Delete Comment'),
});
if (confirmed) {
@@ -356,7 +364,9 @@ export default {
isDirty,
}) {
if (shouldConfirm && isDirty) {
- const msg = __('Are you sure you want to cancel editing this comment?');
+ const msg = sprintf(__('Are you sure you want to cancel editing this %{commentType}?'), {
+ commentType: this.commentType,
+ });
const confirmed = await confirmAction(msg, {
primaryBtnText: __('Cancel editing'),
primaryBtnVariant: 'danger',
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 951fa9733d4..4c0ee81bec0 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -6,19 +6,26 @@ export const COMMENT_FORM = {
),
note: __('Note'),
comment: __('Comment'),
+ internalComment: __('Add internal note'),
issue: __('issue'),
startThread: __('Start thread'),
+ startInternalThread: __('Start internal thread'),
mergeRequest: __('merge request'),
epic: __('epic'),
bodyPlaceholder: __('Write a comment or drag your files here…'),
- confidential: s__('Notes|Make this comment confidential'),
+ bodyPlaceholderInternal: __('Write an internal note or drag your files here…'),
+ confidential: s__('Notes|Make this an internal note'),
confidentialVisibility: s__(
- 'Notes|Confidential comments are only visible to members with the role of Reporter or higher',
+ 'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
),
discussionThatNeedsResolution: __(
'Discuss a specific suggestion or question that needs to be resolved.',
),
+ internalDiscussionThatNeedsResolution: __(
+ 'Discuss a specific suggestion or question internally that needs to be resolved.',
+ ),
discussion: __('Discuss a specific suggestion or question.'),
+ internalDiscussion: __('Discuss a specific suggestion or question internally.'),
actionButtonWithNote: __('%{actionText} & %{openOrClose} %{noteable}'),
actionButton: {
withNote: {
@@ -32,7 +39,10 @@ export const COMMENT_FORM = {
},
submitButton: {
startThread: __('Start thread'),
+ startInternalThread: __('Start internal thread'),
comment: __('Comment'),
+ internalComment: __('Add internal note'),
commentHelp: __('Add a general comment to this %{noteableDisplayName}.'),
+ internalCommentHelp: __('Add a confidential internal note to this %{noteableDisplayName}.'),
},
};
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index 408b7be6dd0..6a4f671abb9 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -90,7 +90,7 @@ const createStatusMessage = ({ reportType, status, total }) => {
if (status) {
message = __('%{reportType} %{status}');
} else if (!total) {
- message = __('%{reportType} detected %{totalStart}no%{totalEnd} new vulnerabilities.');
+ message = __('%{reportType} detected no %{totalStart}new%{totalEnd} vulnerabilities.');
} else {
message = __(
'%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 822b7a93c9c..4747f3c5dea 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -4,6 +4,7 @@ class Admin::LabelsController < Admin::ApplicationController
before_action :set_label, only: [:show, :edit, :update, :destroy]
feature_category :team_planning
+ urgency :low
def index
@labels = Label.templates.page(params[:page])
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index d3189379e6f..f84d2ed320d 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -13,6 +13,7 @@ class AutocompleteController < ApplicationController
feature_category :continuous_delivery, [:deploy_keys_with_owners]
urgency :low, [:merge_request_target_branches, :deploy_keys_with_owners, :users]
+ urgency :low, [:award_emojis]
urgency :medium, [:projects]
def users
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index e7ae941886d..11377df7a10 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -22,6 +22,7 @@ module Boards
before_action :can_move_issues?, only: [:bulk_move]
feature_category :team_planning
+ urgency :low
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 696b251301f..c3b5a887920 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -9,6 +9,7 @@ module Boards
skip_before_action :authenticate_user!, only: [:index]
feature_category :team_planning
+ urgency :low
def index
lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index d2f31258ecd..d23518cf051 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -2,6 +2,7 @@
class Dashboard::LabelsController < Dashboard::ApplicationController
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 34d9739d91c..2cb2d6bbe23 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
before_action :groups, only: :index
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index d3b737a5038..d2434d4b0ba 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -9,6 +9,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
feature_category :team_planning
+ urgency :low
def index
@sort = params[:sort]
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 2e3eebfb0ff..82e5bb6cd7c 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -19,6 +19,7 @@ class DashboardController < Dashboard::ApplicationController
feature_category :code_review, [:merge_requests]
urgency :low, [:merge_requests, :activity]
+ urgency :low, [:issues, :issues_calendar]
def activity
respond_to do |format|
diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb
index 17cdcd9cb9b..a2eb475d360 100644
--- a/app/controllers/groups/autocomplete_sources_controller.rb
+++ b/app/controllers/groups/autocomplete_sources_controller.rb
@@ -5,6 +5,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :team_planning, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
+ urgency :low, [:issues, :labels, :milestones, :commands]
urgency :low, [:merge_requests]
def members
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 39edf672252..0fbceb43be1 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -16,6 +16,7 @@ class Groups::BoardsController < Groups::ApplicationController
end
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/groups/crm/contacts_controller.rb b/app/controllers/groups/crm/contacts_controller.rb
index b59e20d9cea..5bc927911c1 100644
--- a/app/controllers/groups/crm/contacts_controller.rb
+++ b/app/controllers/groups/crm/contacts_controller.rb
@@ -2,6 +2,7 @@
class Groups::Crm::ContactsController < Groups::ApplicationController
feature_category :team_planning
+ urgency :low
before_action :validate_root_group!
before_action :authorize_read_crm_contact!
diff --git a/app/controllers/groups/crm/organizations_controller.rb b/app/controllers/groups/crm/organizations_controller.rb
index 846995ecba5..ef5ddcdbca6 100644
--- a/app/controllers/groups/crm/organizations_controller.rb
+++ b/app/controllers/groups/crm/organizations_controller.rb
@@ -2,6 +2,7 @@
class Groups::Crm::OrganizationsController < Groups::ApplicationController
feature_category :team_planning
+ urgency :low
before_action :validate_root_group!
before_action :authorize_read_crm_organization!
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 7bcc8182bd6..2d821676677 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -10,6 +10,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to :html
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 75877cdef9c..494b8c5621d 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -7,6 +7,7 @@ class Groups::MilestonesController < Groups::ApplicationController
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index bc2dcec625b..d46cf899d8c 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -61,6 +61,7 @@ class GroupsController < Groups::ApplicationController
urgency :high, [:unfoldered_environment_names]
+ urgency :low, [:issues, :issues_calendar, :preview_markdown]
# TODO: Set #show to higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/334795
urgency :low, [:merge_requests, :show, :create, :new, :update, :projects, :destroy, :edit, :activity]
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index ccfd360a781..9323d266cd5 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -2,6 +2,7 @@
class Profiles::NotificationsController < Profiles::ApplicationController
feature_category :team_planning
+ urgency :low
def show
@user = current_user
diff --git a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
index 03dcb164d94..60bcd1d7238 100644
--- a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
+++ b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
@@ -4,6 +4,7 @@ class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::Ap
respond_to :json
feature_category :planning_analytics
+ urgency :low
before_action :authorize_read_cycle_analytics!
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 90e13a7ec1a..9dbf989ca3f 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -10,6 +10,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
feature_category :snippets, [:snippets]
urgency :low, [:merge_requests, :members]
+ urgency :low, [:issues, :labels, :milestones, :commands, :contacts]
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 1ed18401bc5..36986a714fb 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -16,6 +16,7 @@ class Projects::BoardsController < Projects::ApplicationController
end
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index a1da8d4e91f..43b4cdbe9a8 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -12,6 +12,7 @@ module Projects
before_action :authorize_read_merge_request!, only: [:code, :review]
feature_category :planning_analytics
+ urgency :low
def issue
render_events(cycle_analytics[:issue].events)
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index dc6a9a73d9e..6160dafb177 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -14,6 +14,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
track_redis_hll_event :show, name: 'p_analytics_valuestream'
feature_category :planning_analytics
+ urgency :low
before_action do
push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 9f7d47b95f3..a61930d4b99 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -10,6 +10,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
before_action :authorize_resolve_discussion!, only: [:resolve, :unresolve]
feature_category :team_planning
+ urgency :low
def resolve
Discussions::ResolveService.new(project, current_user, one_or_more_discussions: discussion).execute
diff --git a/app/controllers/projects/issue_links_controller.rb b/app/controllers/projects/issue_links_controller.rb
index e8c3110574f..956557457fa 100644
--- a/app/controllers/projects/issue_links_controller.rb
+++ b/app/controllers/projects/issue_links_controller.rb
@@ -8,6 +8,7 @@ module Projects
before_action :authorize_issue_link_association!, only: :destroy
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4e1a45db697..b65616fdb3c 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -65,6 +65,13 @@ class Projects::IssuesController < Projects::ApplicationController
:toggle_award_emoji, :mark_as_spam, :related_branches,
:can_create_branch, :create_merge_request
]
+ urgency :low, [
+ :index, :calendar, :show, :new, :create, :edit, :update,
+ :destroy, :move, :reorder, :designs, :toggle_subscription,
+ :discussions, :bulk_update, :realtime_changes,
+ :toggle_award_emoji, :mark_as_spam, :related_branches,
+ :can_create_branch, :create_merge_request
+ ]
feature_category :service_desk, [:service_desk]
urgency :low, [:service_desk]
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 814081194d6..8ec2cbb41e9 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -15,6 +15,7 @@ class Projects::LabelsController < Projects::ApplicationController
respond_to :js, :html
feature_category :team_planning
+ urgency :low
def index
respond_to do |format|
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index b896e2543ff..744e45a0f9c 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -19,6 +19,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to :html
feature_category :team_planning
+ urgency :low
def index
@sort = params[:sort] || 'due_date_asc'
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 7322e08e62e..d24b232293b 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -12,6 +12,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
feature_category :team_planning
+ urgency :low
def delete_attachment
note.remove_attachment!
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index dafdeb4c9ef..bba1949a084 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -7,6 +7,7 @@ class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create]
feature_category :team_planning
+ urgency :low
private
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index d39664e1deb..27857dac2b7 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -6,6 +6,7 @@ class Projects::WorkItemsController < Projects::ApplicationController
end
feature_category :team_planning
+ urgency :low
def index
render_404 unless project&.work_items_feature_flag_enabled?
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 5667617b610..ec098372c04 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -58,6 +58,7 @@ class ProjectsController < Projects::ApplicationController
feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:export, :remove_export, :generate_new_export, :download_export]
+ urgency :low, [:preview_markdown, :new_issuable_address]
# TODO: Set high urgency for #show https://gitlab.com/gitlab-org/gitlab/-/issues/334444
urgency :low, [:refs, :show, :toggle_star, :transfer, :archive, :destroy, :update, :create,
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index ebadfd1cdfb..6069924b39a 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -4,6 +4,7 @@ class SentNotificationsController < ApplicationController
skip_before_action :authenticate_user!
feature_category :team_planning
+ urgency :low
def unsubscribe
@sent_notification = SentNotification.for(params[:id])
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index b0020f097b5..a70110c4076 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -41,3 +41,5 @@ class GroupGroupLink < ApplicationRecord
Gitlab::Access.human_access(self.group_access)
end
end
+
+GroupGroupLink.prepend_mod_with('GroupGroupLink')
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index c9254aa432f..fcd641671f5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -242,11 +242,11 @@ class Namespace < ApplicationRecord
return unless host.ends_with?(gitlab_host)
name = host.delete_suffix(gitlab_host)
- Namespace.where(parent_id: nil).by_path(name)
+ Namespace.top_most.by_path(name)
end
def top_most
- where(parent_id: nil)
+ by_parent(nil)
end
end
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index 7c67a2e175d..635111130d6 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -133,7 +133,7 @@ module Ci
job.update_column(:artifacts_expire_at, artifact.expire_at)
end
- success
+ success(artifact: artifact)
rescue ActiveRecord::RecordNotUnique => error
track_exception(error, params)
error('another artifact of the same type already exists', :bad_request)
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index c9303e19d5d..09a275c24a1 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -40,6 +40,8 @@
Soft wrap
.file-editor.code
+ - if Feature.enabled?(:source_editor_toolbar, current_user)
+ #editor-toolbar
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
diff --git a/config/feature_flags/development/ci_fix_rules_if_comparison_with_regexp_variable.yml b/config/feature_flags/development/ci_fix_rules_if_comparison_with_regexp_variable.yml
new file mode 100644
index 00000000000..be21707d376
--- /dev/null
+++ b/config/feature_flags/development/ci_fix_rules_if_comparison_with_regexp_variable.yml
@@ -0,0 +1,8 @@
+---
+name: ci_fix_rules_if_comparison_with_regexp_variable
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85310
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/359740
+milestone: '15.0'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/omit_epic_subscribed.yml b/config/feature_flags/development/omit_epic_subscribed.yml
index 13205b9c1dc..885636d6626 100644
--- a/config/feature_flags/development/omit_epic_subscribed.yml
+++ b/config/feature_flags/development/omit_epic_subscribed.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360663
milestone: '15.0'
type: development
group: group::product planning
-default_enabled: false
+default_enabled: true
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index b3c7d9cbad5..29df6da6ef1 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -56,7 +56,7 @@ Sidekiq.configure_server do |config|
config.on :startup do
# Clear any connections that might have been obtained before starting
# Sidekiq (e.g. in an initializer).
- ActiveRecord::Base.clear_all_connections!
+ ActiveRecord::Base.clear_all_connections! # rubocop:disable Database/MultipleDatabases
# Start monitor to track running jobs. By default, cancel job is not enabled
# To cancel job, it requires `SIDEKIQ_MONITOR_WORKER=1` to enable notification channel
diff --git a/db/migrate/20220511151646_add_exclude_from_free_user_cap_to_namespace_settings.rb b/db/migrate/20220511151646_add_exclude_from_free_user_cap_to_namespace_settings.rb
new file mode 100644
index 00000000000..03f6ad5911c
--- /dev/null
+++ b/db/migrate/20220511151646_add_exclude_from_free_user_cap_to_namespace_settings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddExcludeFromFreeUserCapToNamespaceSettings < Gitlab::Database::Migration[2.0]
+ enable_lock_retries!
+
+ def up
+ add_column :namespace_settings, :exclude_from_free_user_cap, :boolean, null: false, default: false
+ end
+
+ def down
+ remove_column :namespace_settings, :exclude_from_free_user_cap
+ end
+end
diff --git a/db/schema_migrations/20220511151646 b/db/schema_migrations/20220511151646
new file mode 100644
index 00000000000..d6ff140cdf8
--- /dev/null
+++ b/db/schema_migrations/20220511151646
@@ -0,0 +1 @@
+cf49c37edf23372a0117895bd2036cefd42616871f42511c6ecbe03a47b96e9f \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2963570aa85..ee69c66a29d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17419,6 +17419,7 @@ CREATE TABLE namespace_settings (
runner_token_expiration_interval integer,
subgroup_runner_token_expiration_interval integer,
project_runner_token_expiration_interval integer,
+ exclude_from_free_user_cap boolean DEFAULT false NOT NULL,
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255))
);
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index 662dc61af89..92e10a91753 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -25,6 +25,7 @@ arity
Artifactory
Asana
Asciidoctor
+asdf
Assembla
Atlassian
auditability
@@ -98,6 +99,7 @@ Casdoor
CentOS
Ceph
Certbot
+cgo
cgroup
cgroups
chai
@@ -109,6 +111,7 @@ chatbots
ChatOps
checksummed
checksumming
+Chemlab
Citrix
Citus
clonable
@@ -206,6 +209,7 @@ enums
ETag
Excon
exfiltration
+ExifTool
expirable
Facebook
failover
@@ -592,6 +596,7 @@ serializer
serializers
serializing
serverless
+setuptools
severities
sharded
sharding
@@ -825,6 +830,7 @@ Vagrantfile
validator
validators
vendored
+vendoring
versionless
viewport
viewports
diff --git a/doc/administration/geo/glossary.md b/doc/administration/geo/glossary.md
index a18e78a5e01..c6b3f26dc67 100644
--- a/doc/administration/geo/glossary.md
+++ b/doc/administration/geo/glossary.md
@@ -26,9 +26,9 @@ these definitions yet.
| Single-node site | A specific configuration of GitLab that uses exactly one node. | GitLab | single-server, single-instance
| Multi-node site | A specific configuration of GitLab that uses more than one node. | GitLab | multi-server, multi-instance, high availability |
| Primary site | A GitLab site whose data is being replicated by at least one secondary site. There can only be a single primary site. | Geo-specific | Geo deployment, Primary node |
-| Secondary site(s) | A GitLab site that is configured to replicate the data of a primary site. There can be one or more secondary sites. | Geo-specific | Geo deployment, Secondary node |
+| Secondary site | A GitLab site that is configured to replicate the data of a primary site. There can be one or more secondary sites. | Geo-specific | Geo deployment, Secondary node |
| Geo deployment | A collection of two or more GitLab sites with exactly one primary site being replicated by one or more secondary sites. | Geo-specific | |
-| Reference architecture(s) | A [specified configuration of GitLab for a number of users](../reference_architectures/index.md), possibly including multiple nodes and multiple sites. | GitLab | |
+| Reference architecture | A [specified configuration of GitLab for a number of users](../reference_architectures/index.md), possibly including multiple nodes and multiple sites. | GitLab | |
| Promoting | Changing the role of a site from secondary to primary. | Geo-specific | |
| Demoting | Changing the role of a site from primary to secondary. | Geo-specific | |
| Failover | The entire process that shifts users from a primary Site to a secondary site. This includes promoting a secondary, but contains other parts as well. For example, scheduling maintenance. | Geo-specific | |
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 2e78c53d197..5a29c5a3c54 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -992,7 +992,7 @@ On the **primary** node:
1. On the left sidebar, select **Geo > Nodes**.
1. Find the affected **secondary** site and select **Edit**.
1. Ensure the **URL** field matches the value found in `/etc/gitlab/gitlab.rb`
- in `external_url "https://gitlab.example.com"` on the frontend server(s) of
+ in `external_url "https://gitlab.example.com"` on the frontend servers of
the **secondary** node.
## Fixing common errors
@@ -1133,7 +1133,7 @@ This happens because GitLab is attempting to display registries from the [Geo tr
## Fixing client errors
-### Authorization errors from LFS HTTP(s) client requests
+### Authorization errors from LFS HTTP(S) client requests
You may have problems if you're running a version of [Git LFS](https://git-lfs.github.com/) before 2.4.2.
As noted in [this authentication issue](https://github.com/git-lfs/git-lfs/issues/3025),
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index b0797445890..6b617a21be8 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -12,9 +12,9 @@ for updating Geo sites.
## Updating to 14.9
-**DO NOT** update to GitLab 14.9.0.
+**DO NOT** update to GitLab 14.9.0. Instead, use 14.9.1 or later.
-We've discovered an issue with Geo's CI verification feature that may [cause job traces to be lost](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6664). This issue will be fixed in the next patch release.
+We've discovered an issue with Geo's CI verification feature that may [cause job traces to be lost](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6664). This issue was fixed in [the GitLab 14.9.1 patch release](https://about.gitlab.com/releases/2022/03/23/gitlab-14-9-1-released/).
If you have already updated to GitLab 14.9.0, you can disable the feature causing the issue by [disabling the `geo_job_artifact_replication` feature flag](../../feature_flags.md#how-to-enable-and-disable-features-behind-flags).
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index bd986b75142..fb9f3e9c361 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -967,7 +967,7 @@ application. This is done by updating the `git_data_dirs`.
Particular attention should be shown to:
- the storage name added to `git_data_dirs` in this section must match the
- storage name under `praefect['virtual_storages']` on the Praefect node(s). This
+ storage name under `praefect['virtual_storages']` on the Praefect nodes. This
was set in the [Praefect](#praefect) section of this guide. This document uses
`default` as the Praefect storage name.
diff --git a/doc/administration/nfs.md b/doc/administration/nfs.md
index 340d8b5c93e..5d2921c6988 100644
--- a/doc/administration/nfs.md
+++ b/doc/administration/nfs.md
@@ -377,7 +377,7 @@ Any `Operation not permitted` errors means you should investigate your NFS serve
## NFS in a Firewalled Environment
-If the traffic between your NFS server and NFS client(s) is subject to port filtering
+If the traffic between your NFS server and NFS clients is subject to port filtering
by a firewall, then you need to reconfigure that firewall to allow NFS communication.
[This guide from The Linux Documentation Project (TDLP)](https://tldp.org/HOWTO/NFS-HOWTO/security.html#FIREWALLS)
diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md
index fd04efe8473..75f2ad5ed26 100644
--- a/doc/administration/operations/extra_sidekiq_processes.md
+++ b/doc/administration/operations/extra_sidekiq_processes.md
@@ -354,7 +354,7 @@ file is written, but this can be changed by passing the `--pidfile` option to
```
Keep in mind that the PID file contains the PID of the `sidekiq-cluster`
-command and not the PID(s) of the started Sidekiq processes.
+command and not the PIDs of the started Sidekiq processes.
### Environment
diff --git a/doc/administration/package_information/licensing.md b/doc/administration/package_information/licensing.md
index a7bf5c52d7b..d27c1df0ccf 100644
--- a/doc/administration/package_information/licensing.md
+++ b/doc/administration/package_information/licensing.md
@@ -66,7 +66,7 @@ This software is based in part on the work of the Independent JPEG Group.
## Trademark Usage
-Within the GitLab documentation, reference to third party technology(ies) and/or trademarks of third party entities, may be made. The inclusion of reference to third party technology and/or entities is solely for the purposes of example(s) of how GitLab software may interact with, or be used in conjunction with, such third party technology.
+Within the GitLab documentation, reference to third-party technologies and/or trademarks of third-party entities may be made. The inclusion of reference to third-party technology and/or entities is solely for the purposes of examples of how GitLab software may interact with, or be used in conjunction with, such third-party technology.
All trademarks, materials, documentation, and other intellectual property remain the property of any/all such third party.
### Trademark Requirements
diff --git a/doc/administration/troubleshooting/elasticsearch.md b/doc/administration/troubleshooting/elasticsearch.md
index c45938ecd3f..97e625eb0a3 100644
--- a/doc/administration/troubleshooting/elasticsearch.md
+++ b/doc/administration/troubleshooting/elasticsearch.md
@@ -289,7 +289,7 @@ If the issue is:
Go indexer was a beta indexer which can be optionally turned on/off, but in 12.3 it reached stable status and is now the default.
- Not concerning the Go indexer, it is almost always an
Elasticsearch-side issue. This means you should reach out to your Elasticsearch administrator
- regarding the error(s) you are seeing. If you are unsure here, it never hurts to reach
+ regarding the errors you are seeing. If you are unsure here, it never hurts to reach
out to GitLab support.
Beyond that, review the error. If it is:
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index 62ea3bcfa3c..7a64bcc9b87 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -315,7 +315,7 @@ queue.each { |job| job.delete if <condition>}
Have a look at the section below for cancelling running jobs.
-In the method above, `<queue-name>` is the name of the queue that contains the job(s) you want to delete and `<condition>` decides which jobs get deleted.
+In the method above, `<queue-name>` is the name of the queue that contains the jobs you want to delete and `<condition>` decides which jobs get deleted.
Commonly, `<condition>` references the job arguments, which depend on the type of job in question. To find the arguments for a specific queue, you can have a look at the `perform` function of the related worker file, commonly found at `/app/workers/<queue-name>_worker.rb`.
diff --git a/doc/api/index.md b/doc/api/index.md
index f78a501fb11..78e5f980679 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -348,7 +348,7 @@ The following table shows the possible return codes for API requests.
| Return values | Description |
|--------------------------|-------------|
-| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, and the resource(s) itself is returned as JSON. |
+| `200 OK` | The `GET`, `PUT` or `DELETE` request was successful, and the resource itself is returned as JSON. |
| `204 No Content` | The server has successfully fulfilled the request, and there is no additional content to send in the response payload body. |
| `201 Created` | The `POST` request was successful, and the resource is returned as JSON. |
| `304 Not Modified` | The resource hasn't been modified since the last request. |
diff --git a/doc/api/issues.md b/doc/api/issues.md
index e82aa8da8ed..44b947f14dc 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1166,7 +1166,7 @@ PUT /projects/:id/issues/:issue_iid
| Attribute | Type | Required | Description |
|----------------|---------|----------|------------------------------------------------------------------------------------------------------------|
| `add_labels` | string | no | Comma-separated label names to add to an issue. |
-| `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | integer array | no | The ID of the users to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
| `confidential` | boolean | no | Updates an issue to be confidential |
| `description` | string | no | The description of an issue. Limited to 1,048,576 characters. |
| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 0c065c0f2f5..abe9cb65f95 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1103,8 +1103,8 @@ POST /projects/:id/merge_requests
| `target_branch` | string | yes | The target branch. |
| `title` | string | yes | Title of MR. |
| `assignee_id` | integer | no | Assignee user ID. |
-| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
-| `reviewer_ids` | integer array | no | The ID of the user(s) added as a reviewer to the MR. If set to `0` or left empty, no reviewers are added. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. |
+| `assignee_ids` | integer array | no | The ID of the users to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
+| `reviewer_ids` | integer array | no | The ID of the users added as a reviewer to the MR. If set to `0` or left empty, no reviewers are added. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. |
| `description` | string | no | Description of MR. Limited to 1,048,576 characters. |
| `target_project_id` | integer | no | The target project (numeric ID). |
| `labels` | string | no | Labels for MR as a comma-separated list. |
@@ -1271,8 +1271,8 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch. |
| `title` | string | no | Title of MR. |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
-| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
-| `reviewer_ids` | integer array | no | The ID of the user(s) set as a reviewer to the MR. Set the value to `0` or provide an empty value to unset all reviewers. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. |
+| `assignee_ids` | integer array | no | The ID of the users to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
+| `reviewer_ids` | integer array | no | The ID of the users set as a reviewer to the MR. Set the value to `0` or provide an empty value to unset all reviewers. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49341) in GitLab 13.8. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `add_labels` | string | no | Comma-separated label names to add to a merge request. |
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 610fd97c810..1c2cfef8e1b 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Pages domains API **(FREE)**
-Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages](https://about.gitlab.com/stages-devops-lifecycle/pages/).
+Endpoints for connecting custom domains and TLS certificates in [GitLab Pages](https://about.gitlab.com/stages-devops-lifecycle/pages/).
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index a55ce223084..7ed0f41448a 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -253,8 +253,8 @@ listed in the descriptions of the relevant settings.
| `asset_proxy_enabled` | boolean | no | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. GitLab restart is required to apply changes. |
| `asset_proxy_secret_key` | string | no | Shared secret with the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_url` | string | no | URL of the asset proxy server. GitLab restart is required to apply changes. |
-| `asset_proxy_whitelist` | string or array of strings | no | (Deprecated: Use `asset_proxy_allowlist` instead) Assets that match these domain(s) are **not** proxied. Wildcards allowed. Your GitLab installation URL is automatically allowlisted. GitLab restart is required to apply changes. |
-| `asset_proxy_allowlist` | string or array of strings | no | Assets that match these domain(s) are **not** proxied. Wildcards allowed. Your GitLab installation URL is automatically allowlisted. GitLab restart is required to apply changes. |
+| `asset_proxy_whitelist` | string or array of strings | no | (Deprecated: Use `asset_proxy_allowlist` instead) Assets that match these domains are **not** proxied. Wildcards allowed. Your GitLab installation URL is automatically allowlisted. GitLab restart is required to apply changes. |
+| `asset_proxy_allowlist` | string or array of strings | no | Assets that match these domains are **not** proxied. Wildcards allowed. Your GitLab installation URL is automatically allowlisted. GitLab restart is required to apply changes. |
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It automatically builds, tests, and deploys applications based on a predefined CI/CD configuration. |
@@ -288,7 +288,7 @@ listed in the descriptions of the relevant settings.
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |
| `domain_denylist_enabled` | boolean | no | (**If enabled, requires:** `domain_denylist`) Allows blocking sign-ups from emails from specific domains. |
-| `domain_denylist` | array of strings | no | Users with email addresses that match these domain(s) **cannot** sign up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
+| `domain_denylist` | array of strings | no | Users with email addresses that match these domains **cannot** sign up. Wildcards allowed. Use separate lines for multiple entries. Ex: `domain.com`, `*.domain.com`. |
| `domain_allowlist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is `null`, meaning there is no restriction. |
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
diff --git a/doc/architecture/blueprints/database/scalability/patterns/time_decay.md b/doc/architecture/blueprints/database/scalability/patterns/time_decay.md
index 9309c581d54..b4614cde9d4 100644
--- a/doc/architecture/blueprints/database/scalability/patterns/time_decay.md
+++ b/doc/architecture/blueprints/database/scalability/patterns/time_decay.md
@@ -133,8 +133,8 @@ You can find more information on table partitioning for PostgreSQL in the
[documentation page for table partitioning](https://www.postgresql.org/docs/12/ddl-partitioning.html).
Partitioning by date intervals (for example, month, year) allows us to create much smaller tables
-(partitions) for each date interval and only access the most recent partition(s) for any
-application related operation.
+(partitions) for each date interval and only access the most recent partitions for any
+application-related operation.
We have to set the partitioning key based on the date interval of interest, which may depend on two
factors:
@@ -214,7 +214,7 @@ offloading metadata but only for the case of old data.
In the simplest use case we can provide fast and direct access to recent data, while allowing users
to download an archive with older data. This is an option evaluated in the `audit_events` use case.
Depending on the country and industry, audit events may have a very long retention period, while
-only the past month(s) of data are actively accessed through GitLab interface.
+only the past months of data are actively accessed through GitLab interface.
Additional use cases may include exporting data to a data warehouse or other types of data stores as
they may be better suited for processing that type of data. An example can be JSON logs that we
diff --git a/doc/ci/examples/semantic-release.md b/doc/ci/examples/semantic-release.md
index c74af852a9a..2e9e2dd33bf 100644
--- a/doc/ci/examples/semantic-release.md
+++ b/doc/ci/examples/semantic-release.md
@@ -35,7 +35,7 @@ You can also view or fork the complete [example source](https://gitlab.com/gitla
}
```
-1. Update the `files` key with glob pattern(s) that selects all files that should be included in the published module. More information about `files` can be found [in npm's documentation](https://docs.npmjs.com/cli/v6/configuring-npm/package-json/#files).
+1. Update the `files` key with glob patterns that selects all files that should be included in the published module. More information about `files` can be found [in npm's documentation](https://docs.npmjs.com/cli/v6/configuring-npm/package-json/#files).
1. Add a `.gitignore` file to the project to avoid committing `node_modules`:
diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md
index e6406818b4c..e876c6d7326 100644
--- a/doc/ci/services/index.md
+++ b/doc/ci/services/index.md
@@ -80,7 +80,7 @@ As mentioned before, this feature is designed to provide **network accessible**
services. A database is the simplest example of such a service.
The services feature is not designed to, and does not, add any software from the
-defined `services` image(s) to the job's container.
+defined `services` images to the job's container.
For example, if you have the following `services` defined in your job, the `php`,
`node` or `go` commands are **not** available for your script, and the job fails:
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 1806a677c23..ac21fdb3714 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -3090,6 +3090,8 @@ job:
- Unlike variables in [`script`](../variables/index.md#use-cicd-variables-in-job-scripts)
sections, variables in rules expressions are always formatted as `$VARIABLE`.
- You can use `rules:if` with `include` to [conditionally include other configuration files](includes.md#use-rules-with-include).
+- In [GitLab 15.0 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/35438),
+ variables on the right side of `=~` and `!~` expressions are evaluated as regular expressions.
**Related topics**:
diff --git a/doc/development/adding_database_indexes.md b/doc/development/adding_database_indexes.md
index d263d9b5eb5..35dbd80e4d1 100644
--- a/doc/development/adding_database_indexes.md
+++ b/doc/development/adding_database_indexes.md
@@ -158,7 +158,7 @@ and should not be used. Some other points to consider:
### Why explicit names are required
As Rails is database agnostic, it generates an index name only
-from the required options of all indexes: table name and column name(s).
+from the required options of all indexes: table name and column names.
For example, imagine the following two indexes are created in a migration:
```ruby
@@ -173,7 +173,7 @@ Creation of the second index would fail, because Rails would generate
the same name for both indexes.
This is further complicated by the behavior of the `index_exists?` method.
-It considers only the table name, column name(s) and uniqueness specification
+It considers only the table name, column names, and uniqueness specification
of the index when making a comparison. Consider:
```ruby
@@ -284,8 +284,9 @@ production clone.
### Add a migration to create the index synchronously
After the index is verified to exist on the production database, create a second
-merge request that adds the index synchronously. The synchronous
-migration results in a no-op on GitLab.com, but you should still add the
+merge request that adds the index synchronously. The schema changes must be
+updated and committed to `structure.sql` in this second merge request.
+The synchronous migration results in a no-op on GitLab.com, but you should still add the
migration as expected for other installations. The below block
demonstrates how to create the second migration for the previous
asynchronous example.
diff --git a/doc/development/code_intelligence/index.md b/doc/development/code_intelligence/index.md
index e1e2105298c..3a8845084c3 100644
--- a/doc/development/code_intelligence/index.md
+++ b/doc/development/code_intelligence/index.md
@@ -38,7 +38,7 @@ sequenceDiagram
1. The CI/CD job generates a document in an LSIF format (usually `dump.lsif`) using [an
indexer](https://lsif.dev) for the language of a project. The format
[describes](https://github.com/sourcegraph/sourcegraph/blob/main/doc/code_intelligence/explanations/writing_an_indexer.md)
- interactions between a method or function and its definition(s) or references. The
+ interactions between a method or function and its definitions or references. The
document is marked to be stored as an LSIF report artifact.
1. After receiving a request for storing the artifact, Workhorse asks
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index afb8eb5d25a..252bd1daf55 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -26,7 +26,7 @@ This is only a recommendation and the reviewer may be from a different team.
However, it is recommended to pick someone who is a [domain expert](#domain-experts).
If your merge request touches more than one domain (for example, Dynamic Analysis and GraphQL), ask for reviews from an expert from each domain.
-You can read more about the importance of involving reviewer(s) in the section on the responsibility of the author below.
+You can read more about the importance of involving reviewers in the section on the responsibility of the author below.
If you need some guidance (for example, it's your first merge request), feel free to ask
one of the [Merge request coaches](https://about.gitlab.com/company/team/).
@@ -107,7 +107,7 @@ For more information, review [the roulette README](https://gitlab.com/gitlab-org
### Approval guidelines
As described in the section on the responsibility of the maintainer below, you
-are recommended to get your merge request approved and merged by maintainer(s)
+are recommended to get your merge request approved and merged by maintainers
with [domain expertise](#domain-experts).
1. If your merge request includes backend changes (*1*), it must be
@@ -444,7 +444,7 @@ experience, refactors the existing code). Then:
- For non-mandatory suggestions, decorate with (non-blocking) so the author knows they can
optionally resolve within the merge request or follow-up at a later stage.
- There's a [Chrome/Firefox add-on](https://gitlab.com/conventionalcomments/conventional-comments-button) which you can use to apply [Conventional Comment](https://conventionalcomments.org/) prefixes.
-- Ensure there are no open dependencies. Check [linked issues](../user/project/issues/related_issues.md) for blockers. Clarify with the author(s)
+- Ensure there are no open dependencies. Check [linked issues](../user/project/issues/related_issues.md) for blockers. Clarify with the authors
if necessary. If blocked by one or more open MRs, set an [MR dependency](../user/project/merge_requests/merge_request_dependencies.md).
- After a round of line notes, it can be helpful to post a summary note such as
"Looks good to me", or "Just a couple things to address."
@@ -697,7 +697,7 @@ Properties of customer critical merge requests:
- The [VP of Development](https://about.gitlab.com/job-families/engineering/development/management/vp/) ([@clefelhocz1](https://gitlab.com/clefelhocz1)) is the DRI for deciding if a merge request qualifies as customer critical.
- The DRI applies the `customer-critical-merge-request` label to the merge request.
-- It is required that the reviewer(s) and maintainer(s) involved with a customer critical merge request are engaged as soon as this decision is made.
+- It is required that the reviewers and maintainers involved with a customer critical merge request are engaged as soon as this decision is made.
- It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it.
- It is required to adhere to GitLab [values](https://about.gitlab.com/handbook/values/) and processes when working on customer critical merge requests, taking particular note of family and friends first/work second, definition of done, iteration, and release when it's ready.
- Customer critical merge requests are required to not reduce security, introduce data-loss risk, reduce availability, nor break existing functionality per the process for [prioritizing technical decisions](https://about.gitlab.com/handbook/engineering/development/principles/#prioritizing-technical-decisions).
diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md
index def39a960d8..7f5c800216a 100644
--- a/doc/development/contributing/design.md
+++ b/doc/development/contributing/design.md
@@ -117,7 +117,7 @@ At any moment, but usually _during_ or _after_ the design's implementation:
for additions or enhancements to the design system.
- Create issues with the [`~UX debt`](issue_workflow.md#technical-and-ux-debt)
label for intentional deviations from the agreed-upon UX requirements due to
- time or feasibility challenges, linking back to the corresponding issue(s) or
- MR(s).
+ time or feasibility challenges, linking back to the corresponding issues or
+ merge requests.
- Create issues for [feature additions or enhancements](issue_workflow.md#feature-proposals)
outside the agreed-upon UX requirements to avoid scope creep.
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 6fe15e51ba8..ee1ed744cd4 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -53,7 +53,7 @@ request is as follows:
1. If you have multiple commits, combine them into a few logically organized
commits by [squashing them](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#_squashing),
but do not change the commit history if you're working on shared branches though.
-1. Push the commit(s) to your working branch in your fork.
+1. Push the commits to your working branch in your fork.
1. Submit a merge request (MR) to the `main` branch in the main GitLab project.
1. Your merge request needs at least 1 approval, but depending on your changes
you might need additional approvals. Refer to the [Approval guidelines](../code_review.md#approval-guidelines).
@@ -65,7 +65,7 @@ request is as follows:
template already provided in the "Description" field.
1. If you are contributing documentation, choose `Documentation` from the
"Choose a template" menu and fill in the description according to the template.
- 1. Use the syntax `Solves #XXX`, `Closes #XXX`, or `Refs #XXX` to mention the issue(s) your merge
+ 1. Use the syntax `Solves #XXX`, `Closes #XXX`, or `Refs #XXX` to mention the issues your merge
request addresses. Referenced issues do not [close automatically](../../user/project/issues/managing_issues.md#closing-issues-automatically).
You must close them manually once the merge request is merged.
1. The MR must include *Before* and *After* screenshots if UI changes are made.
diff --git a/doc/development/database/avoiding_downtime_in_migrations.md b/doc/development/database/avoiding_downtime_in_migrations.md
index a849095d61a..3cf9ab1ab5c 100644
--- a/doc/development/database/avoiding_downtime_in_migrations.md
+++ b/doc/development/database/avoiding_downtime_in_migrations.md
@@ -334,7 +334,7 @@ Renaming a table is possible without downtime by following our multi-release
Adding foreign keys usually works in 3 steps:
1. Start a transaction
-1. Run `ALTER TABLE` to add the constraint(s)
+1. Run `ALTER TABLE` to add the constraints
1. Check all existing data
Because `ALTER TABLE` typically acquires an exclusive lock until the end of a
diff --git a/doc/development/database/loose_foreign_keys.md b/doc/development/database/loose_foreign_keys.md
index 4123be890d6..3db24793f1b 100644
--- a/doc/development/database/loose_foreign_keys.md
+++ b/doc/development/database/loose_foreign_keys.md
@@ -117,8 +117,8 @@ Showing cross-schema foreign keys (20):
18 | N | ci_job_token_project_scope_links | projects | target_project_id | cascade
19 | N | ci_project_monthly_usages | projects | project_id | cascade
-To match FK write one or many filters to match against FROM/TO/COLUMN:
-- scripts/decomposition/generate-loose-foreign-key <filter(s)...>
+To match foreign key (FK), write one or many filters to match against FROM/TO/COLUMN:
+- scripts/decomposition/generate-loose-foreign-key (filters...)
- scripts/decomposition/generate-loose-foreign-key ci_job_artifacts project_id
- scripts/decomposition/generate-loose-foreign-key dast_site_profiles_pipelines
```
diff --git a/doc/development/database/table_partitioning.md b/doc/development/database/table_partitioning.md
index ec768136404..34cb73978bc 100644
--- a/doc/development/database/table_partitioning.md
+++ b/doc/development/database/table_partitioning.md
@@ -43,7 +43,7 @@ problem.
First, a table is partitioned on a partition key, which is a column or
set of columns which determine how the data will be split across the
partitions. The partition key is used by the database when reading or
-writing data, to decide which partition(s) need to be accessed. The
+writing data, to decide which partitions need to be accessed. The
partition key should be a column that would be included in a `WHERE`
clause on almost all queries accessing that table.
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 9cd0cc969b5..5cfdaff0448 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -1987,7 +1987,7 @@ To improve performance, sometimes we want to make initial GraphQL queries early.
}
```
-- Add startup call(s) with correct variables to the HAML file that serves as a view
+- Add startup calls with correct variables to the HAML file that serves as a view
for your application. To add GraphQL startup calls, we use
`add_page_startup_graphql_call` helper where the first parameter is a path to the
query, the second one is an object containing query variables. Path to the query is
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 9274c059cb7..39fb4510004 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -110,6 +110,11 @@ table, that column is added at the bottom. Please do not reorder
columns manually for existing tables as this causes confusion to
other people using `db/structure.sql` generated by Rails.
+NOTE:
+[Creating an index asynchronously requires two merge requests.](adding_database_indexes.md#add-a-migration-to-create-the-index-synchronously)
+When done, commit the schema change in the merge request
+that adds the index with `add_concurrent_index`.
+
When your local database in your GDK is diverging from the schema from
`main` it might be hard to cleanly commit the schema changes to
Git. In that case you can use the `scripts/regenerate-schema` script to
diff --git a/doc/development/workhorse/index.md b/doc/development/workhorse/index.md
index f7ca16e0f31..3aa7e945f53 100644
--- a/doc/development/workhorse/index.md
+++ b/doc/development/workhorse/index.md
@@ -44,7 +44,7 @@ On some operating systems, such as FreeBSD, you may have to use
### Run time dependencies
-Workhorse uses [Exiftool](https://www.sno.phy.queensu.ca/~phil/exiftool/) for
+Workhorse uses [ExifTool](https://www.sno.phy.queensu.ca/~phil/exiftool/) for
removing EXIF data (which may contain sensitive information) from uploaded
images. If you installed GitLab:
diff --git a/doc/install/docker.md b/doc/install/docker.md
index b2d8500596a..61e85778d97 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -257,7 +257,7 @@ Here's an example that deploys GitLab with four runners as a [stack](https://doc
```ruby
external_url 'https://my.domain.com/'
- gitlab_rails['initial_root_password'] = File.read('/run/secrets/gitlab_root_password')
+ gitlab_rails['initial_root_password'] = File.read('/run/secrets/gitlab_root_password').gsub("\n", "")
```
1. Create a `root_password.txt` file:
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 3d07d6d5320..e53ced6c88a 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -199,11 +199,7 @@ sudo apt-get install -y postfix
Then select 'Internet Site' and press <kbd>Enter</kbd> to confirm the hostname.
-<!-- vale gitlab.Spelling = NO -->
-
-### Exiftool
-
-<!-- vale gitlab.Spelling = YES -->
+### ExifTool
[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse#dependencies)
requires `exiftool` to remove EXIF data from uploaded images.
diff --git a/doc/update/index.md b/doc/update/index.md
index 15a14d460bb..9a8e1e2a38b 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -427,6 +427,17 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
```plaintext
Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':
```
+
+ Or
+
+ ```plaintext
+ Error executing action `run` on resource 'bash[migrate gitlab-rails database]'
+ ================================================================================
+
+ Mixlib::ShellOut::ShellCommandFailed
+ ------------------------------------
+ Command execution failed. STDOUT/STDERR suppressed for sensitive resource
+ ```
- GitLab 14.9.0 includes a
[background migration `ResetDuplicateCiRunnersTokenValuesOnProjects`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79140)
diff --git a/doc/user/search/img/issue_search_by_id.png b/doc/user/search/img/issue_search_by_id.png
deleted file mode 100644
index 96c0b5c31e1..00000000000
--- a/doc/user/search/img/issue_search_by_id.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/search/img/issue_search_by_id_v15_0.png b/doc/user/search/img/issue_search_by_id_v15_0.png
new file mode 100644
index 00000000000..f2a47984ac9
--- /dev/null
+++ b/doc/user/search/img/issue_search_by_id_v15_0.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index de5f469498e..171d8a63d2d 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -108,7 +108,7 @@ You can add this URL to your feed reader.
You can filter the **Issues** list to individual instances by their ID. For example, enter filter `#10` to return only issue 10. The same applies to the **Merge requests** list. Enter filter `#30` to return only merge request 30.
-![filter issues by specific ID](img/issue_search_by_id.png)
+![filter issues by specific ID](img/issue_search_by_id_v15_0.png)
### Filtering merge requests by approvers **(PREMIUM)**
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 65e784fe6d6..0d74bc841b1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -228,6 +228,7 @@ module API
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Integrations
+ mount ::API::Integrations::JiraConnect::Subscriptions
mount ::API::Invitations
mount ::API::IssueLinks
mount ::API::Issues
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 56633c07774..6e3005ce676 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -8,6 +8,7 @@ module API
prepend_mod_with('API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule
feature_category :team_planning
+ urgency :low
before { authenticate! }
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 72ba39861a9..b4e411b0457 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -305,7 +305,7 @@ module API
result = ::Ci::JobArtifacts::CreateService.new(job).execute(artifacts, params, metadata_file: metadata)
if result[:status] == :success
- log_artifact_size(artifacts)
+ log_artifact_size(result[:artifact])
status :created
body "201"
else
diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb
index e9350da555c..180b6110cf2 100644
--- a/lib/api/group_boards.rb
+++ b/lib/api/group_boards.rb
@@ -8,6 +8,7 @@ module API
prepend_mod_with('API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule
feature_category :team_planning
+ urgency :low
before { authenticate! }
diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb
index 7c1f23be828..e4cbe442f58 100644
--- a/lib/api/group_labels.rb
+++ b/lib/api/group_labels.rb
@@ -8,6 +8,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
params do
requires :id, type: String, desc: 'The ID of a group'
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
index b097022e9c1..0096e466bef 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -8,6 +8,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
params do
requires :id, type: String, desc: 'The ID of a group'
diff --git a/lib/api/integrations/jira_connect/subscriptions.rb b/lib/api/integrations/jira_connect/subscriptions.rb
new file mode 100644
index 00000000000..fa19dc2be3f
--- /dev/null
+++ b/lib/api/integrations/jira_connect/subscriptions.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module API
+ class Integrations
+ module JiraConnect
+ class Subscriptions < ::API::Base
+ feature_category :integrations
+
+ before { authenticate! }
+
+ namespace :integrations do
+ namespace :jira_connect do
+ resource :subscriptions do
+ desc 'Subscribe a namespace to a JiraConnectInstallation'
+ params do
+ requires :jwt, type: String, desc: 'JWT token for authorization with the Jira Connect installation'
+ requires :namespace_path, type: String, desc: 'Path for the namespace that should be subscribed'
+ end
+ post do
+ not_found! unless Feature.enabled?(:jira_connect_oauth, current_user)
+
+ jwt = Atlassian::JiraConnect::Jwt::Symmetric.new(params[:jwt])
+ installation = JiraConnectInstallation.find_by_client_key(jwt.iss_claim)
+
+ if !installation || !jwt.valid?(installation.shared_secret) || !jwt.verify_context_qsh_claim
+ unauthorized!
+ end
+
+ jira_user = installation.client.user_info(jwt.sub_claim)
+
+ result = ::JiraConnectSubscriptions::CreateService.new(
+ installation,
+ current_user,
+ namespace_path: params['namespace_path'],
+ jira_user: jira_user
+ ).execute
+
+ if result[:status] == :success
+ status :created
+ { success: true }
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb
index 0e93a4adb65..cf075af8373 100644
--- a/lib/api/issue_links.rb
+++ b/lib/api/issue_links.rb
@@ -7,6 +7,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
params do
requires :id, type: String, desc: 'The ID of a project'
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index e9bb9fe7a97..971163c18db 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -9,6 +9,7 @@ module API
before { authenticate_non_get! }
feature_category :team_planning
+ urgency :low
helpers do
params :negatable_issue_filter_params do
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index e3253d15c15..e2d4f5d823a 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -8,6 +8,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
LABEL_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(
name: API::NO_SLASH_URL_PART_REGEX,
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index 420eabb41db..8cd72d2ab15 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -6,6 +6,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
helpers ::API::Helpers::MembersHelpers
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index 435e4bed776..9f82dbf9813 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -8,6 +8,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
params do
requires :id, type: String, desc: 'The ID of a project'
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
index c30b9d7583a..bca1376d489 100644
--- a/lib/api/sidekiq_metrics.rb
+++ b/lib/api/sidekiq_metrics.rb
@@ -10,7 +10,8 @@ module API
helpers do
def queue_metrics
- Sidekiq::Queue.all.each_with_object({}) do |queue, hash|
+ ::Gitlab::SidekiqConfig.routing_queues.each_with_object({}) do |queue_name, hash|
+ queue = Sidekiq::Queue.new(queue_name)
hash[queue.name] = {
backlog: queue.size,
latency: queue.latency.to_i
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 1bc3e25a46c..f1779df7cc6 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -7,6 +7,7 @@ module API
before { authenticate! }
feature_category :team_planning
+ urgency :low
ISSUABLE_TYPES = {
'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index 4d65b914d8d..6efb3a4f16a 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -11,8 +11,15 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+
return false unless regexp
+ if ::Feature.enabled?(:ci_fix_rules_if_comparison_with_regexp_variable)
+ # All variables are evaluated as strings, even if they are regexp strings.
+ # So, we need to convert them to regexp objects.
+ regexp = Lexeme::Pattern.build_and_evaluate(regexp, variables)
+ end
+
regexp.scan(text.to_s).present?
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
index 29c5aa5d753..a72e5dbc822 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
@@ -11,8 +11,15 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+
return true unless regexp
+ if ::Feature.enabled?(:ci_fix_rules_if_comparison_with_regexp_variable)
+ # All variables are evaluated as strings, even if they are regexp strings.
+ # So, we need to convert them to regexp objects.
+ regexp = Lexeme::Pattern.build_and_evaluate(regexp, variables)
+ end
+
regexp.scan(text.to_s).empty?
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
index c7106f3ec39..cd4106b16bb 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -35,6 +35,18 @@ module Gitlab
def self.build(string)
new(string)
end
+
+ def self.build_and_evaluate(data, variables = {})
+ return data if data.is_a?(Gitlab::UntrustedRegexp)
+
+ begin
+ new_pattern = build(data)
+ rescue Lexer::SyntaxError
+ return data
+ end
+
+ new_pattern.evaluate(variables)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
index 6d872fee39d..fa82bbe3275 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
@@ -10,6 +10,8 @@ module Gitlab
:value
end
+ attr_reader :value
+
def initialize(value)
@value = value
end
diff --git a/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
index 7415fa3104c..be41553450c 100644
--- a/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
@@ -55,7 +55,7 @@ brakeman-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -92,7 +92,7 @@ flawfinder-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -113,7 +113,7 @@ kubesec-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -144,7 +144,7 @@ gosec-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/mobsf:$SAST_ANALYZER_IMAGE_TAG"
mobsf-android-sast:
@@ -178,7 +178,7 @@ nodejs-scan-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -194,7 +194,7 @@ phpcs-security-audit-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -210,7 +210,7 @@ pmd-apex-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -226,22 +226,14 @@ security-code-scan-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: '2'
+ SAST_ANALYZER_IMAGE_TAG: '3'
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /security-code-scan/
when: never
- # This rule shim will be removed in %15.0,
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/350935
- - if: $CI_COMMIT_BRANCH && $CI_SERVER_VERSION_MAJOR == '14'
- exists:
- - '**/*.csproj'
- - '**/*.vbproj'
- if: $CI_COMMIT_BRANCH
- variables:
- SAST_ANALYZER_IMAGE_TAG: '3'
exists:
- '**/*.csproj'
- '**/*.vbproj'
@@ -252,7 +244,7 @@ semgrep-sast:
name: "$SAST_ANALYZER_IMAGE"
variables:
SEARCH_MAX_DEPTH: 20
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
- if: $SAST_DISABLED
@@ -275,7 +267,7 @@ sobelow-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -291,7 +283,7 @@ spotbugs-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_EXCLUDED_ANALYZERS =~ /spotbugs/
diff --git a/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
index ab701bcb446..f8e6e152ab9 100644
--- a/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/SAST.latest.gitlab-ci.yml
@@ -60,7 +60,7 @@ brakeman-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -112,7 +112,7 @@ flawfinder-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -143,7 +143,7 @@ kubesec-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -185,7 +185,7 @@ gosec-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/mobsf:$SAST_ANALYZER_IMAGE_TAG"
mobsf-android-sast:
@@ -237,7 +237,7 @@ nodejs-scan-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -258,7 +258,7 @@ phpcs-security-audit-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -279,7 +279,7 @@ pmd-apex-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -324,7 +324,7 @@ semgrep-sast:
name: "$SAST_ANALYZER_IMAGE"
variables:
SERACH_MAX_DEPTH: 20
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
- if: $SAST_DISABLED
@@ -359,7 +359,7 @@ sobelow-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_DISABLED
@@ -380,7 +380,7 @@ spotbugs-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
- SAST_ANALYZER_IMAGE_TAG: 2
+ SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
rules:
- if: $SAST_EXCLUDED_ANALYZERS =~ /spotbugs/
diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
index abd9bb4f0dc..b34bfe2a53c 100644
--- a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
@@ -67,6 +67,8 @@ variables:
bandit:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "2"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -74,6 +76,8 @@ bandit:
brakeman:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -90,6 +94,8 @@ gosec:
spotbugs:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -97,6 +103,8 @@ spotbugs:
flawfinder:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -104,6 +112,8 @@ flawfinder:
phpcs-security-audit:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -120,6 +130,8 @@ security-code-scan:
nodejs-scan:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -127,6 +139,8 @@ nodejs-scan:
eslint:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "2"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -139,10 +153,12 @@ secrets:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
$SECURE_BINARIES_ANALYZERS =~ /\bsecrets\b/
variables:
- SECURE_BINARIES_ANALYZER_VERSION: "3"
+ SECURE_BINARIES_ANALYZER_VERSION: "4"
semgrep:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -150,6 +166,8 @@ semgrep:
sobelow:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -157,6 +175,8 @@ sobelow:
pmd-apex:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
@@ -164,6 +184,8 @@ pmd-apex:
kubesec:
extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
only:
variables:
- $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 1895f0fab32..677b4485288 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -49,7 +49,7 @@ module Gitlab
# It does not include the default public schema
EXTRA_SCHEMAS = [DYNAMIC_PARTITIONS_SCHEMA, STATIC_PARTITIONS_SCHEMA].freeze
- PRIMARY_DATABASE_NAME = ActiveRecord::Base.connection_db_config.name.to_sym
+ PRIMARY_DATABASE_NAME = ActiveRecord::Base.connection_db_config.name.to_sym # rubocop:disable Database/MultipleDatabases
def self.database_base_models
@database_base_models ||= {
@@ -94,21 +94,6 @@ module Gitlab
Gitlab::Application.config.database_configuration[Rails.env].include?(database_name.to_s)
end
- def self.main_database?(name)
- # The database is `main` if it is a first entry in `database.yml`
- # Rails internally names them `primary` to avoid confusion
- # with broad `primary` usage we use `main` instead
- #
- # TODO: The explicit `== 'main'` is needed in a transition period till
- # the `database.yml` is not migrated into `main:` syntax
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65243
- ActiveRecord::Base.configurations.primary?(name.to_s) || name.to_s == 'main'
- end
-
- def self.ci_database?(name)
- name.to_s == CI_DATABASE_NAME
- end
-
class PgUser < ApplicationRecord
self.table_name = 'pg_user'
self.primary_key = :usename
diff --git a/lib/gitlab/database/load_balancing/load_balancer.rb b/lib/gitlab/database/load_balancing/load_balancer.rb
index 1e27bcfc55d..191ebe18b8a 100644
--- a/lib/gitlab/database/load_balancing/load_balancer.rb
+++ b/lib/gitlab/database/load_balancing/load_balancer.rb
@@ -255,6 +255,7 @@ module Gitlab
# ActiveRecord::ConnectionAdapters::ConnectionHandler handles fetching,
# and caching for connections pools for each "connection", so we
# leverage that.
+ # rubocop:disable Database/MultipleDatabases
def pool
ActiveRecord::Base.connection_handler.retrieve_connection_pool(
@configuration.primary_connection_specification_name,
@@ -262,6 +263,7 @@ module Gitlab
shard: ActiveRecord::Base.default_shard
) || raise(::ActiveRecord::ConnectionNotEstablished)
end
+ # rubocop:enable Database/MultipleDatabases
def wal_diff(location1, location2)
read_write do |connection|
diff --git a/lib/gitlab/database/migrations/observers/query_log.rb b/lib/gitlab/database/migrations/observers/query_log.rb
index 8ca57bb7df9..543e6b8e302 100644
--- a/lib/gitlab/database/migrations/observers/query_log.rb
+++ b/lib/gitlab/database/migrations/observers/query_log.rb
@@ -6,7 +6,7 @@ module Gitlab
module Observers
class QueryLog < MigrationObserver
def before
- @logger_was = ActiveRecord::Base.logger
+ @logger_was = ActiveRecord::Base.logger # rubocop:disable Database/MultipleDatabases
file_path = File.join(output_dir, "migration.log")
@logger = Logger.new(file_path)
ActiveRecord::Base.logger = @logger
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index 3eef41a2ca2..ac9a7d25fc2 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -141,6 +141,20 @@ module Gitlab
.to_h
end
+ # Get the list of queues from all available workers following queue
+ # routing rules. Sidekiq::Queue.all fetches the list of queues from Redis.
+ # It may contain some redundant, obsolete queues from previous iterations
+ # of GitLab.
+ def routing_queues
+ @routing_queues ||= workers.map do |worker|
+ if worker.klass.is_a?(Gitlab::SidekiqConfig::DummyWorker)
+ worker.queue
+ else
+ ::Gitlab::SidekiqConfig::WorkerRouter.global.route(worker.klass)
+ end
+ end.uniq.sort
+ end
+
private
def find_workers(root, ee:, jh:)
diff --git a/lib/gitlab/sidekiq_config/dummy_worker.rb b/lib/gitlab/sidekiq_config/dummy_worker.rb
index 8a2ea1acaab..ba1f2b8d2ab 100644
--- a/lib/gitlab/sidekiq_config/dummy_worker.rb
+++ b/lib/gitlab/sidekiq_config/dummy_worker.rb
@@ -22,6 +22,10 @@ module Gitlab
@attributes[:queue]
end
+ def queue
+ @attributes[:queue]
+ end
+
def queue_namespace
nil
end
diff --git a/lib/gitlab/sidekiq_config/worker.rb b/lib/gitlab/sidekiq_config/worker.rb
index 1e3fb675ca7..1abdcde6a15 100644
--- a/lib/gitlab/sidekiq_config/worker.rb
+++ b/lib/gitlab/sidekiq_config/worker.rb
@@ -9,7 +9,7 @@ module Gitlab
delegate :feature_category_not_owned?, :generated_queue_name, :get_feature_category,
:get_sidekiq_options, :get_tags, :get_urgency, :get_weight,
- :get_worker_resource_boundary, :idempotent?, :queue_namespace,
+ :get_worker_resource_boundary, :idempotent?, :queue_namespace, :queue,
:worker_has_external_dependencies?,
to: :klass
diff --git a/lib/tasks/gitlab/db/validate_config.rake b/lib/tasks/gitlab/db/validate_config.rake
index cc5f6bb6e09..66aa949cc94 100644
--- a/lib/tasks/gitlab/db/validate_config.rake
+++ b/lib/tasks/gitlab/db/validate_config.rake
@@ -6,7 +6,7 @@ namespace :gitlab do
namespace :db do
desc 'Validates `config/database.yml` to ensure a correct behavior is configured'
task validate_config: :environment do
- original_db_config = ActiveRecord::Base.connection_db_config
+ original_db_config = ActiveRecord::Base.connection_db_config # rubocop:disable Database/MultipleDatabases
# The include_replicas: is a legacy name to fetch all hidden entries (replica: true or database_tasks: false)
# Once we upgrade to Rails 7.x this should be changed to `include_hidden: true`
@@ -15,6 +15,7 @@ namespace :gitlab do
db_configs = db_configs.reject(&:replica?)
# Map each database connection into unique identifier of system+database
+ # rubocop:disable Database/MultipleDatabases
all_connections = db_configs.map do |db_config|
identifier =
begin
@@ -32,6 +33,7 @@ namespace :gitlab do
identifier: identifier
}
end.compact
+ # rubocop:enable Database/MultipleDatabases
unique_connections = all_connections.group_by { |connection| connection[:identifier] }
primary_connection = all_connections.find { |connection| ActiveRecord::Base.configurations.primary?(connection[:name]) }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c4babc1ae93..d275513fdfc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -902,7 +902,7 @@ msgstr ""
msgid "%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}"
msgstr ""
-msgid "%{reportType} detected %{totalStart}no%{totalEnd} new vulnerabilities."
+msgid "%{reportType} detected no %{totalStart}new%{totalEnd} vulnerabilities."
msgstr ""
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}."
@@ -2088,6 +2088,9 @@ msgstr ""
msgid "Add a comment to this line or drag for multiple lines"
msgstr ""
+msgid "Add a confidential internal note to this %{noteableDisplayName}."
+msgstr ""
+
msgid "Add a custom message with details about the instance's shared runners. The message is visible in group and project CI/CD settings, in the Runners section. Markdown is supported."
msgstr ""
@@ -2211,6 +2214,9 @@ msgstr ""
msgid "Add image comment"
msgstr ""
+msgid "Add internal note"
+msgstr ""
+
msgid "Add italic text"
msgstr ""
@@ -4807,7 +4813,7 @@ msgstr ""
msgid "Are you sure you want to attempt to merge?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to cancel editing this %{commentType}?"
msgstr ""
msgid "Are you sure you want to close this blocked issue?"
@@ -4819,7 +4825,7 @@ msgstr ""
msgid "Are you sure you want to delete these artifacts?"
msgstr ""
-msgid "Are you sure you want to delete this %{typeOfComment}?"
+msgid "Are you sure you want to delete this %{commentType}?"
msgstr ""
msgid "Are you sure you want to delete this SSH key?"
@@ -11920,6 +11926,9 @@ msgstr ""
msgid "Delete Comment"
msgstr ""
+msgid "Delete Internal Note"
+msgstr ""
+
msgid "Delete Key"
msgstr ""
@@ -13214,6 +13223,12 @@ msgstr ""
msgid "Discover|Upgrade now"
msgstr ""
+msgid "Discuss a specific suggestion or question internally that needs to be resolved."
+msgstr ""
+
+msgid "Discuss a specific suggestion or question internally."
+msgstr ""
+
msgid "Discuss a specific suggestion or question that needs to be resolved."
msgstr ""
@@ -18693,6 +18708,9 @@ msgstr ""
msgid "Hide"
msgstr ""
+msgid "Hide Live Preview"
+msgstr ""
+
msgid "Hide archived projects"
msgstr ""
@@ -20616,6 +20634,9 @@ msgstr ""
msgid "Internal error occurred while delivering this webhook."
msgstr ""
+msgid "Internal note"
+msgstr ""
+
msgid "Internal users"
msgstr ""
@@ -25732,16 +25753,19 @@ msgstr ""
msgid "Notes rate limit"
msgstr ""
+msgid "Notes|Are you sure you want to cancel creating this %{commentType}?"
+msgstr ""
+
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
msgid "Notes|Collapse replies"
msgstr ""
-msgid "Notes|Confidential comments are only visible to members with the role of Reporter or higher"
+msgid "Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher"
msgstr ""
-msgid "Notes|Make this comment confidential"
+msgid "Notes|Make this an internal note"
msgstr ""
msgid "Notes|Show all activity"
@@ -25756,10 +25780,7 @@ msgstr ""
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
msgstr ""
-msgid "Notes|This comment is confidential and only visible to group members"
-msgstr ""
-
-msgid "Notes|This comment is confidential and only visible to project members"
+msgid "Notes|This internal note will always remain confidential"
msgstr ""
msgid "Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
@@ -31664,6 +31685,9 @@ msgstr ""
msgid "Reply by email"
msgstr ""
+msgid "Reply internally"
+msgstr ""
+
msgid "Reply to comment"
msgstr ""
@@ -32980,6 +33004,9 @@ msgstr ""
msgid "Save deploy freeze"
msgstr ""
+msgid "Save internal note"
+msgstr ""
+
msgid "Save password"
msgstr ""
@@ -35931,6 +35958,9 @@ msgstr ""
msgid "Start inputting changes and we will generate a YAML-file for you to add to your repository"
msgstr ""
+msgid "Start internal thread"
+msgstr ""
+
msgid "Start merge train"
msgstr ""
@@ -42840,6 +42870,9 @@ msgstr ""
msgid "Write a description…"
msgstr ""
+msgid "Write an internal note or drag your files here…"
+msgstr ""
+
msgid "Write milestone description..."
msgstr ""
@@ -44731,6 +44764,9 @@ msgid_plural "instances completed"
msgstr[0] ""
msgstr[1] ""
+msgid "internal note"
+msgstr ""
+
msgid "invalid milestone state `%{state}`"
msgstr ""
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 08183badda1..ceb4af03f89 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -105,6 +105,24 @@ RSpec.describe 'Group' do
expect(page).to have_content('Group path is available')
end
+
+ context 'when filling in the `Group name` field' do
+ let_it_be(:group1) { create(:group, :public, path: 'foo-bar') }
+ let_it_be(:group2) { create(:group, :public, path: 'bar-baz') }
+
+ it 'automatically populates the `Group URL` field' do
+ fill_in 'Group name', with: 'Foo bar'
+ # Wait for debounce in app/assets/javascripts/group.js#18
+ sleep(1)
+ fill_in 'Group name', with: 'Bar baz'
+ # Wait for debounce in app/assets/javascripts/group.js#18
+ sleep(1)
+
+ wait_for_requests
+
+ expect(page).to have_field('Group URL', with: 'bar-baz1')
+ end
+ end
end
describe 'Mattermost team creation' do
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index ae678f6f25f..c031cae11df 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,3 +1,4 @@
+import { Emitter } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import EditBlob from '~/blob_edit/edit_blob';
@@ -5,6 +6,7 @@ import { SourceEditorExtension } from '~/editor/extensions/source_editor_extensi
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
jest.mock('~/editor/source_editor');
@@ -12,11 +14,13 @@ jest.mock('~/editor/extensions/source_editor_extension_base');
jest.mock('~/editor/extensions/source_editor_file_template_ext');
jest.mock('~/editor/extensions/source_editor_markdown_ext');
jest.mock('~/editor/extensions/source_editor_markdown_livepreview_ext');
+jest.mock('~/editor/extensions/source_editor_toolbar_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
{ definition: SourceEditorExtension },
{ definition: FileTemplateExtension },
+ { definition: ToolbarExtension },
];
const markdownExtensions = [
{ definition: EditorMarkdownExtension },
@@ -27,12 +31,17 @@ const markdownExtensions = [
];
describe('Blob Editing', () => {
- const useMock = jest.fn();
+ let blobInstance;
+ const useMock = jest.fn(() => markdownExtensions);
+ const unuseMock = jest.fn();
+ const emitter = new Emitter();
const mockInstance = {
use: useMock,
+ unuse: unuseMock,
setValue: jest.fn(),
getValue: jest.fn().mockReturnValue('test value'),
focus: jest.fn(),
+ onDidChangeModelLanguage: emitter.event,
};
beforeEach(() => {
setHTMLFixture(`
@@ -45,18 +54,18 @@ describe('Blob Editing', () => {
jest.spyOn(SourceEditor.prototype, 'createInstance').mockReturnValue(mockInstance);
});
afterEach(() => {
- SourceEditorExtension.mockClear();
- EditorMarkdownExtension.mockClear();
- EditorMarkdownPreviewExtension.mockClear();
- FileTemplateExtension.mockClear();
+ jest.clearAllMocks();
+ unuseMock.mockClear();
+ useMock.mockClear();
resetHTMLFixture();
});
const editorInst = (isMarkdown) => {
- return new EditBlob({
+ blobInstance = new EditBlob({
isMarkdown,
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
});
+ return blobInstance;
};
const initEditor = async (isMarkdown = false) => {
@@ -81,6 +90,22 @@ describe('Blob Editing', () => {
expect(useMock).toHaveBeenCalledTimes(2);
expect(useMock.mock.calls[1]).toEqual([markdownExtensions]);
});
+
+ it('correctly handles switching from markdown and un-uses markdown extensions', async () => {
+ await initEditor(true);
+ expect(unuseMock).not.toHaveBeenCalled();
+ await emitter.fire({ newLanguage: 'plaintext', oldLanguage: 'markdown' });
+ expect(unuseMock).toHaveBeenCalledWith(markdownExtensions);
+ });
+
+ it('correctly handles switching from non-markdown to markdown extensions', async () => {
+ const mdSpy = jest.fn();
+ await initEditor();
+ blobInstance.fetchMarkdownExtension = mdSpy;
+ expect(mdSpy).not.toHaveBeenCalled();
+ await emitter.fire({ newLanguage: 'markdown', oldLanguage: 'plaintext' });
+ expect(mdSpy).toHaveBeenCalled();
+ });
});
it('adds trailing newline to the blob content on submit', async () => {
diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js
index 3e6cd2a236d..12f90390c18 100644
--- a/spec/frontend/editor/components/helpers.js
+++ b/spec/frontend/editor/components/helpers.js
@@ -1,12 +1,28 @@
import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
export const buildButton = (id = 'foo-bar-btn', options = {}) => {
return {
__typename: 'Item',
id,
label: options.label || 'Foo Bar Button',
- icon: options.icon || 'foo-bar',
+ icon: options.icon || 'check',
selected: options.selected || false,
group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP,
+ onClick: options.onClick || (() => {}),
+ category: options.category || 'primary',
+ selectedLabel: options.selectedLabel || 'smth',
};
};
+
+export const warmUpCacheWithItems = (items = []) => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getToolbarItemsQuery,
+ data: {
+ items: {
+ nodes: items,
+ },
+ },
+ });
+};
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index 5135091af4a..1475d451ab3 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -1,43 +1,26 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
-import getToolbarItemQuery from '~/editor/graphql/get_item.query.graphql';
-import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
import { buildButton } from './helpers';
-Vue.use(VueApollo);
-
describe('Source Editor Toolbar button', () => {
let wrapper;
- let mockApollo;
const defaultBtn = buildButton();
const findButton = () => wrapper.findComponent(GlButton);
- const createComponentWithApollo = ({ propsData } = {}) => {
- mockApollo = createMockApollo();
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: getToolbarItemQuery,
- variables: { id: defaultBtn.id },
- data: {
- item: {
- ...defaultBtn,
- },
- },
- });
-
+ const createComponent = (props = { button: defaultBtn }) => {
wrapper = shallowMount(SourceEditorToolbarButton, {
- propsData,
- apolloProvider: mockApollo,
+ propsData: {
+ ...props,
+ },
});
};
afterEach(() => {
wrapper.destroy();
- mockApollo = null;
+ wrapper = null;
});
describe('default', () => {
@@ -49,98 +32,51 @@ describe('Source Editor Toolbar button', () => {
category: 'secondary',
variant: 'info',
};
+
+ it('does not render the button if the props have not been passed', () => {
+ createComponent({});
+ expect(findButton().vm).toBeUndefined();
+ });
+
it('renders a default button without props', async () => {
- createComponentWithApollo();
+ createComponent();
const btn = findButton();
expect(btn.exists()).toBe(true);
expect(btn.props()).toMatchObject(defaultProps);
});
it('renders a button based on the props passed', async () => {
- createComponentWithApollo({
- propsData: {
- button: customProps,
- },
+ createComponent({
+ button: customProps,
});
const btn = findButton();
expect(btn.props()).toMatchObject(customProps);
});
});
- describe('button updates', () => {
- it('it properly updates button on Apollo cache update', async () => {
- const { id } = defaultBtn;
-
- createComponentWithApollo({
- propsData: {
- button: {
- id,
- },
- },
- });
-
- expect(findButton().props('selected')).toBe(false);
-
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: getToolbarItemQuery,
- variables: { id },
- data: {
- item: {
- ...defaultBtn,
- selected: true,
- },
- },
- });
-
- jest.runOnlyPendingTimers();
- await nextTick();
-
- expect(findButton().props('selected')).toBe(true);
- });
- });
-
describe('click handler', () => {
- it('fires the click handler on the button when available', () => {
+ it('fires the click handler on the button when available', async () => {
const spy = jest.fn();
- createComponentWithApollo({
- propsData: {
- button: {
- onClick: spy,
- },
+ createComponent({
+ button: {
+ onClick: spy,
},
});
expect(spy).not.toHaveBeenCalled();
findButton().vm.$emit('click');
+
+ await nextTick();
expect(spy).toHaveBeenCalled();
});
- it('emits the "click" event', () => {
- createComponentWithApollo();
+ it('emits the "click" event', async () => {
+ createComponent();
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+
findButton().vm.$emit('click');
+ await nextTick();
+
expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
});
- it('triggers the mutation exposing the changed "selected" prop', () => {
- const { id } = defaultBtn;
- createComponentWithApollo({
- propsData: {
- button: {
- id,
- },
- },
- });
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
- expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled();
- findButton().vm.$emit('click');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateToolbarItemMutation,
- variables: {
- id,
- propsToUpdate: {
- selected: true,
- },
- },
- });
- });
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js b/spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js
new file mode 100644
index 00000000000..41c48aa0a58
--- /dev/null
+++ b/spec/frontend/editor/components/source_editor_toolbar_graphql_spec.js
@@ -0,0 +1,112 @@
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
+import removeItemsMutation from '~/editor/graphql/remove_items.mutation.graphql';
+import updateToolbarItemMutation from '~/editor/graphql/update_item.mutation.graphql';
+import addToolbarItemsMutation from '~/editor/graphql/add_items.mutation.graphql';
+import { buildButton, warmUpCacheWithItems } from './helpers';
+
+describe('Source Editor toolbar Apollo client', () => {
+ const item1 = buildButton('foo');
+ const item2 = buildButton('bar');
+
+ const getItems = () =>
+ apolloProvider.defaultClient.cache.readQuery({ query: getToolbarItemsQuery })?.items?.nodes ||
+ [];
+ const getItem = (id) => {
+ return getItems().find((item) => item.id === id);
+ };
+
+ afterEach(() => {
+ apolloProvider.defaultClient.clearStore();
+ });
+
+ describe('Mutations', () => {
+ describe('addToolbarItems', () => {
+ function addButtons(items) {
+ return apolloProvider.defaultClient.mutate({
+ mutation: addToolbarItemsMutation,
+ variables: {
+ items,
+ },
+ });
+ }
+ it.each`
+ cache | idsToAdd | itemsToAdd | expectedResult | comment
+ ${[]} | ${'empty array'} | ${[]} | ${[]} | ${''}
+ ${[]} | ${'undefined'} | ${undefined} | ${[]} | ${''}
+ ${[]} | ${item2.id} | ${[item2]} | ${[item2]} | ${''}
+ ${[]} | ${item1.id} | ${[item1]} | ${[item1]} | ${''}
+ ${[]} | ${[item1.id, item2.id]} | ${[item1, item2]} | ${[item1, item2]} | ${''}
+ ${[]} | ${[item1.id]} | ${item1} | ${[item1]} | ${'does not fail if the item is an Object'}
+ ${[item2]} | ${[item1.id]} | ${item1} | ${[item2, item1]} | ${'does not fail if the item is an Object'}
+ ${[item1]} | ${[item2.id]} | ${[item2]} | ${[item1, item2]} | ${'correctly adds items to the pre-populated cache'}
+ `('adds $idsToAdd item(s) to $cache', async ({ cache, itemsToAdd, expectedResult }) => {
+ await warmUpCacheWithItems(cache);
+ await addButtons(itemsToAdd);
+ await expect(getItems()).toEqual(expectedResult);
+ });
+ });
+
+ describe('removeToolbarItems', () => {
+ function removeButtons(ids) {
+ return apolloProvider.defaultClient.mutate({
+ mutation: removeItemsMutation,
+ variables: {
+ ids,
+ },
+ });
+ }
+
+ it.each`
+ cache | cacheIds | toRemove | expected
+ ${[item1, item2]} | ${[item1.id, item2.id]} | ${[item1.id]} | ${[item2]}
+ ${[item1, item2]} | ${[item1.id, item2.id]} | ${[item2.id]} | ${[item1]}
+ ${[item1, item2]} | ${[item1.id, item2.id]} | ${[item1.id, item2.id]} | ${[]}
+ ${[item1]} | ${[item1.id]} | ${[item1.id]} | ${[]}
+ ${[item2]} | ${[item2.id]} | ${[]} | ${[item2]}
+ ${[]} | ${['undefined']} | ${[item1.id]} | ${[]}
+ ${[item1]} | ${[item1.id]} | ${[item2.id]} | ${[item1]}
+ `('removes $toRemove from the $cacheIds toolbar', async ({ cache, toRemove, expected }) => {
+ await warmUpCacheWithItems(cache);
+
+ expect(getItems()).toHaveLength(cache.length);
+
+ await removeButtons(toRemove);
+
+ expect(getItems()).toHaveLength(expected.length);
+ expect(getItems()).toEqual(expected);
+ });
+ });
+
+ describe('updateToolbarItem', () => {
+ function mutateButton(item, propsToUpdate = {}) {
+ return apolloProvider.defaultClient.mutate({
+ mutation: updateToolbarItemMutation,
+ variables: {
+ id: item.id,
+ propsToUpdate,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ warmUpCacheWithItems([item1, item2]);
+ });
+
+ it('updates the toolbar items', async () => {
+ expect(getItem(item1.id).selected).toBe(false);
+ expect(getItem(item2.id).selected).toBe(false);
+
+ await mutateButton(item1, { selected: true });
+
+ expect(getItem(item1.id).selected).toBe(true);
+ expect(getItem(item2.id).selected).toBe(false);
+
+ await mutateButton(item2, { selected: true });
+
+ expect(getItem(item1.id).selected).toBe(true);
+ expect(getItem(item2.id).selected).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js b/spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js
new file mode 100644
index 00000000000..fa5a3b2987e
--- /dev/null
+++ b/spec/frontend/editor/extensions/source_editor_toolbar_ext_spec.js
@@ -0,0 +1,156 @@
+import Vue from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
+import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
+import EditorInstance from '~/editor/source_editor_instance';
+import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
+import { buildButton, warmUpCacheWithItems } from '../components/helpers';
+
+describe('Source Editor Toolbar Extension', () => {
+ let instance;
+
+ const createInstance = (baseInstance = {}) => {
+ return new EditorInstance(baseInstance);
+ };
+ const getDefaultEl = () => document.getElementById('editor-toolbar');
+ const getCustomEl = () => document.getElementById('custom-toolbar');
+ const item1 = buildButton('foo');
+ const item2 = buildButton('bar');
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="editor-toolbar"></div><div id="custom-toolbar"></div>');
+ });
+
+ afterEach(() => {
+ apolloProvider.defaultClient.clearStore();
+ resetHTMLFixture();
+ });
+
+ describe('onSetup', () => {
+ beforeEach(() => {
+ instance = createInstance();
+ });
+
+ it.each`
+ id | type | prefix | expectedElFn
+ ${undefined} | ${'default'} | ${'Sets up'} | ${getDefaultEl}
+ ${'custom-toolbar'} | ${'custom'} | ${'Sets up'} | ${getCustomEl}
+ ${'non-existing'} | ${'default'} | ${'Does not set up'} | ${getDefaultEl}
+ `('Sets up the Vue application on $type node when node is $id', ({ id, expectedElFn }) => {
+ jest.spyOn(Vue, 'extend');
+ jest.spyOn(ToolbarExtension, 'setupVue');
+
+ const el = document.getElementById(id);
+ const expectedEl = expectedElFn();
+
+ instance.use({ definition: ToolbarExtension, setupOptions: { el } });
+
+ if (expectedEl) {
+ expect(ToolbarExtension.setupVue).toHaveBeenCalledWith(expectedEl);
+ expect(Vue.extend).toHaveBeenCalledWith(SourceEditorToolbar);
+ } else {
+ expect(ToolbarExtension.setupVue).not.toHaveBeenCalled();
+ }
+ });
+ });
+
+ describe('public API', () => {
+ beforeEach(async () => {
+ await warmUpCacheWithItems();
+ instance = createInstance();
+ instance.use({ definition: ToolbarExtension });
+ });
+
+ describe('getAllItems', () => {
+ it('returns the list of all toolbar items', async () => {
+ await expect(instance.toolbar.getAllItems()).toEqual([]);
+ await warmUpCacheWithItems([item1, item2]);
+ await expect(instance.toolbar.getAllItems()).toEqual([item1, item2]);
+ });
+ });
+
+ describe('getItem', () => {
+ it('returns a toolbar item by id', async () => {
+ await expect(instance.toolbar.getItem(item1.id)).toEqual(undefined);
+ await warmUpCacheWithItems([item1]);
+ await expect(instance.toolbar.getItem(item1.id)).toEqual(item1);
+ });
+ });
+
+ describe('addItems', () => {
+ it.each`
+ idsToAdd | itemsToAdd | expectedResult
+ ${'empty array'} | ${[]} | ${[]}
+ ${'undefined'} | ${undefined} | ${[]}
+ ${item2.id} | ${[item2]} | ${[item2]}
+ ${item1.id} | ${[item1]} | ${[item1]}
+ ${[item1.id, item2.id]} | ${[item1, item2]} | ${[item1, item2]}
+ `('adds $idsToAdd item(s) to cache', async ({ itemsToAdd, expectedResult }) => {
+ await instance.toolbar.addItems(itemsToAdd);
+ await expect(instance.toolbar.getAllItems()).toEqual(expectedResult);
+ });
+
+ it('correctly adds items to the pre-populated cache', async () => {
+ await warmUpCacheWithItems([item1]);
+ await instance.toolbar.addItems([item2]);
+ await expect(instance.toolbar.getAllItems()).toEqual([item1, item2]);
+ });
+
+ it('does not fail if the item is an Object', async () => {
+ await instance.toolbar.addItems(item1);
+ await expect(instance.toolbar.getAllItems()).toEqual([item1]);
+ });
+ });
+
+ describe('removeItems', () => {
+ beforeEach(async () => {
+ await warmUpCacheWithItems([item1, item2]);
+ });
+
+ it.each`
+ idsToRemove | expectedResult
+ ${undefined} | ${[item1, item2]}
+ ${[]} | ${[item1, item2]}
+ ${[item1.id]} | ${[item2]}
+ ${[item2.id]} | ${[item1]}
+ ${[item1.id, item2.id]} | ${[]}
+ `(
+ 'successfully removes $idsToRemove from [foo, bar]',
+ async ({ idsToRemove, expectedResult }) => {
+ await instance.toolbar.removeItems(idsToRemove);
+ await expect(instance.toolbar.getAllItems()).toEqual(expectedResult);
+ },
+ );
+ });
+
+ describe('updateItem', () => {
+ const updatedProp = {
+ icon: 'book',
+ };
+
+ beforeEach(async () => {
+ await warmUpCacheWithItems([item1, item2]);
+ });
+
+ it.each`
+ itemsToUpdate | idToUpdate | propsToUpdate | expectedResult
+ ${undefined} | ${'undefined'} | ${undefined} | ${[item1, item2]}
+ ${item2.id} | ${item2.id} | ${undefined} | ${[item1, item2]}
+ ${item2.id} | ${item2.id} | ${{}} | ${[item1, item2]}
+ ${[item1]} | ${item1.id} | ${updatedProp} | ${[{ ...item1, ...updatedProp }, item2]}
+ ${[item2]} | ${item2.id} | ${updatedProp} | ${[item1, { ...item2, ...updatedProp }]}
+ `(
+ 'updates $idToUpdate item in cache with $propsToUpdate',
+ async ({ idToUpdate, propsToUpdate, expectedResult }) => {
+ await instance.toolbar.updateItem(idToUpdate, propsToUpdate);
+ await expect(instance.toolbar.getAllItems()).toEqual(expectedResult);
+ if (propsToUpdate) {
+ await expect(instance.toolbar.getItem(idToUpdate)).toEqual(
+ expect.objectContaining(propsToUpdate),
+ );
+ }
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index f239dbcd665..1926f3e268e 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -1,5 +1,4 @@
import MockAdapter from 'axios-mock-adapter';
-import { editor as monacoEditor } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import {
@@ -31,7 +30,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
const secondLine = 'multiline';
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
- const plaintextPath = 'foo.txt';
const markdownPath = 'foo.md';
const responseData = '<div>FooBar</div>';
@@ -50,6 +48,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
+
+ instance.toolbar = {
+ addItems: jest.fn(),
+ updateItem: jest.fn(),
+ removeItems: jest.fn(),
+ };
+
extension = instance.use({
definition: EditorMarkdownPreviewExtension,
setupOptions: { previewMarkdownPath },
@@ -67,59 +72,14 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it('sets up the preview on the instance', () => {
expect(instance.markdownPreview).toEqual({
el: undefined,
- action: expect.any(Object),
+ actions: expect.any(Object),
shown: false,
modelChangeListener: undefined,
path: previewMarkdownPath,
+ actionShowPreviewCondition: expect.any(Object),
});
});
- describe('model language changes listener', () => {
- let cleanupSpy;
- let actionSpy;
-
- beforeEach(async () => {
- cleanupSpy = jest.fn();
- actionSpy = jest.fn();
- spyOnApi(extension, {
- cleanup: cleanupSpy,
- setupPreviewAction: actionSpy,
- });
- await togglePreview();
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('cleans up when switching away from markdown', () => {
- expect(cleanupSpy).not.toHaveBeenCalled();
- expect(actionSpy).not.toHaveBeenCalled();
-
- instance.updateModelLanguage(plaintextPath);
-
- expect(cleanupSpy).toHaveBeenCalled();
- expect(actionSpy).not.toHaveBeenCalled();
- });
-
- it.each`
- oldLanguage | newLanguage | setupCalledTimes
- ${'plaintext'} | ${'markdown'} | ${1}
- ${'markdown'} | ${'markdown'} | ${0}
- ${'markdown'} | ${'plaintext'} | ${0}
- ${'markdown'} | ${undefined} | ${0}
- ${undefined} | ${'markdown'} | ${1}
- `(
- 'correctly handles re-enabling of the action when switching from $oldLanguage to $newLanguage',
- ({ oldLanguage, newLanguage, setupCalledTimes } = {}) => {
- expect(actionSpy).not.toHaveBeenCalled();
- instance.updateModelLanguage(oldLanguage);
- instance.updateModelLanguage(newLanguage);
- expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes);
- },
- );
- });
-
describe('model change listener', () => {
let cleanupSpy;
let actionSpy;
@@ -144,33 +104,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(cleanupSpy).not.toHaveBeenCalled();
expect(actionSpy).not.toHaveBeenCalled();
});
-
- it('cleans up the preview when the model changes', () => {
- instance.setModel(monacoEditor.createModel('foo'));
- expect(cleanupSpy).toHaveBeenCalled();
- });
-
- it.each`
- language | setupCalledTimes
- ${'markdown'} | ${1}
- ${'plaintext'} | ${0}
- ${undefined} | ${0}
- `(
- 'correctly handles actions when the new model is $language',
- ({ language, setupCalledTimes } = {}) => {
- instance.setModel(monacoEditor.createModel('foo', language));
-
- expect(actionSpy).toHaveBeenCalledTimes(setupCalledTimes);
- },
- );
});
- describe('cleanup', () => {
+ describe('onBeforeUnuse', () => {
beforeEach(async () => {
mockAxios.onPost().reply(200, { body: responseData });
await togglePreview();
});
+ it('removes the registered buttons from the toolbar', () => {
+ expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(instance.toolbar.removeItems).toHaveBeenCalledWith([
+ EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ ]);
+ });
+
it('disposes the modelChange listener and does not fetch preview on content changes', () => {
expect(instance.markdownPreview.modelChangeListener).toBeDefined();
const fetchPreviewSpy = jest.fn();
@@ -178,7 +127,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
fetchPreview: fetchPreviewSpy,
});
- instance.cleanup();
+ instance.unuse(extension);
instance.setValue('Foo Bar');
jest.advanceTimersByTime(EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY);
@@ -188,17 +137,11 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it('removes the contextual menu action', () => {
expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBeDefined();
- instance.cleanup();
+ instance.unuse(extension);
expect(instance.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)).toBe(null);
});
- it('toggles the `shown` flag', () => {
- expect(instance.markdownPreview.shown).toBe(true);
- instance.cleanup();
- expect(instance.markdownPreview.shown).toBe(false);
- });
-
it('toggles the panel only if the preview is visible', () => {
const { el: previewEl } = instance.markdownPreview;
const parentEl = previewEl.parentElement;
@@ -206,13 +149,13 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(previewEl).toBeVisible();
expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(true);
- instance.cleanup();
+ instance.unuse(extension);
expect(previewEl).toBeHidden();
expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
false,
);
- instance.cleanup();
+ instance.unuse(extension);
expect(previewEl).toBeHidden();
expect(parentEl.classList.contains(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS)).toBe(
false,
@@ -224,12 +167,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(instance.markdownPreview.shown).toBe(true);
- instance.cleanup();
+ instance.unuse(extension);
const { width: newWidth } = instance.getLayoutInfo();
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
- instance.cleanup();
+ instance.unuse(extension);
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
});
});
@@ -307,6 +250,12 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(200, { body: responseData });
});
+ it('toggles the condition to toggle preview/hide actions in the context menu', () => {
+ expect(instance.markdownPreview.actionShowPreviewCondition.get()).toBe(true);
+ instance.togglePreview();
+ expect(instance.markdownPreview.actionShowPreviewCondition.get()).toBe(false);
+ });
+
it('toggles preview flag on instance', () => {
expect(instance.markdownPreview.shown).toBe(false);
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index a605edc4357..ab625a744f5 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -248,13 +248,21 @@ describe('issue_comment_form component', () => {
describe('textarea', () => {
describe('general', () => {
- it('should render textarea with placeholder', () => {
- mountComponent({ mountFunction: mount });
+ it.each`
+ noteType | confidential | placeholder
+ ${'comment'} | ${false} | ${'Write a comment or drag your files here…'}
+ ${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
+ `(
+ 'should render textarea with placeholder for $noteType',
+ ({ confidential, placeholder }) => {
+ mountComponent({
+ mountFunction: mount,
+ noteableData: createNotableDataMock({ confidential }),
+ });
- expect(findTextArea().attributes('placeholder')).toBe(
- 'Write a comment or drag your files here…',
- );
- });
+ expect(findTextArea().attributes('placeholder')).toBe(placeholder);
+ },
+ );
it('should make textarea disabled while requesting', async () => {
mountComponent({ mountFunction: mount });
@@ -380,6 +388,20 @@ describe('issue_comment_form component', () => {
expect(findCloseReopenButton().text()).toBe('Close issue');
});
+ it.each`
+ confidential | buttonText
+ ${false} | ${'Comment'}
+ ${true} | ${'Add internal note'}
+ `('renders comment button with text "$buttonText"', ({ confidential, buttonText }) => {
+ mountComponent({
+ mountFunction: mount,
+ noteableData: createNotableDataMock({ confidential }),
+ initialData: { noteIsConfidential: confidential },
+ });
+
+ expect(findCommentButton().text()).toBe(buttonText);
+ });
+
it('should render comment button as disabled', () => {
mountComponent();
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
index 8ac6144e5c8..cabf551deba 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -28,18 +28,42 @@ describe('CommentTypeDropdown component', () => {
wrapper.destroy();
});
- it('Should label action button "Comment" and correct dropdown item checked when selected', () => {
+ it.each`
+ isInternalNote | buttonText
+ ${false} | ${COMMENT_FORM.comment}
+ ${true} | ${COMMENT_FORM.internalComment}
+ `(
+ 'Should label action button as "$buttonText" for comment when `isInternalNote` is $isInternalNote',
+ ({ isInternalNote, buttonText }) => {
+ mountComponent({ props: { noteType: constants.COMMENT, isInternalNote } });
+
+ expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ },
+ );
+
+ it('Should set correct dropdown item checked when comment is selected', () => {
mountComponent({ props: { noteType: constants.COMMENT } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: COMMENT_FORM.comment });
expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: true });
expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: false });
});
- it('Should label action button "Start Thread" and correct dropdown item option checked when selected', () => {
+ it.each`
+ isInternalNote | buttonText
+ ${false} | ${COMMENT_FORM.startThread}
+ ${true} | ${COMMENT_FORM.startInternalThread}
+ `(
+ 'Should label action button as "$buttonText" for discussion when `isInternalNote` is $isInternalNote',
+ ({ isInternalNote, buttonText }) => {
+ mountComponent({ props: { noteType: constants.DISCUSSION, isInternalNote } });
+
+ expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ },
+ );
+
+ it('Should set correct dropdown item option checked when discussion is selected', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: COMMENT_FORM.startThread });
expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: false });
expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: true });
});
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 63f3cd865d5..378dcb97fab 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -1,9 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { suggestionCommitMessage } from '~/diffs/store/getters';
-import noteBody from '~/notes/components/note_body.vue';
+import NoteBody from '~/notes/components/note_body.vue';
+import NoteAwardsList from '~/notes/components/note_awards_list.vue';
+import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
@@ -11,68 +12,89 @@ import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
+const createComponent = ({
+ props = {},
+ noteableData = noteableDataMock,
+ notesData = notesDataMock,
+ store = null,
+} = {}) => {
+ let mockStore;
+
+ if (!store) {
+ mockStore = createStore();
+
+ mockStore.dispatch('setNoteableData', noteableData);
+ mockStore.dispatch('setNotesData', notesData);
+ }
+
+ return shallowMount(NoteBody, {
+ store: mockStore || store,
+ propsData: {
+ note,
+ canEdit: true,
+ canAwardEmoji: true,
+ isEditing: false,
+ ...props,
+ },
+ });
+};
+
describe('issue_note_body component', () => {
- let store;
- let vm;
+ let wrapper;
beforeEach(() => {
- const Component = Vue.extend(noteBody);
-
- store = createStore();
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
-
- vm = new Component({
- store,
- propsData: {
- note,
- canEdit: true,
- canAwardEmoji: true,
- },
- }).$mount();
+ wrapper = createComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('should render the note', () => {
- expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
+ expect(wrapper.find('.note-text').html()).toContain(note.note_html);
});
it('should render awards list', () => {
- expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).not.toBeNull();
- expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).not.toBeNull();
+ expect(wrapper.findComponent(NoteAwardsList).exists()).toBe(true);
});
describe('isEditing', () => {
- beforeEach(async () => {
- vm.isEditing = true;
- await nextTick();
+ beforeEach(() => {
+ wrapper = createComponent({ props: { isEditing: true } });
});
it('renders edit form', () => {
- expect(vm.$el.querySelector('textarea.js-task-list-field')).not.toBeNull();
+ expect(wrapper.findComponent(NoteForm).exists()).toBe(true);
+ });
+
+ it.each`
+ confidential | buttonText
+ ${false} | ${'Save comment'}
+ ${true} | ${'Save internal note'}
+ `('renders save button with text "$buttonText"', ({ confidential, buttonText }) => {
+ wrapper = createComponent({ props: { note: { ...note, confidential }, isEditing: true } });
+
+ expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
});
it('adds autosave', () => {
const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`;
- expect(vm.autosave.key).toEqual(autosaveKey);
+ // While we discourage testing wrapper props
+ // here we aren't testing a component prop
+ // but instead an instance object property
+ // which is defined in `app/assets/javascripts/notes/mixins/autosave.js`
+ expect(wrapper.vm.autosave.key).toEqual(autosaveKey);
});
});
describe('commitMessage', () => {
- let wrapper;
-
- Vue.use(Vuex);
-
beforeEach(() => {
const notesStore = notes();
notesStore.state.notes = {};
- store = new Vuex.Store({
+ const store = new Vuex.Store({
modules: {
notes: notesStore,
diffs: {
@@ -98,9 +120,9 @@ describe('issue_note_body component', () => {
},
});
- wrapper = shallowMount(noteBody, {
+ wrapper = createComponent({
store,
- propsData: {
+ props: {
note: { ...note, suggestions: [12345] },
canEdit: true,
file: { file_path: 'abc' },
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index aa67a270658..252c24d1117 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -6,7 +6,7 @@ import { getDraft, updateDraft } from '~/lib/utils/autosave';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data';
+import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
@@ -114,6 +114,23 @@ describe('issue_note_form component', () => {
expect(textarea.attributes('data-supports-quick-actions')).toBe('true');
});
+ it.each`
+ confidential | placeholder
+ ${false} | ${'Write a comment or drag your files here…'}
+ ${true} | ${'Write an internal note or drag your files here…'}
+ `(
+ 'should set correct textarea placeholder text when discussion confidentiality is $confidential',
+ ({ confidential, placeholder }) => {
+ props.note = {
+ ...note,
+ confidential,
+ };
+ wrapper = createComponentWrapper();
+
+ expect(wrapper.find('textarea').attributes('placeholder')).toBe(placeholder);
+ },
+ );
+
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
const markdownField = wrapper.find(MarkdownField);
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 3513b562e0a..310a470aa18 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -21,7 +21,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
- const findConfidentialIndicator = () => wrapper.findByTestId('confidentialIndicator');
+ const findConfidentialIndicator = () => wrapper.findByTestId('internalNoteIndicator');
const findSpinner = () => wrapper.find({ ref: 'spinner' });
const findAuthorStatus = () => wrapper.find({ ref: 'authorStatus' });
@@ -297,7 +297,7 @@ describe('NoteHeader component', () => {
createComponent({ isConfidential: true, noteableType: 'issue' });
expect(findConfidentialIndicator().attributes('title')).toBe(
- 'This comment is confidential and only visible to project members',
+ 'This internal note will always remain confidential',
);
});
});
diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb
index 7ea0f324dfa..f9e18a65af4 100644
--- a/spec/lib/gitlab/application_context_spec.rb
+++ b/spec/lib/gitlab/application_context_spec.rb
@@ -91,7 +91,6 @@ RSpec.describe Gitlab::ApplicationContext do
let_it_be(:project) { create(:project) }
let_it_be(:namespace) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: namespace) }
- let_it_be(:artifact) { create(:ci_job_artifact, size: 42) }
def result(context)
context.to_lazy_hash.transform_values { |v| v.respond_to?(:call) ? v.call : v }
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb
new file mode 100644
index 00000000000..81bce989833
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'support/helpers/stubbed_feature'
+require 'support/helpers/stub_feature_flags'
+
+RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If do
+ include StubFeatureFlags
+
+ subject(:if_clause) { described_class.new(expression) }
+
+ describe '#satisfied_by?' do
+ let(:context_class) { Gitlab::Ci::Build::Context::Base }
+ let(:rules_context) { instance_double(context_class, variables_hash: {}) }
+
+ subject(:satisfied_by?) { if_clause.satisfied_by?(nil, rules_context) }
+
+ context 'when expression is a basic string comparison' do
+ context 'when comparison is true' do
+ let(:expression) { '"value" == "value"' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when comparison is false' do
+ let(:expression) { '"value" == "other"' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when expression is a regexp' do
+ context 'when comparison is true' do
+ let(:expression) { '"abcde" =~ /^ab.*/' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when comparison is false' do
+ let(:expression) { '"abcde" =~ /^af.*/' }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when both side of the expression are variables' do
+ let(:expression) { '$teststring =~ $pattern' }
+
+ context 'when comparison is true' do
+ let(:rules_context) do
+ instance_double(context_class, variables_hash: { 'teststring' => 'abcde', 'pattern' => '/^ab.*/' })
+ end
+
+ it { is_expected.to eq(true) }
+
+ context 'when the FF ci_fix_rules_if_comparison_with_regexp_variable is disabled' do
+ before do
+ stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when comparison is false' do
+ let(:rules_context) do
+ instance_double(context_class, variables_hash: { 'teststring' => 'abcde', 'pattern' => '/^af.*/' })
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index 37bfdca4d1d..e82dcd0254d 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -188,6 +188,19 @@ RSpec.describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, { MY_VAR: 'my var' })) }
end
end
+
+ context 'with a regexp variable matching rule' do
+ let(:rule_list) { [{ if: '"abcde" =~ $pattern' }] }
+
+ before do
+ allow(ci_build).to receive(:scoped_variables).and_return(
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'pattern', value: '/^ab.*/', public: true)
+ )
+ end
+
+ it { is_expected.to eq(described_class::Result.new('on_success')) }
+ end
end
describe 'Gitlab::Ci::Build::Rules::Result' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
index 0da04d8dcf7..83742699d3d 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -1,9 +1,13 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'support/helpers/stubbed_feature'
+require 'support/helpers/stub_feature_flags'
require_dependency 're2'
RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
+ include StubFeatureFlags
+
let(:left) { double('left') }
let(:right) { double('right') }
@@ -148,5 +152,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
it { is_expected.to eq(false) }
end
+
+ context 'when right value is a regexp string' do
+ let(:right_value) { '/^ab.*/' }
+
+ context 'when matching' do
+ let(:left_value) { 'abcde' }
+
+ it { is_expected.to eq(true) }
+
+ context 'when the FF ci_fix_rules_if_comparison_with_regexp_variable is disabled' do
+ before do
+ stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when not matching' do
+ let(:left_value) { 'dfg' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
index 9bff2355d58..aad33106647 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -1,9 +1,13 @@
# frozen_string_literal: true
require 'fast_spec_helper'
+require 'support/helpers/stubbed_feature'
+require 'support/helpers/stub_feature_flags'
require_dependency 're2'
RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
+ include StubFeatureFlags
+
let(:left) { double('left') }
let(:right) { double('right') }
@@ -148,5 +152,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
it { is_expected.to eq(true) }
end
+
+ context 'when right value is a regexp string' do
+ let(:right_value) { '/^ab.*/' }
+
+ context 'when matching' do
+ let(:left_value) { 'abcde' }
+
+ it { is_expected.to eq(false) }
+
+ context 'when the FF ci_fix_rules_if_comparison_with_regexp_variable is disabled' do
+ before do
+ stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: false)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ context 'when not matching' do
+ let(:left_value) { 'dfg' }
+
+ it { is_expected.to eq(true) }
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
index fa4f8a20984..be205395b69 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
@@ -1,8 +1,32 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
+ describe '#initialize' do
+ context 'when the value is a valid regular expression' do
+ it 'initializes the pattern' do
+ pattern = described_class.new('/foo/')
+
+ expect(pattern.value).to eq('/foo/')
+ end
+ end
+
+ context 'when the value is a valid regular expression with escaped slashes' do
+ it 'initializes the pattern' do
+ pattern = described_class.new('/foo\\/bar/')
+
+ expect(pattern.value).to eq('/foo/bar/')
+ end
+ end
+
+ context 'when the value is not a valid regular expression' do
+ it 'raises an error' do
+ expect { described_class.new('foo') }.to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError)
+ end
+ end
+ end
+
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('/.*/'))
@@ -15,6 +39,29 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
end
end
+ describe '.build_and_evaluate' do
+ context 'when the value is a valid regular expression' do
+ it 'returns the value as a Gitlab::UntrustedRegexp' do
+ expect(described_class.build_and_evaluate('/foo/'))
+ .to eq(Gitlab::UntrustedRegexp.new('foo'))
+ end
+ end
+
+ context 'when the value is a Gitlab::UntrustedRegexp' do
+ it 'returns the value itself' do
+ expect(described_class.build_and_evaluate(Gitlab::UntrustedRegexp.new('foo')))
+ .to eq(Gitlab::UntrustedRegexp.new('foo'))
+ end
+ end
+
+ context 'when the value is not a valid regular expression' do
+ it 'returns the value itself' do
+ expect(described_class.build_and_evaluate('foo'))
+ .to eq('foo')
+ end
+ end
+ end
+
describe '.type' do
it 'is a value lexeme' do
expect(described_class.type).to eq :value
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 84713e2a798..bbd11a00149 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
.to_hash
end
- subject do
+ subject(:statement) do
described_class.new(text, variables)
end
@@ -29,6 +29,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
describe '#evaluate' do
using RSpec::Parameterized::TableSyntax
+ subject(:evaluate) { statement.evaluate }
+
where(:expression, :value) do
'$PRESENT_VARIABLE == "my variable"' | true
'"my variable" == $PRESENT_VARIABLE' | true
@@ -125,7 +127,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:text) { expression }
it "evaluates to `#{params[:value].inspect}`" do
- expect(subject.evaluate).to eq(value)
+ expect(evaluate).to eq(value)
end
end
end
@@ -133,6 +135,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
describe '#truthful?' do
using RSpec::Parameterized::TableSyntax
+ subject(:truthful?) { statement.truthful? }
+
where(:expression, :value) do
'$PRESENT_VARIABLE == "my variable"' | true
"$PRESENT_VARIABLE == 'no match'" | false
@@ -151,7 +155,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:text) { expression }
it "returns `#{params[:value].inspect}`" do
- expect(subject.truthful?).to eq value
+ expect(truthful?).to eq value
end
end
@@ -159,10 +163,41 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:text) { '$PRESENT_VARIABLE' }
it 'returns false' do
- allow(subject).to receive(:evaluate)
+ allow(statement).to receive(:evaluate)
.and_raise(described_class::StatementError)
- expect(subject.truthful?).to be_falsey
+ expect(truthful?).to be_falsey
+ end
+ end
+
+ context 'when variables have patterns' do
+ let(:variables) do
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'teststring', value: 'abcde')
+ .append(key: 'pattern1', value: '/^ab.*/')
+ .append(key: 'pattern2', value: '/^at.*/')
+ .to_hash
+ end
+
+ where(:expression, :ff, :result) do
+ '$teststring =~ "abcde"' | true | true
+ '$teststring =~ "abcde"' | false | true
+ '$teststring =~ $teststring' | true | true
+ '$teststring =~ $teststring' | false | true
+ '$teststring =~ $pattern1' | true | true
+ '$teststring =~ $pattern1' | false | false
+ '$teststring =~ $pattern2' | true | false
+ '$teststring =~ $pattern2' | false | false
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ before do
+ stub_feature_flags(ci_fix_rules_if_comparison_with_regexp_variable: ff)
+ end
+
+ it { is_expected.to eq(result) }
end
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index ac8616f84a7..23f4f0e7089 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -70,40 +70,6 @@ RSpec.describe Gitlab::Database do
end
end
- describe '.main_database?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:database_name, :result) do
- :main | true
- 'main' | true
- :ci | false
- 'ci' | false
- :archive | false
- 'archive' | false
- end
-
- with_them do
- it { expect(described_class.main_database?(database_name)).to eq(result) }
- end
- end
-
- describe '.ci_database?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:database_name, :result) do
- :main | false
- 'main' | false
- :ci | true
- 'ci' | true
- :archive | false
- 'archive' | false
- end
-
- with_them do
- it { expect(described_class.ci_database?(database_name)).to eq(result) }
- end
- end
-
describe '.check_for_non_superuser' do
subject { described_class.check_for_non_superuser }
diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb
index da135f202f6..c74bd933b22 100644
--- a/spec/lib/gitlab/sidekiq_config_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config_spec.rb
@@ -3,6 +3,11 @@
require 'spec_helper'
RSpec.describe Gitlab::SidekiqConfig do
+ before do
+ # Remove cache
+ described_class.instance_variable_set(:@workers, nil)
+ end
+
describe '.workers' do
it 'includes all workers' do
worker_classes = described_class.workers.map(&:klass)
@@ -161,4 +166,35 @@ RSpec.describe Gitlab::SidekiqConfig do
expect(mappings).not_to include('AdminEmailWorker' => 'cronjob:admin_email')
end
end
+
+ describe '.routing_queues' do
+ let(:test_routes) do
+ [
+ ['tags=needs_own_queue', nil],
+ ['urgency=high', 'high_urgency'],
+ ['feature_category=gitaly', 'gitaly'],
+ ['feature_category=not_exist', 'not_exist'],
+ ['*', 'default']
+ ]
+ end
+
+ before do
+ described_class.instance_variable_set(:@routing_queues, nil)
+ allow(::Gitlab::SidekiqConfig::WorkerRouter)
+ .to receive(:global).and_return(::Gitlab::SidekiqConfig::WorkerRouter.new(test_routes))
+ end
+
+ after do
+ described_class.instance_variable_set(:@routing_queues, nil)
+ end
+
+ it 'returns worker queue mappings that have queues in the current Sidekiq options' do
+ queues = described_class.routing_queues
+
+ expect(queues).to match_array(%w[
+ default mailers high_urgency gitaly email_receiver service_desk_email_receiver
+ ])
+ expect(queues).not_to include('not_exist')
+ end
+ end
end
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index dd9e2c32925..8fabd37c854 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -577,14 +577,21 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when artifact_type is archive' do
context 'when artifact_format is zip' do
+ subject(:request) { upload_artifacts(file_upload, headers_with_token, params) }
+
let(:params) { { artifact_type: :archive, artifact_format: :zip } }
+ let(:expected_params) { { artifact_size: job.reload.artifacts_size } }
+ let(:subject_proc) { proc { subject } }
it 'stores junit test report' do
- upload_artifacts(file_upload, headers_with_token, params)
+ subject
expect(response).to have_gitlab_http_status(:created)
expect(job.reload.job_artifacts_archive).not_to be_nil
end
+
+ it_behaves_like 'storing arguments in the application context'
+ it_behaves_like 'not executing any extra queries for the application context'
end
context 'when artifact_format is gzip' do
diff --git a/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb b/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb
new file mode 100644
index 00000000000..86f8992a624
--- /dev/null
+++ b/spec/requests/api/integrations/jira_connect/subscriptions_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Integrations::JiraConnect::Subscriptions do
+ describe 'POST /integrations/jira_connect/subscriptions' do
+ subject(:post_subscriptions) { post api('/integrations/jira_connect/subscriptions') }
+
+ it 'returns 401' do
+ post_subscriptions
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'with user token' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ subject(:post_subscriptions) do
+ post api('/integrations/jira_connect/subscriptions', user), params: { jwt: jwt, namespace_path: group.path }
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(jira_connect_oauth: false)
+ end
+
+ let(:jwt) { '123' }
+
+ it 'returns 404' do
+ post_subscriptions
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with invalid JWT' do
+ let(:jwt) { '123' }
+
+ it 'returns 401' do
+ post_subscriptions
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with valid JWT' do
+ let_it_be(:installation) { create(:jira_connect_installation) }
+ let_it_be(:user) { create(:user) }
+
+ let(:claims) { { iss: installation.client_key, qsh: 'context-qsh', sub: 1234 } }
+ let(:jwt) { Atlassian::Jwt.encode(claims, installation.shared_secret) }
+ let(:jira_user) { { 'groups' => { 'items' => [{ 'name' => jira_group_name }] } } }
+ let(:jira_group_name) { 'site-admins' }
+
+ before do
+ WebMock
+ .stub_request(:get, "#{installation.base_url}/rest/api/3/user?accountId=1234&expand=groups")
+ .to_return(body: jira_user.to_json, status: 200, headers: { 'Content-Type' => 'application/json' })
+ end
+
+ it 'returns 401 if the user does not have access to the group' do
+ post_subscriptions
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'user has access to the group' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it 'creates a subscription' do
+ expect { post_subscriptions }.to change { installation.subscriptions.count }.from(0).to(1)
+ end
+
+ it 'returns 201' do
+ post_subscriptions
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 23ac2ea5c0b..302d824e650 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -10,7 +10,18 @@ RSpec.describe API::SidekiqMetrics do
get api('/sidekiq/queue_metrics', admin)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a Hash
+ expect(json_response).to match a_hash_including(
+ 'queues' => a_hash_including(
+ 'default' => {
+ 'backlog' => be_a(Integer),
+ 'latency' => be_a(Integer)
+ },
+ 'mailers' => {
+ 'backlog' => be_a(Integer),
+ 'latency' => be_a(Integer)
+ }
+ )
+ )
end
it 'defines the `process_metrics` endpoint' do
diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb
index b8487e438a9..01f240805f5 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -42,6 +42,13 @@ RSpec.describe Ci::JobArtifacts::CreateService do
subject { service.execute(artifacts_file, params, metadata_file: metadata_file) }
context 'when artifacts file is uploaded' do
+ it 'returns artifact in the response' do
+ response = subject
+ new_artifact = job.job_artifacts.last
+
+ expect(response[:artifact]).to eq(new_artifact)
+ end
+
it 'saves artifact for the given type' do
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
@@ -84,7 +91,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
it 'sets expiration date according to application settings' do
expected_expire_at = 1.day.from_now
- expect(subject).to match(a_hash_including(status: :success))
+ expect(subject).to match(a_hash_including(status: :success, artifact: anything))
archive_artifact, metadata_artifact = job.job_artifacts.last(2)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
@@ -100,7 +107,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
it 'sets expiration date according to the parameter' do
expected_expire_at = 2.hours.from_now
- expect(subject).to match(a_hash_including(status: :success))
+ expect(subject).to match(a_hash_including(status: :success, artifact: anything))
archive_artifact, metadata_artifact = job.job_artifacts.last(2)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb
index e63adc07313..6aaf5f45069 100644
--- a/spec/services/groups/group_links/destroy_service_spec.rb
+++ b/spec/services/groups/group_links/destroy_service_spec.rb
@@ -3,54 +3,77 @@
require 'spec_helper'
RSpec.describe Groups::GroupLinks::DestroyService, '#execute' do
- let(:user) { create(:user) }
-
+ let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: shared_group) }
let_it_be(:owner) { create(:user) }
- before do
- group.add_developer(owner)
- shared_group.add_owner(owner)
- end
-
subject { described_class.new(shared_group, owner) }
- context 'single link' do
- let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
+ context 'when authorizing by user' do
+ before do
+ group.add_developer(owner)
+ shared_group.add_owner(owner)
+ end
+
+ context 'single link' do
+ let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
- it 'destroys link' do
- expect { subject.execute(link) }.to change { shared_group.shared_with_group_links.count }.from(1).to(0)
+ it 'destroys the link' do
+ expect { subject.execute(link) }.to change { shared_group.shared_with_group_links.count }.from(1).to(0)
+ end
+
+ it 'revokes project authorization', :sidekiq_inline do
+ group.add_developer(user)
+
+ expect { subject.execute(link) }.to(
+ change { Ability.allowed?(user, :read_project, project) }.from(true).to(false))
+ end
end
- it 'revokes project authorization', :sidekiq_inline do
- group.add_developer(user)
+ context 'multiple links' do
+ let_it_be(:another_group) { create(:group, :private) }
+ let_it_be(:another_shared_group) { create(:group, :private) }
+
+ let!(:links) do
+ [
+ create(:group_group_link, shared_group: shared_group, shared_with_group: group),
+ create(:group_group_link, shared_group: shared_group, shared_with_group: another_group),
+ create(:group_group_link, shared_group: another_shared_group, shared_with_group: group),
+ create(:group_group_link, shared_group: another_shared_group, shared_with_group: another_group)
+ ]
+ end
- expect { subject.execute(link) }.to(
- change { Ability.allowed?(user, :read_project, project) }.from(true).to(false))
+ it 'updates project authorization once per group' do
+ expect(GroupGroupLink).to receive(:delete).and_call_original
+ expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
+ expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
+
+ subject.execute(links)
+ end
end
end
- context 'multiple links' do
- let_it_be(:another_group) { create(:group, :private) }
- let_it_be(:another_shared_group) { create(:group, :private) }
-
- let!(:links) do
- [
- create(:group_group_link, shared_group: shared_group, shared_with_group: group),
- create(:group_group_link, shared_group: shared_group, shared_with_group: another_group),
- create(:group_group_link, shared_group: another_shared_group, shared_with_group: group),
- create(:group_group_link, shared_group: another_shared_group, shared_with_group: another_group)
- ]
+ context 'when skipping authorization' do
+ let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
+
+ context 'with provided group and owner' do
+ it 'destroys the link' do
+ expect do
+ subject.execute(link, skip_authorization: true)
+ end.to change { shared_group.shared_with_group_links.count }.from(1).to(0)
+ end
end
- it 'updates project authorization once per group' do
- expect(GroupGroupLink).to receive(:delete).and_call_original
- expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
- expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once
+ context 'without providing group or owner' do
+ subject { described_class.new(nil, nil) }
- subject.execute(links)
+ it 'destroys the link' do
+ expect do
+ subject.execute(link, skip_authorization: true)
+ end.to change { shared_group.shared_with_group_links.count }.from(1).to(0)
+ end
end
end
end