From c46e0d0c271a21b67a3412faf750d27dd63432bb Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 26 Jun 2023 15:07:59 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .dockerignore | 1 - .gitlab/ci/gitlab-gems.gitlab-ci.yml | 8 + .../ci/package-and-test-nightly/main.gitlab-ci.yml | 11 + .gitlab/ci/package-and-test/main.gitlab-ci.yml | 33 +- .gitlab/ci/preflight.gitlab-ci.yml | 3 - .gitlab/ci/rules.gitlab-ci.yml | 5 + .rubocop_todo/gitlab/doc_url.yml | 1 - .rubocop_todo/gitlab/namespaced_class.yml | 1 - .rubocop_todo/layout/argument_alignment.yml | 1 - .rubocop_todo/layout/line_length.yml | 1 - .rubocop_todo/lint/ambiguous_regexp_literal.yml | 1 - .rubocop_todo/lint/symbol_conversion.yml | 1 - .rubocop_todo/rspec/context_wording.yml | 2 - .rubocop_todo/rspec/missing_feature_category.yml | 1 - .rubocop_todo/style/percent_literal_delimiters.yml | 1 - .rubocop_todo/style/string_concatenation.yml | 1 - Gemfile | 5 + Gemfile.lock | 11 + .../javascripts/behaviors/markdown/render_gfm.js | 4 +- .../behaviors/markdown/render_sandboxed_mermaid.js | 11 +- .../javascripts/issuable/components/status_box.vue | 41 +- .../issuable/popover/components/issue_popover.vue | 8 +- .../issuable/popover/components/mr_popover.vue | 8 +- app/assets/javascripts/issuable/popover/index.js | 20 +- app/assets/javascripts/lib/mermaid.js | 7 + .../lib/utils/datetime/date_format_utility.js | 32 ++ app/assets/javascripts/tracking/constants.js | 4 + app/assets/javascripts/tracking/index.js | 2 + app/assets/javascripts/tracking/internal_events.js | 36 ++ app/controllers/concerns/uploads_actions.rb | 17 +- .../concerns/project_features_compatibility.rb | 2 +- .../sast_ui_schema.json | 38 +- bin/audit-event-type | 3 +- config/application.rb | 3 +- config/environments/test.rb | 2 +- db/docs/subscription_user_add_on_assignments.yml | 10 + ..._create_subscription_user_add_on_assignments.rb | 17 + ...e_id_on_subscription_user_add_on_assignments.rb | 16 + ...r_id_on_subscription_user_add_on_assignments.rb | 15 + db/schema_migrations/20230616164309 | 1 + db/schema_migrations/20230616164705 | 1 + db/schema_migrations/20230616164731 | 1 + db/structure.sql | 32 ++ doc/administration/instance_limits.md | 157 ++----- doc/ci/runners/saas/macos_saas_runner.md | 21 +- doc/development/gems.md | 2 +- doc/development/pipelines/index.md | 8 +- .../elasticsearch_troubleshooting.md | 18 + doc/user/permissions.md | 2 +- gems/gitlab-rspec/.gitlab-ci.yml | 2 +- gems/gitlab-rspec/gitlab-rspec.gemspec | 2 +- gems/gitlab-utils/.gitignore | 11 + gems/gitlab-utils/.gitlab-ci.yml | 30 ++ gems/gitlab-utils/.rspec | 3 + gems/gitlab-utils/.rubocop.yml | 31 ++ gems/gitlab-utils/Gemfile | 10 + gems/gitlab-utils/Gemfile.lock | 193 +++++++++ gems/gitlab-utils/README.md | 8 + gems/gitlab-utils/Rakefile | 12 + gems/gitlab-utils/gitlab-utils.gemspec | 34 ++ gems/gitlab-utils/lib/gitlab/utils.rb | 263 +++++++++++ gems/gitlab-utils/lib/gitlab/utils/all.rb | 6 + .../lib/gitlab/utils/strong_memoize.rb | 147 +++++++ gems/gitlab-utils/lib/gitlab/utils/version.rb | 9 + gems/gitlab-utils/lib/gitlab/version_info.rb | 98 +++++ .../spec/gitlab/utils/strong_memoize_spec.rb | 372 ++++++++++++++++ gems/gitlab-utils/spec/gitlab/utils_spec.rb | 479 +++++++++++++++++++++ gems/gitlab-utils/spec/gitlab/version_info_spec.rb | 193 +++++++++ gems/gitlab-utils/spec/spec_helper.rb | 23 + lib/gitlab/cluster/lifecycle_events.rb | 2 +- lib/gitlab/sidekiq_logging/structured_logger.rb | 1 + lib/gitlab/sidekiq_middleware/defer_jobs.rb | 2 + lib/gitlab/task_helpers.rb | 2 +- lib/gitlab/utils.rb | 259 ----------- lib/gitlab/utils/override.rb | 2 +- lib/gitlab/utils/strong_memoize.rb | 147 ------- lib/gitlab/version_info.rb | 98 ----- lib/tasks/gettext.rake | 2 +- locale/gitlab.pot | 18 +- metrics_server/dependencies.rb | 4 +- qa/Dockerfile | 4 +- qa/Gemfile | 1 + qa/Gemfile.lock | 29 +- qa/gdk/Dockerfile.gdk | 4 +- qa/qa.rb | 3 +- qa/qa/scenario/test/integration/oauth.rb | 13 + scripts/gitaly-test-build | 1 + scripts/gitaly-test-spawn | 1 + scripts/merge-simplecov | 1 + scripts/setup-test-env | 2 +- sidekiq_cluster/cli.rb | 2 +- spec/controllers/groups/uploads_controller_spec.rb | 20 +- spec/deprecation_warnings.rb | 2 +- spec/fast_spec_helper.rb | 3 +- spec/features/markdown/sandboxed_mermaid_spec.rb | 5 +- .../markdown/render_sandboxed_mermaid_spec.js | 2 +- .../issuable/components/status_box_spec.js | 2 + .../popover/components/issue_popover_spec.js | 2 +- .../issuable/popover/components/mr_popover_spec.js | 2 +- spec/frontend/issuable/popover/index_spec.js | 68 ++- .../lib/utils/datetime/date_format_utility_spec.js | 15 + .../sidekiq_logging/structured_logger_spec.rb | 1 + .../gitlab/sidekiq_middleware/defer_jobs_spec.rb | 9 +- spec/lib/gitlab/utils/strong_memoize_spec.rb | 374 ---------------- spec/lib/gitlab/utils_spec.rb | 477 -------------------- spec/lib/gitlab/version_info_spec.rb | 193 --------- spec/simplecov_env.rb | 2 +- spec/support/ability_check.rb | 2 +- spec/support/formatters/json_formatter.rb | 10 +- spec/support/helpers/gitaly_setup.rb | 3 +- spec/support/rspec.rb | 1 + .../structured_logger_shared_context.rb | 3 +- .../controllers/uploads_actions_shared_examples.rb | 8 - 113 files changed, 2480 insertions(+), 1875 deletions(-) create mode 100644 app/assets/javascripts/tracking/internal_events.js create mode 100644 db/docs/subscription_user_add_on_assignments.yml create mode 100644 db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb create mode 100644 db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb create mode 100644 db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb create mode 100644 db/schema_migrations/20230616164309 create mode 100644 db/schema_migrations/20230616164705 create mode 100644 db/schema_migrations/20230616164731 create mode 100644 gems/gitlab-utils/.gitignore create mode 100644 gems/gitlab-utils/.gitlab-ci.yml create mode 100644 gems/gitlab-utils/.rspec create mode 100644 gems/gitlab-utils/.rubocop.yml create mode 100644 gems/gitlab-utils/Gemfile create mode 100644 gems/gitlab-utils/Gemfile.lock create mode 100644 gems/gitlab-utils/README.md create mode 100644 gems/gitlab-utils/Rakefile create mode 100644 gems/gitlab-utils/gitlab-utils.gemspec create mode 100644 gems/gitlab-utils/lib/gitlab/utils.rb create mode 100644 gems/gitlab-utils/lib/gitlab/utils/all.rb create mode 100644 gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb create mode 100644 gems/gitlab-utils/lib/gitlab/utils/version.rb create mode 100644 gems/gitlab-utils/lib/gitlab/version_info.rb create mode 100644 gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb create mode 100644 gems/gitlab-utils/spec/gitlab/utils_spec.rb create mode 100644 gems/gitlab-utils/spec/gitlab/version_info_spec.rb create mode 100644 gems/gitlab-utils/spec/spec_helper.rb delete mode 100644 lib/gitlab/utils.rb delete mode 100644 lib/gitlab/utils/strong_memoize.rb delete mode 100644 lib/gitlab/version_info.rb create mode 100644 qa/qa/scenario/test/integration/oauth.rb delete mode 100644 spec/lib/gitlab/utils/strong_memoize_spec.rb delete mode 100644 spec/lib/gitlab/utils_spec.rb delete mode 100644 spec/lib/gitlab/version_info_spec.rb diff --git a/.dockerignore b/.dockerignore index 0782627230a..a2d54fa65dd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -80,4 +80,3 @@ /spec/ /symbol/ /tmp/ -/vendor/ diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml index adab179d2a2..aff272935c5 100644 --- a/.gitlab/ci/gitlab-gems.gitlab-ci.yml +++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml @@ -5,3 +5,11 @@ gems gitlab-rspec: trigger: include: gems/gitlab-rspec/.gitlab-ci.yml strategy: depend + +gems gitlab-utils: + extends: + - .gems:rules:gitlab-utils + needs: [] + trigger: + include: gems/gitlab-utils/.gitlab-ci.yml + strategy: depend diff --git a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml index 34b4e853415..d1062250040 100644 --- a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml @@ -97,6 +97,17 @@ integration-opensearch-compatibility-version-2: OPENSEARCH_VERSION: "2.0.1" QA_SCENARIO: "Test::Integration::Opensearch" +# ------------------------------------------ +# Relative url +# ------------------------------------------ +relative-url: + extends: + - .qa + - .ee + - .parallel + variables: + QA_SCENARIO: Test::Instance::RelativeUrl + # ========================================== # Post test stage # ========================================== diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index fcea49e3103..8ed1bb5d067 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -81,6 +81,7 @@ instance-ff-inverse: # ------------------------------------------ # ========== instance =========== + instance: extends: - .parallel @@ -151,38 +152,6 @@ praefect-selective-parallel: variables: QA_TESTS: "" -# ========== relative-url =========== - -relative-url: - extends: - - .qa - - .parallel - variables: - QA_SCENARIO: Test::Instance::RelativeUrl - rules: - - !reference [.rules:test:qa-parallel, rules] - - if: $QA_SUITES =~ /Test::Instance::All/ - -relative-url-selective: - extends: .qa - variables: - QA_SCENARIO: Test::Instance::RelativeUrl - rules: - - !reference [.rules:test:qa-selective, rules] - - if: $QA_SUITES =~ /Test::Instance::All/ - -relative-url-selective-parallel: - extends: - - .qa - - .parallel - variables: - QA_SCENARIO: Test::Instance::RelativeUrl - rules: - - !reference [.rules:test:qa-selective-parallel, rules] - - if: $QA_SUITES =~ /Test::Instance::All/ - variables: - QA_TESTS: "" - # ========== decomposition-single-db =========== decomposition-single-db: diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml index 968402b2ea5..6a2ea85f393 100644 --- a/.gitlab/ci/preflight.gitlab-ci.yml +++ b/.gitlab/ci/preflight.gitlab-ci.yml @@ -24,9 +24,6 @@ - .ruby-cache - .preflight:rules:rails-production-server-boot - .use-pg13 - variables: - BUNDLE_WITHOUT: "development:test" - BUNDLE_WITH: "production" # Test the puma configuration present in `config/puma.rb.example` rails-production-server-boot-puma-example: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 6acc2dea2e2..007e5cefb89 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -2135,6 +2135,11 @@ - <<: *if-merge-request changes: ["gems/gitlab-rspec/**/*"] +.gems:rules:gitlab-utils: + rules: + - <<: *if-merge-request + changes: ["gems/gitlab-utils/**/*"] + ####################### # Vendored gems rules # ####################### diff --git a/.rubocop_todo/gitlab/doc_url.yml b/.rubocop_todo/gitlab/doc_url.yml index 119de2296ce..13537a142a1 100644 --- a/.rubocop_todo/gitlab/doc_url.yml +++ b/.rubocop_todo/gitlab/doc_url.yml @@ -43,7 +43,6 @@ Gitlab/DocUrl: - 'lib/gitlab/pagination/keyset/unsupported_scope_order.rb' - 'lib/gitlab/redis/hll.rb' - 'lib/gitlab/slash_commands/presenters/help.rb' - - 'lib/gitlab/utils/strong_memoize.rb' - 'lib/initializer_connections.rb' - 'lib/security/ci_configuration/base_build_action.rb' - 'lib/tasks/db_obsolete_ignored_columns.rake' diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 8a0dd63ae7c..4ab42f01790 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -1248,7 +1248,6 @@ Gitlab/NamespacedClass: - 'lib/gitlab/user_access.rb' - 'lib/gitlab/user_access_snippet.rb' - 'lib/gitlab/uuid.rb' - - 'lib/gitlab/version_info.rb' - 'lib/gitlab/visibility_level_checker.rb' - 'lib/gitlab/wiki_file_finder.rb' - 'lib/gitlab/workhorse.rb' diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 826e20fb40f..e774c081ef4 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -1820,7 +1820,6 @@ Layout/ArgumentAlignment: - 'spec/lib/gitlab/usage_data_queries_spec.rb' - 'spec/lib/gitlab/usage_data_spec.rb' - 'spec/lib/gitlab/utils/lazy_attributes_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/lib/google_api/cloud_platform/client_spec.rb' - 'spec/lib/peek/views/detailed_view_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 1deedbfc96e..a5fafa59388 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -4230,7 +4230,6 @@ Layout/LineLength: - 'spec/lib/gitlab/utils/measuring_spec.rb' - 'spec/lib/gitlab/utils/nokogiri_spec.rb' - 'spec/lib/gitlab/utils/usage_data_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/gitlab/web_ide/config/entry/global_spec.rb' - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb' - 'spec/lib/gitlab/webpack/file_loader_spec.rb' diff --git a/.rubocop_todo/lint/ambiguous_regexp_literal.yml b/.rubocop_todo/lint/ambiguous_regexp_literal.yml index c3b06ede5be..4754e381780 100644 --- a/.rubocop_todo/lint/ambiguous_regexp_literal.yml +++ b/.rubocop_todo/lint/ambiguous_regexp_literal.yml @@ -66,7 +66,6 @@ Lint/AmbiguousRegexpLiteral: - 'spec/lib/gitlab/pagination/keyset/iterator_spec.rb' - 'spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb' - 'spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb' - - 'spec/lib/gitlab/utils/strong_memoize_spec.rb' - 'spec/lib/gitlab/web_ide/config/entry/global_spec.rb' - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb' - 'spec/lib/object_storage/direct_upload_spec.rb' diff --git a/.rubocop_todo/lint/symbol_conversion.yml b/.rubocop_todo/lint/symbol_conversion.yml index 147cd66f993..8799414c911 100644 --- a/.rubocop_todo/lint/symbol_conversion.yml +++ b/.rubocop_todo/lint/symbol_conversion.yml @@ -117,7 +117,6 @@ Lint/SymbolConversion: - 'spec/lib/gitlab/search/abuse_validators/no_abusive_coercion_from_string_validator_spec.rb' - 'spec/lib/gitlab/slug/path_spec.rb' - 'spec/lib/gitlab/tracking_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/google_api/cloud_platform/client_spec.rb' - 'spec/lib/service_ping/devops_report_spec.rb' - 'spec/models/integrations/prometheus_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index af708ad54e5..1ce003ecd2f 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -2062,9 +2062,7 @@ RSpec/ContextWording: - 'spec/lib/gitlab/usage_data_spec.rb' - 'spec/lib/gitlab/utils/lazy_attributes_spec.rb' - 'spec/lib/gitlab/utils/mime_type_spec.rb' - - 'spec/lib/gitlab/utils/strong_memoize_spec.rb' - 'spec/lib/gitlab/utils/usage_data_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/gitlab/view/presenter/base_spec.rb' - 'spec/lib/gitlab/visibility_level_checker_spec.rb' - 'spec/lib/gitlab/visibility_level_spec.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index e3829a97b02..3cf0ed5c38c 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -4413,7 +4413,6 @@ RSpec/MissingFeatureCategory: - 'spec/lib/gitlab/utils/safe_inline_hash_spec.rb' - 'spec/lib/gitlab/utils/sanitize_node_link_spec.rb' - 'spec/lib/gitlab/utils/usage_data_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/gitlab/uuid_spec.rb' - 'spec/lib/gitlab/verify/job_artifacts_spec.rb' - 'spec/lib/gitlab/verify/lfs_objects_spec.rb' diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml index 2e03bbf4557..bb29652fe96 100644 --- a/.rubocop_todo/style/percent_literal_delimiters.yml +++ b/.rubocop_todo/style/percent_literal_delimiters.yml @@ -843,7 +843,6 @@ Style/PercentLiteralDelimiters: - 'spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb' - 'spec/lib/gitlab/usage_data_spec.rb' - 'spec/lib/gitlab/utils/log_limited_array_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/gitlab/webpack/graphql_known_operations_spec.rb' - 'spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb' - 'spec/lib/object_storage/config_spec.rb' diff --git a/.rubocop_todo/style/string_concatenation.yml b/.rubocop_todo/style/string_concatenation.yml index 4e3b17fe4c0..34e4549a987 100644 --- a/.rubocop_todo/style/string_concatenation.yml +++ b/.rubocop_todo/style/string_concatenation.yml @@ -212,7 +212,6 @@ Style/StringConcatenation: - 'spec/lib/gitlab/themes_spec.rb' - 'spec/lib/gitlab/throttle_spec.rb' - 'spec/lib/gitlab/tree_summary_spec.rb' - - 'spec/lib/gitlab/utils_spec.rb' - 'spec/lib/gitlab/visibility_level_spec.rb' - 'spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb' diff --git a/Gemfile b/Gemfile index faada712239..d746892802e 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,11 @@ gem 'bootsnap', '~> 1.16.0', require: false gem 'openssl', '~> 3.0' gem 'ipaddr', '~> 1.2.5' +# GitLab Monorepo Gems +group :monorepo do + gem 'gitlab-utils', path: 'gems/gitlab-utils' +end + # Responders respond_to and respond_with gem 'responders', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 8844732d35e..395293f17a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,16 @@ PATH gitlab-rspec (0.1.0) rspec (~> 3.0) +PATH + remote: gems/gitlab-utils + specs: + gitlab-utils (0.1.0) + actionview (>= 6.1.7.2) + activesupport (>= 6.1.7.2) + addressable (~> 2.8) + nokogiri (~> 1.15.2) + rake (~> 13.0) + PATH remote: vendor/gems/attr_encrypted specs: @@ -1778,6 +1788,7 @@ DEPENDENCIES gitlab-rspec! gitlab-sidekiq-fetcher! gitlab-styles (~> 10.0.0) + gitlab-utils! gitlab_chronic_duration (~> 0.10.6.2) gitlab_omniauth-ldap (~> 2.2.0) gitlab_quality-test_tooling (~> 0.8.1) diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 39a7a76e91f..3b55611a8fa 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -9,7 +9,7 @@ import { renderJSONTable } from './render_json_table'; function initPopovers(elements) { if (!elements.length) return; - import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover') + import(/* webpackChunkName: 'IssuablePopoverBundle' */ 'ee_else_ce/issuable/popover') .then(({ default: initIssuablePopovers }) => { initIssuablePopovers(elements); }) @@ -39,7 +39,7 @@ export function renderGFM(element) { '.js-render-mermaid', '[lang="json"][data-lang-params="table"]', '.gfm-project_member', - '.gfm-issue, .gfm-work_item, .gfm-merge_request', + '.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic', '.js-render-metrics', '.js-render-observability', ].map((selector) => Array.from(element.querySelectorAll(selector))); diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js index bd9e41ac0ba..22e9b2b9709 100644 --- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js @@ -72,11 +72,14 @@ function fixElementSource(el) { export function getSandboxFrameSrc() { const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH); - if (!darkModeEnabled()) { - return path; + let absoluteUrl = relativePathToAbsolute(path, getBaseURL()); + if (darkModeEnabled()) { + absoluteUrl = setUrlParams({ darkMode: darkModeEnabled() }, absoluteUrl); } - const absoluteUrl = relativePathToAbsolute(path, getBaseURL()); - return setUrlParams({ darkMode: darkModeEnabled() }, absoluteUrl); + if (window.gon?.relative_url_root) { + absoluteUrl = setUrlParams({ relativeRootPath: window.gon.relative_url_root }, absoluteUrl); + } + return absoluteUrl; } function renderMermaidEl(el, source) { diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue index 799c0a18444..a83884dd261 100644 --- a/app/assets/javascripts/issuable/components/status_box.vue +++ b/app/assets/javascripts/issuable/components/status_box.vue @@ -4,7 +4,13 @@ import Vue from 'vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { fetchPolicies } from '~/lib/graphql'; import { __ } from '~/locale'; -import { STATUS_CLOSED, STATUS_OPEN, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants'; +import { + STATUS_CLOSED, + STATUS_OPEN, + TYPE_ISSUE, + TYPE_MERGE_REQUEST, + TYPE_EPIC, +} from '~/issues/constants'; export const badgeState = Vue.observable({ state: '', @@ -18,17 +24,22 @@ const CLASSES = { merged: 'issuable-status-badge-merged', }; -const ISSUE_ICONS = { - opened: 'issues', - locked: 'issues', - closed: 'issue-closed', -}; - -const MERGE_REQUEST_ICONS = { - opened: 'merge-request-open', - locked: 'merge-request-open', - closed: 'merge-request-close', - merged: 'merge', +const ICONS = { + [TYPE_EPIC]: { + opened: 'epic', + closed: 'epic-closed', + }, + [TYPE_ISSUE]: { + opened: 'issues', + locked: 'issues', + closed: 'issue-closed', + }, + [TYPE_MERGE_REQUEST]: { + opened: 'merge-request-open', + locked: 'merge-request-open', + closed: 'merge-request-close', + merged: 'merge', + }, }; const STATUS = { @@ -91,10 +102,8 @@ export default { return STATUS[this.state]; }, badgeIcon() { - if (this.issuableType === TYPE_ISSUE) { - return ISSUE_ICONS[this.state]; - } - return MERGE_REQUEST_ICONS[this.state]; + const type = this.issuableType || TYPE_MERGE_REQUEST; + return ICONS[type][this.state]; }, }, created() { diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue index 55fb3958e82..044a1bba7ad 100644 --- a/app/assets/javascripts/issuable/popover/components/issue_popover.vue +++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue @@ -28,7 +28,7 @@ export default { type: HTMLAnchorElement, required: true, }, - projectPath: { + namespacePath: { type: String, required: true, }, @@ -65,10 +65,10 @@ export default { query, update: (data) => data.project.issue, variables() { - const { projectPath, iid } = this; + const { namespacePath, iid } = this; return { - projectPath, + projectPath: namespacePath, iid, }; }, @@ -100,7 +100,7 @@ export default {
- {{ `${projectPath}#${iid}` }} + {{ `${namespacePath}#${iid}` }}
diff --git a/app/assets/javascripts/issuable/popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue index af93430963e..e2c2181684f 100644 --- a/app/assets/javascripts/issuable/popover/components/mr_popover.vue +++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue @@ -19,7 +19,7 @@ export default { type: HTMLAnchorElement, required: true, }, - projectPath: { + namespacePath: { type: String, required: true, }, @@ -76,10 +76,10 @@ export default { query, update: (data) => data.project.mergeRequest, variables() { - const { projectPath, iid } = this; + const { namespacePath, iid } = this; return { - projectPath, + projectPath: namespacePath, iid, }; }, @@ -108,7 +108,7 @@ export default {
{{ title }}
- {{ `${projectPath}!${iid}` }} + {{ `${namespacePath}!${iid}` }}
diff --git a/app/assets/javascripts/issuable/popover/index.js b/app/assets/javascripts/issuable/popover/index.js index 9430419685b..58f015fe40e 100644 --- a/app/assets/javascripts/issuable/popover/index.js +++ b/app/assets/javascripts/issuable/popover/index.js @@ -4,7 +4,7 @@ import createDefaultClient from '~/lib/graphql'; import IssuePopover from './components/issue_popover.vue'; import MRPopover from './components/mr_popover.vue'; -const componentsByReferenceType = { +export const componentsByReferenceTypeMap = { issue: IssuePopover, work_item: IssuePopover, merge_request: MRPopover, @@ -26,9 +26,10 @@ const popoverMountedAttr = 'data-popover-mounted'; * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes. * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover */ -const handleIssuablePopoverMount = ({ +export const handleIssuablePopoverMount = ({ + componentsByReferenceType = componentsByReferenceTypeMap, apolloProvider, - projectPath, + namespacePath, title, iid, referenceType, @@ -42,7 +43,7 @@ const handleIssuablePopoverMount = ({ new PopoverComponent({ propsData: { target, - projectPath, + namespacePath, iid, cachedTitle: title, }, @@ -53,7 +54,7 @@ const handleIssuablePopoverMount = ({ }, 200); // 200ms delay so not every mouseover triggers Popover + API Call }; -export default (elements) => { +export default (elements, issuablePopoverMount = handleIssuablePopoverMount) => { if (elements.length > 0) { Vue.use(VueApollo); @@ -63,15 +64,16 @@ export default (elements) => { const listenerAddedAttr = 'data-popover-listener-added'; elements.forEach((el) => { - const { projectPath, iid, referenceType } = el.dataset; + const { projectPath, groupPath, iid, referenceType } = el.dataset; const title = el.dataset.mrTitle || el.title; + const namespacePath = groupPath || projectPath; - if (!el.getAttribute(listenerAddedAttr) && projectPath && title && iid && referenceType) { + if (!el.getAttribute(listenerAddedAttr) && namespacePath && title && iid && referenceType) { el.addEventListener('mouseenter', ({ target }) => { if (!el.getAttribute(popoverMountedAttr)) { - handleIssuablePopoverMount({ + issuablePopoverMount({ apolloProvider, - projectPath, + namespacePath, title, iid, referenceType, diff --git a/app/assets/javascripts/lib/mermaid.js b/app/assets/javascripts/lib/mermaid.js index bbc1d8ae1e1..5e62a023777 100644 --- a/app/assets/javascripts/lib/mermaid.js +++ b/app/assets/javascripts/lib/mermaid.js @@ -1,6 +1,13 @@ import mermaid from 'mermaid'; import { getParameterByName } from '~/lib/utils/url_utility'; +import { resetServiceWorkersPublicPath } from '~/lib/utils/webpack'; +const resetWebpackPublicPath = () => { + window.gon = { relative_url_root: getParameterByName('relativeRootPath') }; + resetServiceWorkersPublicPath(); +}; + +resetWebpackPublicPath(); const setIframeRenderedSize = (h, w) => { const { origin } = window.location; window.parent.postMessage({ h, w }, origin); diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js index e1a57bf4589..b0264796d90 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js @@ -3,6 +3,7 @@ import dateFormat from '~/lib/dateformat'; import { roundToNearestHalf } from '~/lib/utils/common_utils'; import { sanitize } from '~/lib/dompurify'; import { s__, n__, __, sprintf } from '~/locale'; +import { parsePikadayDate } from './pikaday_utility'; /** * Returns i18n month names array. @@ -420,3 +421,34 @@ export const formatUtcOffset = (offset) => { * @returns {String} the UTC timezone with the offset, e.g. `[UTC+2] Berlin, [UTC 0] London` */ export const formatTimezone = ({ offset, name }) => `[UTC${formatUtcOffset(offset)}] ${name}`; + +/** + * Returns humanized string showing date range from provided start and due dates. + * + * @param {Date} startDate + * @param {Date} dueDate + * @returns + */ +export const humanTimeframe = (startDate, dueDate) => { + const start = startDate ? parsePikadayDate(startDate) : null; + const due = dueDate ? parsePikadayDate(dueDate) : null; + + if (startDate && dueDate) { + const startDateInWords = dateInWords(start, true, start.getFullYear() === due.getFullYear()); + const dueDateInWords = dateInWords(due, true); + + return sprintf(__('%{startDate} – %{dueDate}'), { + startDate: startDateInWords, + dueDate: dueDateInWords, + }); + } else if (startDate && !dueDate) { + return sprintf(__('%{startDate} – No due date'), { + startDate: dateInWords(start, true, false), + }); + } else if (!startDate && dueDate) { + return sprintf(__('No start date – %{dueDate}'), { + dueDate: dateInWords(due, true, false), + }); + } + return ''; +}; diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js index 968e866eedd..0e440750fdb 100644 --- a/app/assets/javascripts/tracking/constants.js +++ b/app/assets/javascripts/tracking/constants.js @@ -25,3 +25,7 @@ export const URLS_CACHE_STORAGE_KEY = 'gl-snowplow-pseudonymized-urls'; export const REFERRER_TTL = 24 * 60 * 60 * 1000; export const GOOGLE_ANALYTICS_ID_COOKIE_NAME = '_ga'; + +export const GITLAB_INTERNAL_EVENT_CATEGORY = 'InternalEventTracking'; + +export const SERVICE_PING_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-0'; diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js index 472ce3c5bbf..6494838abac 100644 --- a/app/assets/javascripts/tracking/index.js +++ b/app/assets/javascripts/tracking/index.js @@ -2,8 +2,10 @@ import { getAllExperimentContexts } from '~/experimentation/utils'; import { DEFAULT_SNOWPLOW_OPTIONS } from './constants'; import getStandardContext from './get_standard_context'; import Tracking from './tracking'; +import InternalEvents from './internal_events'; export { Tracking as default }; +export { InternalEvents }; /** * Tracker initialization as defined in: diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js new file mode 100644 index 00000000000..a69f192f520 --- /dev/null +++ b/app/assets/javascripts/tracking/internal_events.js @@ -0,0 +1,36 @@ +import API from '~/api'; + +import Tracking from './tracking'; +import { GITLAB_INTERNAL_EVENT_CATEGORY, SERVICE_PING_SCHEMA } from './constants'; + +const InternalEvents = { + /** + * Returns an implementation of this class in the form of + * a Vue mixin. + * + * @param {Object} opts - default options for all events + * @returns {Object} + */ + mixin(opts = {}) { + return { + mixins: [Tracking.mixin(opts)], + methods: { + track_event(event) { + API.trackRedisHllUserEvent(event); + this.track(event, { + context: { + schema: SERVICE_PING_SCHEMA, + data: { + event_name: event, + data_source: 'redis_hll', + }, + }, + category: GITLAB_INTERNAL_EVENT_CATEGORY, + }); + }, + }, + }; + }, +}; + +export default InternalEvents; diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 222fcc17222..29b61264322 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -110,7 +110,7 @@ module UploadsActions if uploader_mounted? model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend else - build_uploader_from_upload || build_uploader_from_params + build_uploader_from_upload end end strong_memoize_attr :uploader @@ -125,21 +125,6 @@ module UploadsActions end # rubocop: enable CodeReuse/ActiveRecord - def build_uploader_from_params - return unless uploader = build_uploader - - uploader.retrieve_from_store!(params[:filename]) - - Gitlab::AppJsonLogger.info( - message: 'Deprecated usage of build_uploader_from_params', - uploader_class: uploader.class.name, - path: params[:filename], - exists: uploader.exists? - ) - - uploader - end - def build_uploader return unless params[:secret] && params[:filename] diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 76c733b1c0b..c70100c03c8 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -4,7 +4,7 @@ # # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled # fields to a new table "project_features", support for the old fields is still needed in the API. -require 'gitlab/utils' +require 'gitlab/utils/all' module ProjectFeaturesCompatibility extend ActiveSupport::Concern diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json index fb6b80e0725..9cfb62d4439 100644 --- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json +++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json @@ -32,12 +32,12 @@ }, { "field": "SEARCH_MAX_DEPTH", - "label": "Search maximum depth", + "label": "Search Maximum Depth", "type": "string", "default_value": "", "value": "", "size": "SMALL", - "description": "Maximum depth of language and framework detection" + "description": "Specifies the number of directory levels to be included in the repository search phase during SAST analysis. SAST scanner searches through the repository to detect the programming languages used and selects the corresponding analyzers. After that, the entire repository is analyzed." } ], "analyzers": [ @@ -80,56 +80,72 @@ "label": "Kubesec", "enabled": true, "description": "Kubernetes manifests, Helm Charts", - "variables": [] + "variables": [ + + ] }, { "name": "nodejs-scan", "label": "Node.js Scan", "enabled": true, "description": "Node.js", - "variables": [] + "variables": [ + + ] }, { "name": "phpcs-security-audit", "label": "PHP Security Audit", "enabled": true, "description": "PHP", - "variables": [] + "variables": [ + + ] }, { "name": "pmd-apex", "label": "PMD APEX", "enabled": true, "description": "Apex (Salesforce)", - "variables": [] + "variables": [ + + ] }, { "name": "security-code-scan", "label": "Security Code Scan", "enabled": true, "description": ".NET Core, .NET Framework", - "variables": [] + "variables": [ + + ] }, { "name": "semgrep", "label": "Semgrep", "enabled": true, "description": "Multi-language scanning", - "variables": [] + "variables": [ + + ] }, { "name": "sobelow", "label": "Sobelow", "enabled": true, "description": "Elixir (Phoenix)", - "variables": [] + "variables": [ + + ] }, { "name": "spotbugs", "label": "Spotbugs", "enabled": true, "description": "Groovy, Java, Scala", - "variables": [] + "variables": [ + + ] } ] -} \ No newline at end of file +} diff --git a/bin/audit-event-type b/bin/audit-event-type index fec34724c7c..e9d72aaba46 100755 --- a/bin/audit-event-type +++ b/bin/audit-event-type @@ -11,9 +11,10 @@ require 'yaml' require 'fileutils' require 'uri' require 'readline' +require_relative '../config/bundler_setup' +require 'gitlab/utils/all' require_relative '../lib/gitlab/audit/type/shared' unless defined?(::Gitlab::Audit::Type::Shared) -require_relative '../lib/gitlab/utils' unless defined?(::Gitlab::Utils) module AuditEventTypeHelpers Abort = Class.new(StandardError) diff --git a/config/application.rb b/config/application.rb index 06153b377f3..c8bb56ce956 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,8 @@ require 'action_mailer/railtie' require 'action_cable/engine' require 'rails/test_unit/railtie' +require 'gitlab/utils/all' + Bundler.require(*Rails.groups) module Gitlab @@ -49,7 +51,6 @@ module Gitlab ActiveSupport.to_time_preserves_timezone = false require_dependency Rails.root.join('lib/gitlab') - require_dependency Rails.root.join('lib/gitlab/utils') require_dependency Rails.root.join('lib/gitlab/action_cable/config') require_dependency Rails.root.join('lib/gitlab/redis/wrapper') require_dependency Rails.root.join('lib/gitlab/redis/cache') diff --git a/config/environments/test.rb b/config/environments/test.rb index da91752549e..b919df45214 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -4,7 +4,7 @@ require 'gitlab/testing/request_blocker_middleware' require 'gitlab/testing/robots_blocker_middleware' require 'gitlab/testing/request_inspector_middleware' require 'gitlab/testing/clear_process_memory_cache_middleware' -require 'gitlab/utils' +require 'gitlab/utils/all' Rails.application.configure do # Make sure the middleware is inserted first in middleware chain diff --git a/db/docs/subscription_user_add_on_assignments.yml b/db/docs/subscription_user_add_on_assignments.yml new file mode 100644 index 00000000000..acd9b821115 --- /dev/null +++ b/db/docs/subscription_user_add_on_assignments.yml @@ -0,0 +1,10 @@ +--- +table_name: subscription_user_add_on_assignments +description: Tracks the assignment of an add-on to a user within a namespace +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123967 +milestone: '16.2' +feature_categories: +- seat_cost_management +classes: +- GitlabSubscriptions::UserAddOnAssignment +gitlab_schema: gitlab_main diff --git a/db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb b/db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb new file mode 100644 index 00000000000..cb184cd1987 --- /dev/null +++ b/db/migrate/20230616164309_create_subscription_user_add_on_assignments.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateSubscriptionUserAddOnAssignments < Gitlab::Database::Migration[2.1] + UNIQUE_INDEX_NAME = 'uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user' + + def change + create_table :subscription_user_add_on_assignments do |t| + t.bigint :add_on_purchase_id, null: false + t.bigint :user_id, null: false + + t.timestamps_with_timezone null: false + + t.index [:add_on_purchase_id, :user_id], unique: true, name: UNIQUE_INDEX_NAME + t.index :user_id + end + end +end diff --git a/db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb b/db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb new file mode 100644 index 00000000000..d0d89bd5027 --- /dev/null +++ b/db/migrate/20230616164705_add_foreign_key_add_on_purchase_id_on_subscription_user_add_on_assignments.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddForeignKeyAddOnPurchaseIdOnSubscriptionUserAddOnAssignments < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :subscription_user_add_on_assignments, :subscription_add_on_purchases, + column: :add_on_purchase_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :subscription_user_add_on_assignments, column: :add_on_purchase_id + end + end +end diff --git a/db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb b/db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb new file mode 100644 index 00000000000..a28c798deec --- /dev/null +++ b/db/migrate/20230616164731_add_foreign_key_user_id_on_subscription_user_add_on_assignments.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddForeignKeyUserIdOnSubscriptionUserAddOnAssignments < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :subscription_user_add_on_assignments, :users, column: :user_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :subscription_user_add_on_assignments, column: :user_id + end + end +end diff --git a/db/schema_migrations/20230616164309 b/db/schema_migrations/20230616164309 new file mode 100644 index 00000000000..b9fbdc7d33e --- /dev/null +++ b/db/schema_migrations/20230616164309 @@ -0,0 +1 @@ +f3b14748f1702972e7f5069edd9ed25d9896dfb11f4fc4a4386ca9c94533e10a \ No newline at end of file diff --git a/db/schema_migrations/20230616164705 b/db/schema_migrations/20230616164705 new file mode 100644 index 00000000000..1bcb723524b --- /dev/null +++ b/db/schema_migrations/20230616164705 @@ -0,0 +1 @@ +75310614bb98a598b8425aa87a0a4a6561fa1b166461d55329c21aff849d71fc \ No newline at end of file diff --git a/db/schema_migrations/20230616164731 b/db/schema_migrations/20230616164731 new file mode 100644 index 00000000000..2588271d9ac --- /dev/null +++ b/db/schema_migrations/20230616164731 @@ -0,0 +1 @@ +e6308ee437b6e57da16e1b8aff1d6a571ef849c4c7cccafe940710c799fa6eea \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 35c119628ce..5cff7ab4337 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23057,6 +23057,23 @@ CREATE SEQUENCE subscription_add_ons_id_seq ALTER SEQUENCE subscription_add_ons_id_seq OWNED BY subscription_add_ons.id; +CREATE TABLE subscription_user_add_on_assignments ( + id bigint NOT NULL, + add_on_purchase_id bigint NOT NULL, + user_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE subscription_user_add_on_assignments_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE subscription_user_add_on_assignments_id_seq OWNED BY subscription_user_add_on_assignments.id; + CREATE TABLE subscriptions ( id integer NOT NULL, user_id integer, @@ -25862,6 +25879,8 @@ ALTER TABLE ONLY subscription_add_on_purchases ALTER COLUMN id SET DEFAULT nextv ALTER TABLE ONLY subscription_add_ons ALTER COLUMN id SET DEFAULT nextval('subscription_add_ons_id_seq'::regclass); +ALTER TABLE ONLY subscription_user_add_on_assignments ALTER COLUMN id SET DEFAULT nextval('subscription_user_add_on_assignments_id_seq'::regclass); + ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscriptions_id_seq'::regclass); ALTER TABLE ONLY suggestions ALTER COLUMN id SET DEFAULT nextval('suggestions_id_seq'::regclass); @@ -28351,6 +28370,9 @@ ALTER TABLE ONLY subscription_add_on_purchases ALTER TABLE ONLY subscription_add_ons ADD CONSTRAINT subscription_add_ons_pkey PRIMARY KEY (id); +ALTER TABLE ONLY subscription_user_add_on_assignments + ADD CONSTRAINT subscription_user_add_on_assignments_pkey PRIMARY KEY (id); + ALTER TABLE ONLY subscriptions ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id); @@ -32910,6 +32932,8 @@ CREATE INDEX index_subscription_add_on_purchases_on_subscription_add_on_id ON su CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons USING btree (name); +CREATE INDEX index_subscription_user_add_on_assignments_on_user_id ON subscription_user_add_on_assignments USING btree (user_id); + CREATE INDEX index_subscriptions_on_project_id ON subscriptions USING btree (project_id); CREATE UNIQUE INDEX index_subscriptions_on_subscribable_and_user_id_and_project_id ON subscriptions USING btree (subscribable_id, subscribable_type, user_id, project_id); @@ -33556,6 +33580,8 @@ CREATE UNIQUE INDEX u_project_compliance_standards_adherence_for_reporting ON pr CREATE UNIQUE INDEX uniq_idx_packages_packages_on_project_id_name_version_ml_model ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 14); +CREATE UNIQUE INDEX uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user ON subscription_user_add_on_assignments USING btree (add_on_purchase_id, user_id); + CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name); CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name); @@ -35227,6 +35253,9 @@ ALTER TABLE ONLY notification_settings ALTER TABLE ONLY lists ADD CONSTRAINT fk_0d3f677137 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE; +ALTER TABLE ONLY subscription_user_add_on_assignments + ADD CONSTRAINT fk_0d89020c49 FOREIGN KEY (add_on_purchase_id) REFERENCES subscription_add_on_purchases(id) ON DELETE CASCADE; + ALTER TABLE ONLY deployment_approvals ADD CONSTRAINT fk_0f58311058 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -35596,6 +35625,9 @@ ALTER TABLE ONLY integrations ALTER TABLE ONLY user_interacted_projects ADD CONSTRAINT fk_722ceba4f7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY subscription_user_add_on_assignments + ADD CONSTRAINT fk_724c2df9a8 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY vulnerabilities ADD CONSTRAINT fk_725465b774 FOREIGN KEY (dismissed_by_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index df364a3f737..7a24db4347b 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -19,8 +19,6 @@ Read more about [configuring rate limits](../security/rate_limits.md). ### Issue creation -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10. - This setting limits the request rate to the issue creation endpoint. Read more about [issue creation rate limits](../user/admin_area/settings/rate_limit_on_issues_creation.md). @@ -37,8 +35,6 @@ Read more about [User and IP rate limits](../user/admin_area/settings/user_and_i ### By raw endpoint -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30829) in GitLab 12.2. - This setting limits the request rate per endpoint. Read more about [raw endpoint rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md). @@ -69,8 +65,6 @@ Read more about [protected path rate limits](../user/admin_area/settings/protect ### Package Registry -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57029) in GitLab 13.12. - This setting limits the request rate on the Packages API per user or IP. For more information, see [Package Registry Rate Limits](../user/admin_area/settings/package_registry_rate_limits.md). @@ -107,18 +101,16 @@ This setting limits the request rate on deprecated API endpoints per user or IP ### Import/Export -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35728) in GitLab 13.2. - This setting limits the import/export actions for groups and projects. | Limit | Default (per minute per user) | |-------------------------|-------------------------------| -| Project Import | 6 | -| Project Export | 6 | -| Project Export Download | 1 | -| Group Import | 6 | -| Group Export | 6 | -| Group Export Download | 1 | +| Project Import | 6 | +| Project Export | 6 | +| Project Export Download | 1 | +| Group Import | 6 | +| Group Export | 6 | +| Group Export Download | 1 | Read more about [import/export rate limits](../user/admin_area/settings/import_export_rate_limits.md). @@ -162,10 +154,10 @@ Set the limit to `0` to disable it. This setting limits search requests as follows: -| Limit | Default (requests per minute) | -|-------------------------|-------------------------------| -| Authenticated user | 300 | -| Unauthenticated user | 100 | +| Limit | Default (requests per minute) | +|----------------------|-------------------------------| +| Authenticated user | 300 | +| Unauthenticated user | 100 | Search requests that exceed the search rate limit per minute return the following error: @@ -191,8 +183,6 @@ Read more about [Gitaly concurrency limits](gitaly/configure_gitaly.md#limit-rpc ## Number of comments per issue, merge request, or commit -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22388) in GitLab 12.4. - There's a limit to the number of comments that can be submitted on an issue, merge request, or commit. When the limit is reached, system notes can still be added so that the history of events is not lost, but the user-submitted @@ -202,8 +192,6 @@ comment fails. ## Size of comments and descriptions of issues, merge requests, and epics -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/61974) in GitLab 12.2. - There is a limit to the size of comments and descriptions of issues, merge requests, and epics. Attempting to add a body of text larger than the limit, results in an error, and the item is also not created. @@ -214,8 +202,6 @@ It's possible that this limit changes to a lower number in the future. ## Size of commit titles and descriptions -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292039) in GitLab 13.9. - Commits with arbitrarily large messages may be pushed to GitLab, but the following display limits apply: @@ -231,9 +217,6 @@ are processed. ## Number of issues in the milestone overview -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39453) in GitLab 12.10. -> - [Set](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58168) to 500 in GitLab 13.11. - The maximum number of issues loaded on the milestone overview page is 500. When the number exceeds the limit the page displays an alert and links to a paginated [issue list](../user/project/issues/managing_issues.md) of all issues in the milestone. @@ -242,8 +225,6 @@ When the number exceeds the limit the page displays an alert and links to a pagi ## Number of pipelines per Git push -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51401) in GitLab 11.10. - When pushing multiple changes with a single Git push, like multiple tags or branches, only four tag or branch pipelines can be triggered. This limit prevents the accidental creation of a large number of pipelines when using `git push --all` or `git push --mirror`. @@ -259,12 +240,10 @@ instance if too many changes are pushed at once and a flood of pipelines are cre ## Retention of activity history -Activity history for projects and individuals' profiles was limited to one year until [GitLab 11.4](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/52246) when it was extended to two years, and in [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/issues/33840) to three years. +Activity history for projects and individuals' profiles is limited to three years. ## Number of embedded metrics -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14939) in GitLab 12.7. - There is a limit when embedding metrics in GitLab Flavored Markdown (GLFM) for performance reasons. - **Max limit**: 100 embeds. @@ -343,8 +322,6 @@ Blocked recursive webhook calls are logged in `auth.log` with the message `"Recu ## Pull Mirroring Interval -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/237891) in GitLab 13.7. - The [minimum wait time between pull refreshes](../user/project/repository/mirror/index.md) defaults to 300 seconds (5 minutes). For example, a pull refresh only runs once in a given 300 second period, regardless of how many times you trigger it. @@ -362,8 +339,6 @@ Plan.default.actual_limits.update!(pull_mirror_interval_seconds: 200) ## Incoming emails from auto-responders -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30327) in GitLab 12.4. - GitLab ignores all incoming emails sent from auto-responders by looking for the `X-Autoreply` header. Such emails don't create comments on issues or merge requests. @@ -376,8 +351,6 @@ and to limit memory consumption. ## Max offset allowed by the REST API for offset-based pagination -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0. - When using offset-based pagination in the REST API, there is a limit to the maximum requested offset into the set of results. This limit is only applied to endpoints that also support keyset-based pagination. More information about pagination options can be @@ -401,8 +374,6 @@ Set the limit to `0` to disable it. ### Number of jobs in active pipelines -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32823) in GitLab 12.6. - The total number of jobs in active pipelines can be limited per project. This limit is checked each time a new pipeline is created. An active pipeline is any pipeline in one of the following states: @@ -433,8 +404,6 @@ Set the limit to `0` to disable it. ### Maximum time jobs can run -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16777) in GitLab 12.3. - The default maximum time that jobs can run for is 60 minutes. Jobs that run for more than 60 minutes time out. @@ -447,8 +416,6 @@ You can change the maximum time a job can run before it times out: ### Maximum number of deployment jobs in a pipeline -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46931) in GitLab 13.7. - You can limit the maximum number of deployment jobs in a pipeline. A deployment is any job with an [`environment`](../ci/environments/index.md) specified. The number of deployments in a pipeline is checked at pipeline creation. Pipelines that have @@ -470,8 +437,6 @@ Set the limit to `0` to disable it. ### Number of CI/CD subscriptions to a project -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in GitLab 12.9. - The total number of subscriptions can be limited per project. This limit is checked each time a new subscription is created. @@ -516,8 +481,6 @@ This limit is [enabled on GitLab.com](../user/gitlab_com/index.md#gitlab-cicd). ### Number of pipeline schedules -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29566) in GitLab 12.10. - The total number of pipeline schedules can be limited per project. This limit is checked each time a new pipeline schedule is created. If a new pipeline schedule would cause the total number of pipeline schedules to exceed the limit, the @@ -585,8 +548,6 @@ This limit is [enabled on GitLab.com](../user/gitlab_com/index.md#gitlab-cicd). ### Number of instance level variables -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216097) in GitLab 13.1. - The total number of instance level CI/CD variables is limited at the instance level. This limit is checked each time a new instance level variable is created. If a new variable would cause the total number of variables to exceed the limit, the new variable is not created. @@ -639,8 +600,6 @@ Plan.default.actual_limits.update!(project_ci_variables: 10000) ### Maximum file size per type of artifact -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37226) in GitLab 13.3. - Job artifacts defined with [`artifacts:reports`](../ci/yaml/index.md#artifactsreports) that are uploaded by the runner are rejected if the file size exceeds the maximum file size limit. The limit is determined by comparing the project's @@ -655,35 +614,35 @@ setting is used: | Artifact limit name | Default value | |---------------------------------------------|---------------| -| `ci_max_artifact_size_accessibility` | 0 | -| `ci_max_artifact_size_api_fuzzing` | 0 | -| `ci_max_artifact_size_archive` | 0 | -| `ci_max_artifact_size_browser_performance` | 0 | -| `ci_max_artifact_size_cluster_applications` | 0 | -| `ci_max_artifact_size_cobertura` | 0 | -| `ci_max_artifact_size_codequality` | 0 | -| `ci_max_artifact_size_container_scanning` | 0 | -| `ci_max_artifact_size_coverage_fuzzing` | 0 | -| `ci_max_artifact_size_dast` | 0 | -| `ci_max_artifact_size_dependency_scanning` | 0 | -| `ci_max_artifact_size_dotenv` | 0 | -| `ci_max_artifact_size_junit` | 0 | -| `ci_max_artifact_size_license_management` | 0 | -| `ci_max_artifact_size_license_scanning` | 0 | -| `ci_max_artifact_size_load_performance` | 0 | -| `ci_max_artifact_size_lsif` | 100 MB ([Introduced at 20 MB](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37226) in GitLab 13.3 and [raised to 100 MB](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46980) in GitLab 13.6.) | -| `ci_max_artifact_size_metadata` | 0 | -| `ci_max_artifact_size_metrics_referee` | 0 | -| `ci_max_artifact_size_metrics` | 0 | -| `ci_max_artifact_size_network_referee` | 0 | -| `ci_max_artifact_size_performance` | 0 | -| `ci_max_artifact_size_requirements` | 0 | -| `ci_max_artifact_size_requirements_v2` | 0 | -| `ci_max_artifact_size_sast` | 0 | -| `ci_max_artifact_size_secret_detection` | 0 | -| `ci_max_artifact_size_terraform` | 5 MB ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37018) in GitLab 13.3) | -| `ci_max_artifact_size_trace` | 0 | -| `ci_max_artifact_size_cyclonedx` | 1 MB | +| `ci_max_artifact_size_accessibility` | 0 | +| `ci_max_artifact_size_api_fuzzing` | 0 | +| `ci_max_artifact_size_archive` | 0 | +| `ci_max_artifact_size_browser_performance` | 0 | +| `ci_max_artifact_size_cluster_applications` | 0 | +| `ci_max_artifact_size_cobertura` | 0 | +| `ci_max_artifact_size_codequality` | 0 | +| `ci_max_artifact_size_container_scanning` | 0 | +| `ci_max_artifact_size_coverage_fuzzing` | 0 | +| `ci_max_artifact_size_dast` | 0 | +| `ci_max_artifact_size_dependency_scanning` | 0 | +| `ci_max_artifact_size_dotenv` | 0 | +| `ci_max_artifact_size_junit` | 0 | +| `ci_max_artifact_size_license_management` | 0 | +| `ci_max_artifact_size_license_scanning` | 0 | +| `ci_max_artifact_size_load_performance` | 0 | +| `ci_max_artifact_size_lsif` | 100 MB | +| `ci_max_artifact_size_metadata` | 0 | +| `ci_max_artifact_size_metrics_referee` | 0 | +| `ci_max_artifact_size_metrics` | 0 | +| `ci_max_artifact_size_network_referee` | 0 | +| `ci_max_artifact_size_performance` | 0 | +| `ci_max_artifact_size_requirements` | 0 | +| `ci_max_artifact_size_requirements_v2` | 0 | +| `ci_max_artifact_size_sast` | 0 | +| `ci_max_artifact_size_secret_detection` | 0 | +| `ci_max_artifact_size_terraform` | 5 MB | +| `ci_max_artifact_size_trace` | 0 | +| `ci_max_artifact_size_cyclonedx` | 1 MB | For example, to set the `ci_max_artifact_size_junit` limit to 10 MB on a self-managed installation, run the following in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): @@ -732,10 +691,10 @@ GitLab SaaS subscribers have different limits defined per plan, affecting all pr Self-managed GitLab Premium and Ultimate limits are defined by a default plan that affects all projects: -| Runner scope | Default value | -|---------------------------------------------|---------------| -| `ci_registered_group_runners` | 1000 | -| `ci_registered_project_runners` | 1000 | +| Runner scope | Default value | +|---------------------------------|---------------| +| `ci_registered_group_runners` | 1000 | +| `ci_registered_project_runners` | 1000 | To update these limits, run the following in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): @@ -839,28 +798,20 @@ Plan.default.actual_limits.update!(dotenv_size: 5.kilobytes) ### Limit inbound incident management alerts -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17859) in GitLab 12.5. - This setting limits the number of inbound alert payloads over a period of time. Read more about [incident management rate limits](../user/admin_area/settings/rate_limit_on_pipelines_creation.md). ### Prometheus Alert JSON payloads -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19940) in GitLab 12.6. - Prometheus alert payloads sent to the `notify.json` endpoint are limited to 1 MB in size. ### Generic Alert JSON payloads -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16441) in GitLab 12.4. - Alert payloads sent to the `notify.json` endpoint are limited to 1 MB in size. ### Metrics dashboard YAML files -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34834) in GitLab 13.2. - The memory occupied by a parsed metrics dashboard YAML file cannot exceed 1 MB. The maximum depth of each YAML file is limited to 100. The maximum depth of a YAML @@ -891,8 +842,6 @@ panel_groups: ## Environment Dashboard limits **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33895) in GitLab 13.4. - See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding-a-project-to-the-dashboard) for the maximum number of displayed projects. ## Environment data on deploy boards @@ -932,8 +881,6 @@ Reports that go over the 20 MB limit aren't loaded. Affected reports: ### Maximum file size indexed -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8638) in GitLab 13.3. - You can set a limit on the content of repository files that are indexed in Elasticsearch. Any files larger than this limit only index the filename. The file content is neither indexed nor searchable. @@ -949,8 +896,6 @@ is pre-allocated during indexing. ### Maximum field length -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201826) in GitLab 12.8. - You can set a limit on the content of text fields indexed for advanced search. Setting a maximum helps to reduce the load of the indexing processes. If any text field exceeds this limit, then the text is truncated to this number of @@ -987,8 +932,6 @@ The maximum allowed [push size](../user/admin_area/settings/account_and_limit_se ### Webhooks and Project Services -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31009) in GitLab 12.4. - Total number of changes (branches or tags) in a single push. If changes are more than the specified limit, hooks are not executed. @@ -999,8 +942,6 @@ More information can be found in these documentations: ### Activities -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31007) in GitLab 12.4. - Total number of changes (branches or tags) in a single push to determine whether individual push events or a bulk push event are created. @@ -1010,8 +951,6 @@ More information can be found in the [Push event activities limit and bulk push ### File Size Limits -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218017) in GitLab 13.4. - The default maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) varies by format: - Conan: 3 GB @@ -1074,14 +1013,6 @@ varies by file type: - Image blob: 5 GB - Image manifest: 10 MB -## Branch retargeting on merge - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320902) in GitLab 13.9. - -If a branch is merged while open merge requests still point to it, GitLab can -retarget merge requests pointing to the now-merged branch. For more information, see -[Update merge requests when target branch merges](../user/project/merge_requests/index.md#update-merge-requests-when-target-branch-merges). - ## Maximum number of assignees and reviewers > - Maximum assignees [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/368936) in GitLab 15.6. diff --git a/doc/ci/runners/saas/macos_saas_runner.md b/doc/ci/runners/saas/macos_saas_runner.md index 836a14d7521..a559fc7d53e 100644 --- a/doc/ci/runners/saas/macos_saas_runner.md +++ b/doc/ci/runners/saas/macos_saas_runner.md @@ -45,7 +45,7 @@ Each image runs a specific version of macOS and Xcode. | (none, awaiting macOS 13) | `beta` | NOTE: -Each time you run a job that requires tooling or dependencies not available in the base image, those items must be added to the newly provisioned build VM increasing the total job duration. +If your job requires tooling or dependencies not available in our available images, those can only be installed in the job execution. ## Image update policy @@ -88,9 +88,6 @@ test: - echo "running scripts in the test job" ``` -NOTE: -You can specify a different Xcode image to run a job. To do so, replace the value for the `image` keyword with the value of the [virtual machine image name](#supported-macos-images) from the list of available images. The default value is our latest image. - ## Code signing iOS Projects with fastlane Before you can integrate GitLab with Apple services, install to a device, or deploy to the Apple App Store, you must [code sign](https://developer.apple.com/support/code-signing/) your application. @@ -106,22 +103,22 @@ Related topics: - [Code Signing Best Practice Guide](https://codesigning.guide/) - [fastlane authentication with Apple Services guide](https://docs.fastlane.tools/getting-started/ios/authentication/) -## Known Limitations and Usage Constraints - -- If the VM image does not include the specific software version you need for your job, then the job execution time will increase as the required software needs to be fetched and installed. -- At this time, it is not possible to bring your own OS image. -- The keychain for user `gitlab` is not publicly available. You must create a keychain instead. - ## Optimizing Homebrew By default, Homebrew checks for updates at the start of any operation. Homebrew has a -release cycle that may be more frequent than the GitLab MacOS image release cycle. This +release cycle that may be more frequent than the GitLab macOS image release cycle. This difference in release cycles may cause steps that call `brew` to take extra time to complete while Homebrew makes updates. -To reduce build time due to unintended Homebrew updates, set the `HOMEBREW_NO_AUTO_UPDATE` variable in `.gitlab-ci.yml` : +To reduce build time due to unintended Homebrew updates, set the `HOMEBREW_NO_AUTO_UPDATE` variable in `.gitlab-ci.yml`: ```yaml variables: HOMEBREW_NO_AUTO_UPDATE: 1 ``` + +## Known issues and usage constraints + +- If the VM image does not include the specific software version you need for your job, the required software must be fetched and installed. This causes an increase in job execution time. +- It is not possible to bring your own OS image. +- The keychain for user `gitlab` is not publicly available. You must create a keychain instead. diff --git a/doc/development/gems.md b/doc/development/gems.md index 4d364242efd..55963438347 100644 --- a/doc/development/gems.md +++ b/doc/development/gems.md @@ -107,7 +107,7 @@ You can see example adding new Gem: [!121676](https://gitlab.com/gitlab-org/gitl rspec: image: "ruby:${RUBY_VERSION}" cache: - key: gitlab- + key: gitlab--${RUBY_VERSION} paths: - gitlab-/vendor/ruby before_script: diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md index cadba17f03e..9e6abaa8eb3 100644 --- a/doc/development/pipelines/index.md +++ b/doc/development/pipelines/index.md @@ -755,10 +755,14 @@ graph RL; click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations" 2_5-1 --> 1-3 & 1-6 & 1-14 & 1-15; - 3_2-1["rspec:coverage (5 minutes)"]; + ac-1["rspec:artifact-collector (2 minutes)
(workaround for 'needs' limitation)"]; + class ac-1 criticalPath; + ac-1 --> 2_5-1; + + 3_2-1["rspec:coverage (3 minutes)"]; class 3_2-1 criticalPath; click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0" - 3_2-1 -.->|"(don't use needs
because of limitations)"| 2_5-1; + 3_2-1 --> ac-1; 4_3-1["rspec:undercoverage (1.3 minutes)"]; class 4_3-1 criticalPath; diff --git a/doc/integration/advanced_search/elasticsearch_troubleshooting.md b/doc/integration/advanced_search/elasticsearch_troubleshooting.md index e8eace7bd16..d3a065f1f99 100644 --- a/doc/integration/advanced_search/elasticsearch_troubleshooting.md +++ b/doc/integration/advanced_search/elasticsearch_troubleshooting.md @@ -256,6 +256,24 @@ Bulk requests getting rejected by the Elasticsearch nodes are likely due to load Ensure that your Elasticsearch cluster meets the [system requirements](elasticsearch.md#system-requirements) and has enough resources to perform bulk operations. See also the error ["429 (Too Many Requests)"](#indexing-fails-with-error-elastic-error-429-too-many-requests). +### Indexing fails with `strict_dynamic_mapping_exception` + +Indexing might fail if all [advanced search migrations were not finished before doing a major upgrade](elasticsearch.md#all-migrations-must-be-finished-before-doing-a-major-upgrade). +A large Sidekiq backlog might accompany this error. To fix the indexing failures, you must re-index the database, repositories, and wikis. + +1. Pause indexing so Sidekiq can catch up: + + ```shell + sudo gitlab-rake gitlab:elastic:pause_indexing + ``` + +1. [Recreate the index from scratch](#last-resort-to-recreate-an-index). +1. Resume indexing: + + ```shell + sudo gitlab-rake gitlab:elastic:resume_indexing + ``` + ### Last resort to recreate an index There may be cases where somehow data never got indexed and it's not in the diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 0b02de59ab4..c3a3c4f6d3c 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -58,11 +58,11 @@ The following table lists project permissions available for each role: | Action | Guest | Reporter | Developer | Maintainer | Owner | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------|-----------|------------|----------| | [Analytics](analytics/index.md):
View [issue analytics](analytics/issue_analytics.md) | ✓ | ✓ | ✓ | ✓ | ✓ | -| [Analytics](analytics/index.md):
View [merge request analytics](analytics/merge_request_analytics.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | [Analytics](analytics/index.md):
View [value stream analytics](group/value_stream_analytics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | [Analytics](analytics/index.md):
View [DORA metrics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | [Analytics](analytics/index.md):
View [CI/CD analytics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | [Analytics](analytics/index.md):
View [code review analytics](analytics/code_review_analytics.md) | | ✓ | ✓ | ✓ | ✓ | +| [Analytics](analytics/index.md):
View [merge request analytics](analytics/merge_request_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | [Analytics](analytics/index.md):
View [repository analytics](analytics/repository_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | [Application security](application_security/index.md):
View licenses in [dependency list](application_security/dependency_list/index.md) | | | ✓ | ✓ | ✓ | | [Application security](application_security/index.md):
Create and run [on-demand DAST scans](application_security/dast/proxy-based.md#on-demand-scans) | | | ✓ | ✓ | ✓ | diff --git a/gems/gitlab-rspec/.gitlab-ci.yml b/gems/gitlab-rspec/.gitlab-ci.yml index 0932753d1e7..95bdc51cb7d 100644 --- a/gems/gitlab-rspec/.gitlab-ci.yml +++ b/gems/gitlab-rspec/.gitlab-ci.yml @@ -12,7 +12,7 @@ workflow: rspec: image: "ruby:${RUBY_VERSION}" cache: - key: gitlab-rspec + key: gitlab-rspec-${RUBY_VERSION} paths: - gitlab-rspec/vendor/ruby before_script: diff --git a/gems/gitlab-rspec/gitlab-rspec.gemspec b/gems/gitlab-rspec/gitlab-rspec.gemspec index 061647190ec..f9cc83bb497 100644 --- a/gems/gitlab-rspec/gitlab-rspec.gemspec +++ b/gems/gitlab-rspec/gitlab-rspec.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |spec| spec.description = "A set of useful helpers to configure RSpec with various stubs and CI configs." spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-rspec" spec.license = "MIT" - spec.required_ruby_version = ">= 3.0" + spec.required_ruby_version = ">= 2.7" spec.files = Dir['lib/**/*.rb'] spec.test_files = Dir['spec/**/*'] diff --git a/gems/gitlab-utils/.gitignore b/gems/gitlab-utils/.gitignore new file mode 100644 index 00000000000..b04a8c840df --- /dev/null +++ b/gems/gitlab-utils/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status diff --git a/gems/gitlab-utils/.gitlab-ci.yml b/gems/gitlab-utils/.gitlab-ci.yml new file mode 100644 index 00000000000..ab92953b57d --- /dev/null +++ b/gems/gitlab-utils/.gitlab-ci.yml @@ -0,0 +1,30 @@ +# You can override the included template(s) by including variable overrides +# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings +# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings +# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings +# Note that environment variables can be set in several places +# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence +workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + +rspec: + image: "ruby:${RUBY_VERSION}" + cache: + key: gitlab-utils-${RUBY_VERSION} + paths: + - gitlab-utils/vendor/ruby + before_script: + - cd gems/gitlab-utils + - ruby -v # Print out ruby version for debugging + - gem install bundler --no-document # Bundler is not installed with the image + - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby + - bundle config set with 'development' + - bundle config set --local frozen 'true' # Disallow Gemfile.lock changes on CI + - bundle config # Show bundler configuration + - bundle install -j $(nproc) + script: + - bundle exec rspec + parallel: + matrix: + - RUBY_VERSION: ["2.7", "3.0", "3.1", "3.2"] diff --git a/gems/gitlab-utils/.rspec b/gems/gitlab-utils/.rspec new file mode 100644 index 00000000000..34c5164d9b5 --- /dev/null +++ b/gems/gitlab-utils/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/gems/gitlab-utils/.rubocop.yml b/gems/gitlab-utils/.rubocop.yml new file mode 100644 index 00000000000..7f0d48d1b2b --- /dev/null +++ b/gems/gitlab-utils/.rubocop.yml @@ -0,0 +1,31 @@ +inherit_from: + - ../../.rubocop.yml + +CodeReuse/ActiveRecord: + Enabled: false + +Gitlab/DocUrl: + Enabled: false + +Gitlab/NamespacedClass: + Enabled: false + +AllCops: + TargetRubyVersion: 3.0 + +Naming/FileName: + Exclude: + - spec/**/*.rb + - lib/gitlab/utils/all.rb + +Lint/AmbiguousRegexpLiteral: + Exclude: + - spec/**/*.rb + +RSpec/InstanceVariable: + Exclude: + - spec/**/*.rb + +Lint/BinaryOperatorWithIdenticalOperands: + Exclude: + - spec/**/*.rb diff --git a/gems/gitlab-utils/Gemfile b/gems/gitlab-utils/Gemfile new file mode 100644 index 00000000000..2c7228c874c --- /dev/null +++ b/gems/gitlab-utils/Gemfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in gitlab-utils.gemspec +gemspec + +group :development, :test do + gem 'gitlab-rspec', path: '../gitlab-rspec' +end diff --git a/gems/gitlab-utils/Gemfile.lock b/gems/gitlab-utils/Gemfile.lock new file mode 100644 index 00000000000..e7f954eeea3 --- /dev/null +++ b/gems/gitlab-utils/Gemfile.lock @@ -0,0 +1,193 @@ +PATH + remote: ../gitlab-rspec + specs: + gitlab-rspec (0.1.0) + rspec (~> 3.0) + +PATH + remote: . + specs: + gitlab-utils (0.1.0) + actionview (>= 6.1.7.2) + activesupport (>= 6.1.7.2) + addressable (~> 2.8) + nokogiri (~> 1.15.2) + rake (~> 13.0) + +GEM + remote: https://rubygems.org/ + specs: + actionpack (7.0.5) + actionview (= 7.0.5) + activesupport (= 7.0.5) + rack (~> 2.0, >= 2.2.4) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionview (7.0.5) + activesupport (= 7.0.5) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activesupport (7.0.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + benchmark-malloc (0.2.0) + benchmark-perf (0.6.0) + benchmark-trend (0.4.0) + binding_of_caller (1.0.0) + debug_inspector (>= 0.0.1) + builder (3.2.4) + coderay (1.1.3) + concurrent-ruby (1.2.2) + crass (1.0.6) + debug_inspector (1.1.0) + diff-lcs (1.5.0) + erubi (1.12.0) + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) + gitlab-styles (10.0.0) + rubocop (~> 1.43.0) + rubocop-graphql (~> 0.18) + rubocop-performance (~> 1.15) + rubocop-rails (~> 2.17) + rubocop-rspec (~> 2.18) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + json (2.6.3) + loofah (2.21.3) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + method_source (1.0.0) + mini_portile2 (2.8.2) + minitest (5.18.1) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + parallel (1.22.1) + parser (3.2.0.0) + ast (~> 2.4.1) + proc_to_ast (0.1.0) + coderay + parser + unparser + public_suffix (5.0.0) + racc (1.7.1) + rack (2.2.7) + rack-test (2.1.0) + rack (>= 1.3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.0.5) + actionpack (= 7.0.5) + activesupport (= 7.0.5) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rainbow (3.1.1) + rake (13.0.6) + regexp_parser (2.6.0) + rexml (3.2.5) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-benchmark (0.6.0) + benchmark-malloc (~> 0.2) + benchmark-perf (~> 0.6) + benchmark-trend (~> 0.4) + rspec (>= 3.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-parameterized (1.0.0) + rspec-parameterized-core (< 2) + rspec-parameterized-table_syntax (< 2) + rspec-parameterized-core (1.0.0) + parser + proc_to_ast + rspec (>= 2.13, < 4) + unparser + rspec-parameterized-table_syntax (1.0.0) + binding_of_caller + rspec-parameterized-core (< 2) + rspec-rails (6.0.1) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) + rspec-support (3.12.0) + rubocop (1.43.0) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.24.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.24.1) + parser (>= 3.1.1.0) + rubocop-capybara (2.18.0) + rubocop (~> 1.41) + rubocop-graphql (0.19.0) + rubocop (>= 0.87, < 2) + rubocop-performance (1.18.0) + rubocop (>= 1.7.0, < 2.0) + rubocop-ast (>= 0.4.0) + rubocop-rails (2.19.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rspec (2.18.1) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + ruby-progressbar (1.11.0) + thor (1.2.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.4.2) + unparser (0.6.7) + diff-lcs (~> 1.3) + parser (>= 3.2.0) + zeitwerk (2.6.8) + +PLATFORMS + ruby + +DEPENDENCIES + factory_bot_rails (~> 6.2.0) + gitlab-rspec! + gitlab-styles (~> 10.0.0) + gitlab-utils! + rspec-benchmark (~> 0.6.0) + rspec-parameterized (~> 1.0) + rspec-rails (~> 6.0.1) + rubocop (~> 1.21) + rubocop-rspec (~> 2.18.1) + +BUNDLED WITH + 2.4.4 diff --git a/gems/gitlab-utils/README.md b/gems/gitlab-utils/README.md new file mode 100644 index 00000000000..f7c7d83888b --- /dev/null +++ b/gems/gitlab-utils/README.md @@ -0,0 +1,8 @@ +# Gitlab::Utils + +This Gem contains all code that is not dependent on application code +or business logic and provides a generic functions like: + +- safe parsing of YAML +- version comparisions +- `strong_memoize` diff --git a/gems/gitlab-utils/Rakefile b/gems/gitlab-utils/Rakefile new file mode 100644 index 00000000000..cca71754493 --- /dev/null +++ b/gems/gitlab-utils/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[spec rubocop] diff --git a/gems/gitlab-utils/gitlab-utils.gemspec b/gems/gitlab-utils/gitlab-utils.gemspec new file mode 100644 index 00000000000..15e40ecc279 --- /dev/null +++ b/gems/gitlab-utils/gitlab-utils.gemspec @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative "lib/gitlab/utils/version" + +Gem::Specification.new do |spec| + spec.name = "gitlab-utils" + spec.version = Gitlab::Utils::Version::VERSION + spec.authors = ["group::tenant scale"] + spec.email = ["engineering@gitlab.com"] + + spec.summary = "GitLab's common helper methods" + spec.description = "A set of useful helpers methods to perform various conversions and checks." + spec.homepage = 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-utils' + spec.license = 'MIT' + spec.required_ruby_version = ">= 3.0" + + spec.files = Dir['lib/**/*.rb'] + spec.test_files = Dir['spec/**/*'] + spec.require_paths = ["lib"] + + spec.add_runtime_dependency 'actionview', '>= 6.1.7.2' + spec.add_runtime_dependency 'activesupport', '>= 6.1.7.2' + spec.add_runtime_dependency 'addressable', '~> 2.8' + spec.add_runtime_dependency 'nokogiri', '~> 1.15.2' + spec.add_runtime_dependency 'rake', '~> 13.0' + + spec.add_development_dependency 'factory_bot_rails', '~> 6.2.0' + spec.add_development_dependency 'gitlab-styles', '~> 10.0.0' + spec.add_development_dependency 'rspec-benchmark', '~> 0.6.0' + spec.add_development_dependency 'rspec-parameterized', '~> 1.0' + spec.add_development_dependency 'rspec-rails', '~> 6.0.1' + spec.add_development_dependency 'rubocop', '~> 1.21' + spec.add_development_dependency 'rubocop-rspec', '~> 2.18.1' +end diff --git a/gems/gitlab-utils/lib/gitlab/utils.rb b/gems/gitlab-utils/lib/gitlab/utils.rb new file mode 100644 index 00000000000..4e08ee8fcaf --- /dev/null +++ b/gems/gitlab-utils/lib/gitlab/utils.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +require "addressable/uri" +require "active_support/all" +require "action_view" + +module Gitlab + module Utils + extend self + DoubleEncodingError = Class.new(StandardError) + + def allowlisted?(absolute_path, allowlist) + path = absolute_path.downcase + + allowlist.map(&:downcase).any? do |allowed_path| + path.start_with?(allowed_path) + end + end + + def decode_path(encoded_path) + decoded = CGI.unescape(encoded_path) + if decoded != CGI.unescape(decoded) # rubocop:disable Style/IfUnlessModifier + raise DoubleEncodingError, "path #{encoded_path} is not allowed" + end + + decoded + end + + def force_utf8(str) + str.dup.force_encoding(Encoding::UTF_8) + end + + def ensure_utf8_size(str, bytes:) + raise ArgumentError, 'Empty string provided!' if str.empty? + raise ArgumentError, 'Negative string size provided!' if bytes < 0 + + truncated = str.each_char.each_with_object(+'') do |char, object| + if object.bytesize + char.bytesize > bytes # rubocop:disable Style/GuardClause + break object + else + object.concat(char) + end + end + + truncated + ('0' * (bytes - truncated.bytesize)) + end + + # Append path to host, making sure there's one single / in between + def append_path(host, path) + "#{host.to_s.sub(%r{\/+$}, '')}/#{remove_leading_slashes(path)}" # rubocop:disable Style/RedundantRegexpEscape + end + + def remove_leading_slashes(str) + str.to_s.sub(%r{^/+}, '') + end + + # A slugified version of the string, suitable for inclusion in URLs and + # domain names. Rules: + # + # * Lowercased + # * Anything not matching [a-z0-9-] is replaced with a - + # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen + def slugify(str) + str.downcase + .gsub(/[^a-z0-9]/, '-')[0..62] + .gsub(/(\A-+|-+\z)/, '') + end + + # Converts newlines into HTML line break elements + def nlbr(str) + ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '
').html_safe + end + + def remove_line_breaks(str) + str.gsub(/\r?\n/, '') + end + + def to_boolean(value, default: nil) + value = value.to_s if [0, 1].include?(value) + + return value if [true, false].include?(value) + return true if value =~ /^(true|t|yes|y|1|on)$/i + return false if value =~ /^(false|f|no|n|0|off)$/i + + default + end + + def boolean_to_yes_no(bool) + if bool + 'Yes' + else + 'No' + end + end + + # Behaves like `which` on Linux machines: given PATH, try to resolve the given + # executable name to an absolute path, or return nil. + # + # which('ruby') #=> /usr/bin/ruby + def which(filename) + ENV['PATH']&.split(File::PATH_SEPARATOR)&.each do |path| + full_path = File.join(path, filename) + return full_path if File.executable?(full_path) + end + + nil + end + + def try_megabytes_to_bytes(size) + Integer(size).megabytes + rescue ArgumentError + size + end + + def bytes_to_megabytes(bytes) + bytes.to_f / Numeric::MEGABYTE + end + + def ms_to_round_sec(ms) + (ms.to_f / 1000).round(6) + end + + # Used in EE + # Accepts either an Array or a String and returns an array + def ensure_array_from_string(string_or_array) + return string_or_array if string_or_array.is_a?(Array) + + string_or_array.split(',').map(&:strip) + end + + def deep_indifferent_access(data) + case data + when Array + data.map { |item| deep_indifferent_access(item) } + when Hash + data.with_indifferent_access + else + data + end + end + + def deep_symbolized_access(data) + case data + when Array + data.map { |item| deep_symbolized_access(item) } + when Hash + data.deep_symbolize_keys + else + data + end + end + + def string_to_ip_object(str) + return unless str + + IPAddr.new(str) + rescue IPAddr::InvalidAddressError + end + + # A safe alternative to String#downcase! + # + # This will make copies of frozen strings but downcase unfrozen + # strings in place, reducing allocations. + def safe_downcase!(str) + if str.frozen? + str.downcase + else + str.downcase! || str + end + end + + # Converts a string to an Addressable::URI object. + # If the string is not a valid URI, it returns nil. + # Param uri_string should be a String object. + # This method returns an Addressable::URI object or nil. + def parse_url(uri_string) + Addressable::URI.parse(uri_string) + rescue Addressable::URI::InvalidURIError, TypeError + end + + def add_url_parameters(url, params) + uri = parse_url(url.to_s) + uri.query_values = uri.query_values.to_h.merge(params.to_h.stringify_keys) + uri.query_values = nil if uri.query_values.empty? + uri.to_s + end + + def removes_sensitive_data_from_url(uri_string) + uri = parse_url(uri_string) + + return unless uri + return uri_string unless uri.fragment + + stripped_params = CGI.parse(uri.fragment) + if stripped_params['access_token'] + stripped_params['access_token'] = 'filtered' + filtered_query = Addressable::URI.new + filtered_query.query_values = stripped_params + + uri.fragment = filtered_query.query + end + + uri.to_s + end + + # Invert a hash, collecting all keys that map to a given value in an array. + # + # Unlike `Hash#invert`, where the last encountered pair wins, and which has the + # type `Hash[k, v] => Hash[v, k]`, `multiple_key_invert` does not lose any + # information, has the type `Hash[k, v] => Hash[v, Array[k]]`, and the original + # hash can always be reconstructed. + # + # example: + # + # multiple_key_invert({ a: 1, b: 2, c: 1 }) + # # => { 1 => [:a, :c], 2 => [:b] } + # + def multiple_key_invert(hash) + hash.flat_map { |k, v| Array.wrap(v).zip([k].cycle) } + .group_by(&:first) + .transform_values { |kvs| kvs.map(&:last) } + end + + # This sort is stable (see https://en.wikipedia.org/wiki/Sorting_algorithm#Stability) + # contrary to the bare Ruby sort_by method. Using just sort_by leads to + # instability across different platforms (e.g., x86_64-linux and x86_64-darwin18) + # which in turn leads to different sorting results for the equal elements across + # these platforms. + # This method uses a list item's original index position to break ties. + def stable_sort_by(list) + list.sort_by.with_index { |x, idx| [yield(x), idx] } + end + + # Check for valid brackets (`[` and `]`) in a string using this aspects: + # * open brackets count == closed brackets count + # * (optionally) reject nested brackets via `allow_nested: false` + # * open / close brackets coherence, eg. ][[] -> invalid + def valid_brackets?(string = '', allow_nested: true) + # remove everything except brackets + brackets = string.remove(/[^\[\]]/) + + return true if brackets.empty? + # balanced counts check + return false if brackets.size.odd? + + unless allow_nested + # nested brackets check + return false if brackets.include?('[[') || brackets.include?(']]') # rubocop:disable Style/SoleNestedConditional + end + + # open / close brackets coherence check + untrimmed = brackets + loop do + trimmed = untrimmed.gsub('[]', '') + return true if trimmed.empty? + return false if trimmed == untrimmed + + untrimmed = trimmed + end + end + end +end diff --git a/gems/gitlab-utils/lib/gitlab/utils/all.rb b/gems/gitlab-utils/lib/gitlab/utils/all.rb new file mode 100644 index 00000000000..200a21aad88 --- /dev/null +++ b/gems/gitlab-utils/lib/gitlab/utils/all.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "../utils" +require_relative "../version_info" +require_relative "version" +require_relative "strong_memoize" diff --git a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb new file mode 100644 index 00000000000..2b3841b8f09 --- /dev/null +++ b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module StrongMemoize + # Instead of writing patterns like this: + # + # def trigger_from_token + # return @trigger if defined?(@trigger) + # + # @trigger = Ci::Trigger.find_by_token(params[:token].to_s) + # end + # + # We could write it like: + # + # include Gitlab::Utils::StrongMemoize + # + # def trigger_from_token + # Ci::Trigger.find_by_token(params[:token].to_s) + # end + # strong_memoize_attr :trigger_from_token + # + # def enabled? + # Feature.enabled?(:some_feature) + # end + # strong_memoize_attr :enabled? + # + def strong_memoize(name) + key = ivar(name) + + if instance_variable_defined?(key) + instance_variable_get(key) + else + instance_variable_set(key, yield) + end + end + + # Works the same way as "strong_memoize" but takes + # a second argument - expire_in. This allows invalidate + # the data after specified number of seconds + def strong_memoize_with_expiration(name, expire_in) + key = ivar(name) + expiration_key = "#{key}_expired_at" + + if instance_variable_defined?(expiration_key) + expire_at = instance_variable_get(expiration_key) + clear_memoization(name) if Time.current > expire_at + end + + if instance_variable_defined?(key) + instance_variable_get(key) + else + value = instance_variable_set(key, yield) + instance_variable_set(expiration_key, Time.current + expire_in) + value + end + end + + def strong_memoize_with(name, *args) + container = strong_memoize(name) { {} } + + if container.key?(args) + container[args] + else + container[args] = yield + end + end + + def strong_memoized?(name) + key = ivar(StrongMemoize.normalize_key(name)) + instance_variable_defined?(key) + end + + def clear_memoization(name) + key = ivar(StrongMemoize.normalize_key(name)) + remove_instance_variable(key) if instance_variable_defined?(key) + end + + module StrongMemoizeClassMethods + def strong_memoize_attr(method_name) + member_name = StrongMemoize.normalize_key(method_name) + + StrongMemoize.send(:do_strong_memoize, self, method_name, member_name) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def self.included(base) + base.singleton_class.prepend(StrongMemoizeClassMethods) + end + + private + + # Convert `"name"`/`:name` into `:@name` + # + # Depending on a type ensure that there's a single memory allocation + def ivar(name) + case name + when Symbol + name.to_s.prepend("@").to_sym + when String + :"@#{name}" + else + raise ArgumentError, "Invalid type of '#{name}'" + end + end + + class << self + def normalize_key(key) + return key unless key.end_with?('!', '?') + + # Replace invalid chars like `!` and `?` with allowed Unicode codeparts. + key.to_s.tr('!?', "\uFF01\uFF1F") + end + + private + + def do_strong_memoize(klass, method_name, member_name) + method = klass.instance_method(method_name) + + unless method.arity == 0 + raise <<~ERROR + Using `strong_memoize_attr` on methods with parameters is not supported. + + Use `strong_memoize_with` instead. + See https://docs.gitlab.com/ee/development/utilities.html#strongmemoize + ERROR + end + + # Methods defined within a class method are already public by default, so we don't need to + # explicitly make them public. + scope = %i[private protected].find do |scope| + klass.send("#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend + .include? method_name + end + + klass.define_method(method_name) do |&block| + strong_memoize(member_name) do + method.bind_call(self, &block) + end + end + + klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end +end diff --git a/gems/gitlab-utils/lib/gitlab/utils/version.rb b/gems/gitlab-utils/lib/gitlab/utils/version.rb new file mode 100644 index 00000000000..a9afe5bf845 --- /dev/null +++ b/gems/gitlab-utils/lib/gitlab/utils/version.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + module Utils + module Version + VERSION = "0.1.0" + end + end +end diff --git a/gems/gitlab-utils/lib/gitlab/version_info.rb b/gems/gitlab-utils/lib/gitlab/version_info.rb new file mode 100644 index 00000000000..0f94aea04ad --- /dev/null +++ b/gems/gitlab-utils/lib/gitlab/version_info.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Gitlab + class VersionInfo + include Comparable + + attr_reader :major, :minor, :patch + + VERSION_REGEX = /(\d+)\.(\d+)\.(\d+)/ + # To mitigate ReDoS, limit the length of the version string we're + # willing to check + MAX_VERSION_LENGTH = 128 + + def self.parse(str, parse_suffix: false) + if str.is_a?(self) + str + elsif str && str.length <= MAX_VERSION_LENGTH && m = str.match(VERSION_REGEX) + VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i, parse_suffix ? m.post_match : nil) + else + VersionInfo.new + end + end + + def initialize(major = 0, minor = 0, patch = 0, suffix = nil) # rubocop:disable Metrics/ParameterLists + @major = major + @minor = minor + @patch = patch + @suffix_s = suffix.to_s + end + + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def <=>(other) + return unless other.is_a? VersionInfo + return unless valid? && other.valid? + + if other.major < @major + 1 + elsif @major < other.major + -1 + elsif other.minor < @minor + 1 + elsif @minor < other.minor + -1 + elsif other.patch < @patch + 1 + elsif @patch < other.patch + -1 + elsif @suffix_s.empty? && other.suffix.present? + 1 + elsif other.suffix.empty? && @suffix_s.present? + -1 + else + suffix <=> other.suffix + end + end + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/PerceivedComplexity + + def to_s + if valid? + "%d.%d.%d%s" % [@major, @minor, @patch, @suffix_s] # rubocop:disable Style/FormatString + else + 'Unknown' + end + end + + def to_json(*_args) + { major: @major, minor: @minor, patch: @patch }.to_json + end + + def suffix + @suffix ||= @suffix_s.strip.gsub('-', '.pre.').scan(/\d+|[a-z]+/i).map do |s| + /^\d+$/ =~ s ? s.to_i : s + end.freeze + end + + def valid? + @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 + end + + def hash + [self.class, to_s].hash + end + + def eql?(other) + (self <=> other) == 0 + end + + def same_minor_version?(other) + @major == other.major && @minor == other.minor + end + + def without_patch + self.class.new(@major, @minor, 0) + end + end +end diff --git a/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb new file mode 100644 index 00000000000..05b25681043 --- /dev/null +++ b/gems/gitlab-utils/spec/gitlab/utils/strong_memoize_spec.rb @@ -0,0 +1,372 @@ +# frozen_string_literal: true + +# rubocop:disable GitlabSecurity/PublicSend + +require 'spec_helper' +require 'active_support/testing/time_helpers' + +RSpec.describe Gitlab::Utils::StrongMemoize, feature_category: :shared do + include ActiveSupport::Testing::TimeHelpers + + let(:klass) do + strong_memoize_class = described_class + + Struct.new(:value) do + include strong_memoize_class + + def self.method_added_list + @method_added_list ||= [] + end + + def self.method_added(name) + method_added_list << name + end + + def method_name + strong_memoize(:method_name) do # rubocop: disable Gitlab/StrongMemoizeAttr + trace << value + value + end + end + + def method_name_with_expiration + strong_memoize_with_expiration(:method_name_with_expiration, 1) do + trace << value + value + end + end + + def method_name_attr + trace << value + value + end + strong_memoize_attr :method_name_attr + + def enabled? + trace << value + value + end + strong_memoize_attr :enabled? + + def method_name_with_args(*args) + strong_memoize_with(:method_name_with_args, args) do + trace << [value, args] + value + end + end + + def trace + @trace ||= [] + end + + protected + + def private_method; end + private :private_method + strong_memoize_attr :private_method + + public + + def protected_method; end + protected :protected_method + strong_memoize_attr :protected_method + + private + + def public_method; end + public :public_method + strong_memoize_attr :public_method + end + end + + subject(:object) { klass.new(value) } + + shared_examples 'caching the value' do + let(:member_name) { described_class.normalize_key(method_name) } + + it 'only calls the block once' do + value0 = object.public_send(method_name) + value1 = object.public_send(method_name) + + expect(value0).to eq(value) + expect(value1).to eq(value) + expect(object.trace).to contain_exactly(value) + end + + it 'returns and defines the instance variable for the exact value' do + returned_value = object.public_send(method_name) + memoized_value = object.instance_variable_get(:"@#{member_name}") + + expect(returned_value).to eql(value) + expect(memoized_value).to eql(value) + end + end + + describe '#strong_memoize' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + let(:method_name) { :method_name } + + it_behaves_like 'caching the value' + + it 'raises exception for invalid type as key' do + expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/ + end + + it 'raises exception for invalid characters in key' do + expect { object.strong_memoize(:enabled?) { 20 } } + .to raise_error /is not allowed as an instance variable name/ + end + end + end + + context "with memory allocation", type: :benchmark do + let(:value) { 'aaa' } + + before do + object.method_name # warmup + end + + [:method_name, "method_name"].each do |argument| + context "for #{argument.class}" do + it 'does allocate exactly one string when fetching value' do + expect do + object.strong_memoize(argument) { 10 } + end.to perform_allocation(1) + end + + it 'does allocate exactly one string when storing value' do + object.clear_memoization(:method_name) # clear to force set + + expect do + object.strong_memoize(argument) { 10 } + end.to perform_allocation(1) + end + end + end + end + end + + describe '#strong_memoize_with_expiration' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + let(:method_name) { :method_name_with_expiration } + + it_behaves_like 'caching the value' + + it 'raises exception for invalid type as key' do + expect { object.strong_memoize_with_expiration(10, 1) { 20 } }.to raise_error /Invalid type of '10'/ + end + + it 'raises exception for invalid characters in key' do + expect { object.strong_memoize_with_expiration(:enabled?, 1) { 20 } } + .to raise_error /is not allowed as an instance variable name/ + end + end + end + + context 'with value memoization test' do + let(:value) { 'value' } + + it 'caches the value for specified number of seconds' do + object.method_name_with_expiration + object.method_name_with_expiration + + expect(object.trace.count).to eq(1) + + travel_to(Time.current + 2.seconds) do + object.method_name_with_expiration + + expect(object.trace.count).to eq(2) + end + end + end + end + + describe '#strong_memoize_with' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value #{value}" do + let(:value) { value } + + it 'only calls the block once' do + value0 = object.method_name_with_args(1) + value1 = object.method_name_with_args(1) + value2 = object.method_name_with_args([2, 3]) + value3 = object.method_name_with_args([2, 3]) + + expect(value0).to eq(value) + expect(value1).to eq(value) + expect(value2).to eq(value) + expect(value3).to eq(value) + + expect(object.trace).to contain_exactly([value, [1]], [value, [[2, 3]]]) + end + + it 'returns and defines the instance variable for the exact value' do + returned_value = object.method_name_with_args(1, 2, 3) + memoized_value = object.instance_variable_get(:@method_name_with_args) + + expect(returned_value).to eql(value) + expect(memoized_value).to eql({ [[1, 2, 3]] => value }) + end + end + end + end + + describe '#strong_memoized?' do + shared_examples 'memoization check' do |method_name| + context "for #{method_name}" do + let(:value) { :anything } + + subject { object.strong_memoized?(method_name) } + + it 'returns false if the value is uncached' do + is_expected.to be(false) + end + + it 'returns true if the value is cached' do + object.public_send(method_name) + + is_expected.to be(true) + end + end + end + + it_behaves_like 'memoization check', :method_name + it_behaves_like 'memoization check', :enabled? + end + + describe '#clear_memoization' do + shared_examples 'clearing memoization' do |method_name| + let(:member_name) { described_class.normalize_key(method_name) } + let(:value) { 'mepmep' } + + it 'removes the instance variable' do + object.public_send(method_name) + + object.clear_memoization(method_name) + + expect(object.instance_variable_defined?(:"@#{member_name}")).to be(false) + end + end + + it_behaves_like 'clearing memoization', :method_name + it_behaves_like 'clearing memoization', :enabled? + end + + describe '.strong_memoize_attr' do + [nil, false, true, 'value', 0, [0]].each do |value| + context "with value '#{value}'" do + let(:value) { value } + + context 'with memoized after method definition' do + let(:method_name) { :method_name_attr } + + it_behaves_like 'caching the value' + + it 'calls the existing .method_added' do + expect(klass.method_added_list).to include(:method_name_attr) + end + + it 'retains method arity' do + expect(klass.instance_method(method_name).arity).to eq(0) + end + end + end + end + + describe 'method visibility' do + it 'sets private visibility' do + expect(klass.private_instance_methods).to include(:private_method) + expect(klass.protected_instance_methods).not_to include(:private_method) + expect(klass.public_instance_methods).not_to include(:private_method) + end + + it 'sets protected visibility' do + expect(klass.private_instance_methods).not_to include(:protected_method) + expect(klass.protected_instance_methods).to include(:protected_method) + expect(klass.public_instance_methods).not_to include(:protected_method) + end + + it 'sets public visibility' do + expect(klass.private_instance_methods).not_to include(:public_method) + expect(klass.protected_instance_methods).not_to include(:public_method) + expect(klass.public_instance_methods).to include(:public_method) + end + end + + context "when method doesn't exist" do + let(:klass) do + strong_memoize_class = described_class + + Struct.new(:value) do + include strong_memoize_class + end + end + + subject { klass.strong_memoize_attr(:nonexistent_method) } + + it 'fails when strong-memoizing a nonexistent method' do + expect { subject }.to raise_error(NameError, %r{undefined method `nonexistent_method' for class}) + end + end + + context 'when memoized method has parameters' do + it 'raises an error' do + expected_message = /Using `strong_memoize_attr` on methods with parameters is not supported/ + + expect do + strong_memoize_class = described_class + + Class.new do + include strong_memoize_class + + def method_with_parameters(params); end + strong_memoize_attr :method_with_parameters + end + end.to raise_error(RuntimeError, expected_message) + end + end + end + + describe '.normalize_key' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.normalize_key(input) } + + where(:input, :output, :valid) do + :key | :key | true + "key" | "key" | true + :key? | "key?" | true + "key?" | "key?" | true + :key! | "key!" | true + "key!" | "key!" | true + # invalid cases caught elsewhere + :"ke?y" | :"ke?y" | false + "ke?y" | "ke?y" | false + :"ke!y" | :"ke!y" | false + "ke!y" | "ke!y" | false + end + + with_them do + let(:ivar) { "@#{output}" } + + it { is_expected.to eq(output) } + + if params[:valid] + it 'is a valid ivar name' do + expect { instance_variable_defined?(ivar) }.not_to raise_error + end + else + it 'raises a NameError error' do + expect { instance_variable_defined?(ivar) } + .to raise_error(NameError, /not allowed as an instance/) + end + end + end + end +end + +# rubocop:enable GitlabSecurity/PublicSend diff --git a/gems/gitlab-utils/spec/gitlab/utils_spec.rb b/gems/gitlab-utils/spec/gitlab/utils_spec.rb new file mode 100644 index 00000000000..53593190eea --- /dev/null +++ b/gems/gitlab-utils/spec/gitlab/utils_spec.rb @@ -0,0 +1,479 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Utils, feature_category: :shared do + using RSpec::Parameterized::TableSyntax + include StubENV + + delegate :to_boolean, :boolean_to_yes_no, :slugify, :which, + :ensure_array_from_string, :bytes_to_megabytes, + :append_path, :remove_leading_slashes, :allowlisted?, + :decode_path, :ms_to_round_sec, to: :described_class + + describe '.allowlisted?' do + let(:allowed_paths) { ['/home/foo', '/foo/bar', '/etc/passwd'] } + + it 'returns true if path is allowed' do + expect(allowlisted?('/foo/bar', allowed_paths)).to be(true) + end + + it 'returns false if path is not allowed' do + expect(allowlisted?('/test/test', allowed_paths)).to be(false) + end + end + + describe '.decode_path' do + it 'returns path unencoded for singled-encoded paths' do + expect(decode_path('%2Fhome%2Fbar%3Fasd%3Dqwe')).to eq('/home/bar?asd=qwe') + end + + it 'returns path when it is unencoded' do + expect(decode_path('/home/bar?asd=qwe')).to eq('/home/bar?asd=qwe') + end + + [ + '..%252F..%252F..%252Fetc%252Fpasswd', + '%25252Fresult%25252Fchosennickname%25253D%252522jj%252522' + ].each do |multiple_encoded_path| + it 'raises an exception when the path is multiple-encoded' do + expect { decode_path(multiple_encoded_path) }.to raise_error(/path #{multiple_encoded_path} is not allowed/) + end + end + end + + describe '.slugify' do + { + 'TEST' => 'test', + 'project_with_underscores' => 'project-with-underscores', + 'namespace/project' => 'namespace-project', + 'a' * 70 => 'a' * 63, + 'test_trailing_' => 'test-trailing' + }.each do |original, expected| + it "slugifies #{original} to #{expected}" do + expect(slugify(original)).to eq(expected) + end + end + end + + describe '.ms_to_round_sec' do + where(:original, :expected) do + 1999.8999 | 1.9999 + 12384 | 12.384 + 333 | 0.333 + 1333.33333333 | 1.333333 + end + + with_them do + it "returns rounded seconds" do + expect(ms_to_round_sec(original)).to eq(expected) + end + end + end + + describe '.nlbr' do + it 'replaces new lines with
' do + expect(described_class.nlbr("hello\nworld")).to eq("hello
world") + end + end + + describe '.remove_line_breaks' do + where(:original, :expected) do + "foo\nbar\nbaz" | "foobarbaz" + "foo\r\nbar\r\nbaz" | "foobarbaz" + "foobar" | "foobar" + end + + with_them do + it "replace line breaks with an empty string" do + expect(described_class.remove_line_breaks(original)).to eq(expected) + end + end + end + + describe '.to_boolean' do + it 'accepts booleans' do + expect(to_boolean(true)).to be(true) + expect(to_boolean(false)).to be(false) + end + + it 'converts a valid value to a boolean' do + expect(to_boolean(true)).to be(true) + expect(to_boolean('true')).to be(true) + expect(to_boolean('YeS')).to be(true) + expect(to_boolean('t')).to be(true) + expect(to_boolean('1')).to be(true) + expect(to_boolean(1)).to be(true) + expect(to_boolean('ON')).to be(true) + + expect(to_boolean('FaLse')).to be(false) + expect(to_boolean('F')).to be(false) + expect(to_boolean('NO')).to be(false) + expect(to_boolean('n')).to be(false) + expect(to_boolean('0')).to be(false) + expect(to_boolean(0)).to be(false) + expect(to_boolean('oFF')).to be(false) + end + + it 'converts an invalid value to nil' do + expect(to_boolean('fals')).to be_nil + expect(to_boolean('yeah')).to be_nil + expect(to_boolean('')).to be_nil + expect(to_boolean(nil)).to be_nil + end + + it 'accepts a default value, and does not return it when a valid value is given' do + expect(to_boolean(true, default: false)).to be(true) + expect(to_boolean('true', default: false)).to be(true) + expect(to_boolean('YeS', default: false)).to be(true) + expect(to_boolean('t', default: false)).to be(true) + expect(to_boolean('1', default: 'any value')).to be(true) + expect(to_boolean('ON', default: 42)).to be(true) + + expect(to_boolean('FaLse', default: true)).to be(false) + expect(to_boolean('F', default: true)).to be(false) + expect(to_boolean('NO', default: true)).to be(false) + expect(to_boolean('n', default: true)).to be(false) + expect(to_boolean('0', default: 'any value')).to be(false) + expect(to_boolean('oFF', default: 42)).to be(false) + end + + it 'accepts a default value, and returns it when an invalid value is given' do + expect(to_boolean('fals', default: true)).to eq(true) + expect(to_boolean('yeah', default: false)).to eq(false) + expect(to_boolean('', default: 'any value')).to eq('any value') + expect(to_boolean(nil, default: 42)).to eq(42) + end + end + + describe '.boolean_to_yes_no' do + it 'converts booleans to Yes or No' do + expect(boolean_to_yes_no(true)).to eq('Yes') + expect(boolean_to_yes_no(false)).to eq('No') + end + end + + describe '.which' do + before do + stub_env('PATH', '/sbin:/usr/bin:/home/joe/bin') + end + + it 'finds the full path to an executable binary in order of appearance' do + expect(File).to receive(:executable?).with('/sbin/tool').ordered.and_return(false) + expect(File).to receive(:executable?).with('/usr/bin/tool').ordered.and_return(true) + expect(File).not_to receive(:executable?).with('/home/joe/bin/tool') + + expect(which('tool')).to eq('/usr/bin/tool') + end + end + + describe '.ensure_array_from_string' do + it 'returns the same array if given one' do + arr = ['a', 4, true, { test: 1 }] + + expect(ensure_array_from_string(arr)).to eq(arr) + end + + it 'turns comma-separated strings into arrays' do + str = 'seven, eight, 9, 10' + + expect(ensure_array_from_string(str)).to eq(%w[seven eight 9 10]) + end + end + + describe '.bytes_to_megabytes' do + it 'converts bytes to megabytes' do + bytes = 1.megabyte + + expect(bytes_to_megabytes(bytes)).to eq(1) + end + end + + describe '.append_path' do + where(:host, :path, :result) do + 'http://test/' | '/foo/bar' | 'http://test/foo/bar' + 'http://test/' | '//foo/bar' | 'http://test/foo/bar' + 'http://test//' | '/foo/bar' | 'http://test/foo/bar' + 'http://test' | 'foo/bar' | 'http://test/foo/bar' + 'http://test//' | '' | 'http://test/' + 'http://test//' | nil | 'http://test/' + '' | '/foo/bar' | '/foo/bar' + nil | '/foo/bar' | '/foo/bar' + end + + with_them do + it 'makes sure there is only one slash as path separator' do + expect(append_path(host, path)).to eq(result) + end + end + end + + describe '.remove_leading_slashes' do + where(:str, :result) do + '/foo/bar' | 'foo/bar' + '//foo/bar' | 'foo/bar' + '/foo/bar/' | 'foo/bar/' + 'foo/bar' | 'foo/bar' + '' | '' + nil | '' + end + + with_them do + it 'removes leading slashes' do + expect(remove_leading_slashes(str)).to eq(result) + end + end + end + + describe '.ensure_utf8_size' do + context 'with string is has less bytes than expected' do + it 'backfills string with null characters' do + transformed = described_class.ensure_utf8_size('a' * 10, bytes: 32) + + expect(transformed.bytesize).to eq 32 + expect(transformed).to eq(('a' * 10) + ('0' * 22)) + end + end + + context 'with string size is exactly the one that is expected' do + it 'returns original value' do + transformed = described_class.ensure_utf8_size('a' * 32, bytes: 32) + + expect(transformed).to eq 'a' * 32 + expect(transformed.bytesize).to eq 32 + end + end + + context 'when string contains a few multi-byte UTF characters' do + it 'backfills string with null characters' do + transformed = described_class.ensure_utf8_size('❤' * 6, bytes: 32) + + expect(transformed).to eq '❤❤❤❤❤❤' + ('0' * 14) # rubocop:disable Style/StringConcatenation + expect(transformed.bytesize).to eq 32 + end + end + + context 'when string has multiple multi-byte UTF chars exceeding 32 bytes' do + it 'truncates string to 32 characters and backfills it if needed' do + transformed = described_class.ensure_utf8_size('❤' * 18, bytes: 32) + + expect(transformed).to eq(('❤' * 10) + ('0' * 2)) + expect(transformed.bytesize).to eq 32 + end + end + end + + describe '.deep_indifferent_access' do + let(:hash) do + { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] } + end + + subject { described_class.deep_indifferent_access(hash) } + + it 'allows to access hash keys with symbols' do + expect(subject[:variables]).to be_a(Array) + end + + it 'allows to access array keys with symbols' do + expect(subject[:variables].first[:key]).to eq('VAR1') + end + end + + describe '.deep_symbolized_access' do + let(:hash) do + { "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] } + end + + subject { described_class.deep_symbolized_access(hash) } + + it 'allows to access hash keys with symbols' do + expect(subject[:variables]).to be_a(Array) + end + + it 'allows to access array keys with symbols' do + expect(subject[:variables].first[:key]).to eq('VAR1') + end + end + + describe '.try_megabytes_to_bytes' do + context 'when the size can be converted to megabytes' do + it 'returns the size in megabytes' do + size = described_class.try_megabytes_to_bytes(1) + + expect(size).to eq(1.megabytes) + end + end + + context 'when the size can not be converted to megabytes' do + it 'returns the input size' do + size = described_class.try_megabytes_to_bytes('foo') + + expect(size).to eq('foo') + end + end + end + + describe '.string_to_ip_object' do + it 'returns nil when string is nil' do + expect(described_class.string_to_ip_object(nil)).to eq(nil) + end + + it 'returns nil when string is invalid IP' do + expect(described_class.string_to_ip_object('invalid ip')).to eq(nil) + expect(described_class.string_to_ip_object('')).to eq(nil) + end + + it 'returns IP object when string is valid IP' do + expect(described_class.string_to_ip_object('192.168.1.1')).to eq(IPAddr.new('192.168.1.1')) + expect(described_class.string_to_ip_object('::ffff:a9fe:a864')).to eq(IPAddr.new('::ffff:a9fe:a864')) + expect(described_class.string_to_ip_object('[::ffff:a9fe:a864]')).to eq(IPAddr.new('::ffff:a9fe:a864')) + expect(described_class.string_to_ip_object('127.0.0.0/28')).to eq(IPAddr.new('127.0.0.0/28')) + expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124')) + end + end + + describe ".safe_downcase!" do + where(:str, :result) do + "test" | "test" + "Test" | "test" + "test" | "test" + "Test" | "test" + end + + with_them do + it "downcases the string" do + expect(described_class.safe_downcase!(str)).to eq(result) + end + end + end + + describe '.parse_url' do + it 'returns Addressable::URI object' do + expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI) + end + + it 'returns nil when URI cannot be parsed' do + expect(described_class.parse_url('://gitlab.com')).to be nil + end + + it 'returns nil with invalid parameter' do + expect(described_class.parse_url(1)).to be nil + end + end + + describe '.add_url_parameters' do + subject { described_class.add_url_parameters(url, params) } + + where(:url, :params, :expected_url) do + nil | nil | '' + nil | { b: 3, a: 2 } | '?a=2&b=3' + 'https://gitlab.com' | nil | 'https://gitlab.com' + 'https://gitlab.com' | { b: 3, a: 2 } | 'https://gitlab.com?a=2&b=3' + 'https://gitlab.com?a=1#foo' | { b: 3, 'a' => 2 } | 'https://gitlab.com?a=2&b=3#foo' + 'https://gitlab.com?a=1#foo' | [[:b, 3], [:a, 2]] | 'https://gitlab.com?a=2&b=3#foo' + end + + with_them do + it { is_expected.to eq(expected_url) } + end + end + + describe '.removes_sensitive_data_from_url' do + it 'returns string object' do + expect(described_class.removes_sensitive_data_from_url('http://gitlab.com')).to be_instance_of(String) + end + + it 'returns nil when URI cannot be parsed' do + expect(described_class.removes_sensitive_data_from_url('://gitlab.com')).to be nil + end + + it 'returns nil with invalid parameter' do + expect(described_class.removes_sensitive_data_from_url(1)).to be nil + end + + it 'returns string with filtered access_token param' do + expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token')) + .to eq('http://gitlab.com/auth.html#access_token=filtered') + end + + it 'returns string with filtered access_token param but other params preserved' do + expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token&token_type=Bearer&state=test')) + .to include('&token_type=Bearer', '&state=test') + end + end + + describe 'multiple_key_invert' do + it 'invert keys with array values' do + hash = { + dast: [:vulnerabilities_count, :scanned_resources_count], + sast: [:vulnerabilities_count] + } + expect(described_class.multiple_key_invert(hash)).to eq({ + vulnerabilities_count: [:dast, :sast], + scanned_resources_count: [:dast] + }) + end + end + + describe '.stable_sort_by' do + subject(:sorted_list) { described_class.stable_sort_by(list) { |obj| obj[:priority] } } + + context 'when items have the same priority' do + let(:list) do + [ + { name: 'obj 1', priority: 1 }, + { name: 'obj 2', priority: 1 }, + { name: 'obj 3', priority: 1 } + ] + end + + it 'does not change order in cases of ties' do + expect(sorted_list).to eq(list) + end + end + + context 'when items have different priorities' do + let(:list) do + [ + { name: 'obj 1', priority: 2 }, + { name: 'obj 2', priority: 1 }, + { name: 'obj 3', priority: 3 } + ] + end + + it 'sorts items like the regular sort_by' do + expect(sorted_list).to eq( + [ + { name: 'obj 2', priority: 1 }, + { name: 'obj 1', priority: 2 }, + { name: 'obj 3', priority: 3 } + ]) + end + end + end + + describe '.valid_brackets?' do + where(:input, :allow_nested, :valid) do + 'no brackets' | true | true + 'no brackets' | false | true + 'user[avatar]' | true | true + 'user[avatar]' | false | true + 'user[avatar][friends]' | true | true + 'user[avatar][friends]' | false | true + 'user[avatar[image[url]]]' | true | true + 'user[avatar[image[url]]]' | false | false + 'user[avatar[]friends]' | true | true + 'user[avatar[]friends]' | false | false + 'user[avatar]]' | true | false + 'user[avatar]]' | false | false + 'user][avatar]]' | true | false + 'user][avatar]]' | false | false + 'user[avatar' | true | false + 'user[avatar' | false | false + end + + with_them do + it { expect(described_class.valid_brackets?(input, allow_nested: allow_nested)).to eq(valid) } + end + end +end diff --git a/gems/gitlab-utils/spec/gitlab/version_info_spec.rb b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb new file mode 100644 index 00000000000..2b5f6bcb4c1 --- /dev/null +++ b/gems/gitlab-utils/spec/gitlab/version_info_spec.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::VersionInfo, feature_category: :shared do + before do + @unknown = described_class.new + @v0_0_1 = described_class.new(0, 0, 1) + @v0_1_0 = described_class.new(0, 1, 0) + @v1_0_0 = described_class.new(1, 0, 0) + @v1_0_1 = described_class.new(1, 0, 1) + @v1_0_1_b1 = described_class.new(1, 0, 1, '-b1') + @v1_0_1_rc1 = described_class.new(1, 0, 1, '-rc1') + @v1_0_1_rc2 = described_class.new(1, 0, 1, '-rc2') + @v1_1_0 = described_class.new(1, 1, 0) + @v1_1_0_beta1 = described_class.new(1, 1, 0, '-beta1') + @v2_0_0 = described_class.new(2, 0, 0) + @v13_10_1_1574_89 = described_class.parse("v13.10.1~beta.1574.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_89 = described_class.parse("v13.10.1~beta.1575.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_90 = described_class.parse("v13.10.1~beta.1575.gf6ea9390", parse_suffix: true) + end + + describe '>' do + it { expect(@v2_0_0).to be > @v1_1_0 } + it { expect(@v1_1_0).to be > @v1_0_1 } + it { expect(@v1_0_1_b1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_1_b1 } + it { expect(@v1_0_1_rc2).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc2 } + it { expect(@v1_0_1).to be > @v1_0_0 } + it { expect(@v1_0_0).to be > @v0_1_0 } + it { expect(@v1_1_0_beta1).to be > @v1_0_1_rc2 } + it { expect(@v1_1_0).to be > @v1_1_0_beta1 } + it { expect(@v0_1_0).to be > @v0_0_1 } + end + + describe '>=' do + it { expect(@v2_0_0).to be >= described_class.new(2, 0, 0) } + it { expect(@v2_0_0).to be >= @v1_1_0 } + it { expect(@v1_0_1_rc2).to be >= @v1_0_1_rc1 } + end + + describe '<' do + it { expect(@v0_0_1).to be < @v0_1_0 } + it { expect(@v0_1_0).to be < @v1_0_0 } + it { expect(@v1_0_0).to be < @v1_0_1 } + it { expect(@v1_0_1).to be < @v1_1_0 } + it { expect(@v1_0_0).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc2).to be < @v1_0_1 } + it { expect(@v1_1_0).to be < @v2_0_0 } + it { expect(@v13_10_1_1574_89).to be < @v13_10_1_1575_89 } + it { expect(@v13_10_1_1575_89).to be < @v13_10_1_1575_90 } + end + + describe '<=' do + it { expect(@v0_0_1).to be <= described_class.new(0, 0, 1) } + it { expect(@v0_0_1).to be <= @v0_1_0 } + it { expect(@v1_0_1_b1).to be <= @v1_0_1_rc1 } + it { expect(@v1_0_1_rc1).to be <= @v1_0_1_rc2 } + it { expect(@v1_1_0_beta1).to be <= @v1_1_0 } + end + + describe '==' do + it { expect(@v0_0_1).to eq(described_class.new(0, 0, 1)) } + it { expect(@v0_1_0).to eq(described_class.new(0, 1, 0)) } + it { expect(@v1_0_0).to eq(described_class.new(1, 0, 0)) } + it { expect(@v1_0_1_rc1).to eq(described_class.new(1, 0, 1, '-rc1')) } + end + + describe '!=' do + it { expect(@v0_0_1).not_to eq(@v0_1_0) } + it { expect(@v1_0_1_rc1).not_to eq(@v1_0_1_rc2) } + end + + describe '.unknown' do + it { expect(@unknown).not_to be @v0_0_1 } + it { expect(@unknown).not_to be described_class.new } + it { expect { @unknown > @v0_0_1 }.to raise_error(ArgumentError) } + it { expect { @unknown < @v0_0_1 }.to raise_error(ArgumentError) } + end + + describe '.parse' do + it { expect(described_class.parse(described_class.new(1, 0, 0))).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0.0b1")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0b1")).not_to be_valid } + it { expect(described_class.parse("1.1.#{'1' * described_class::MAX_VERSION_LENGTH}")).not_to be_valid } + it { expect(described_class.parse(nil)).not_to be_valid } + + context 'with parse_suffix: true' do + let(:versions) do + <<-VERSIONS.lines + 0.0.1 + 0.1.0 + 1.0.0 + 1.0.1-b1 + 1.0.1-rc1 + 1.0.1-rc2 + 1.0.1 + 1.1.0-beta1 + 1.1.0 + 2.0.0 + v13.10.0-pre + v13.10.0-rc1 + v13.10.0-rc2 + v13.10.0 + v13.10.1~beta.1574.gf6ea9389 + v13.10.1~beta.1575.gf6ea9389 + v13.10.1-rc1 + v13.10.1-rc2 + v13.10.1 + VERSIONS + end + + let(:parsed_versions) do + versions.map(&:strip).map { |version| described_class.parse(version, parse_suffix: true) } + end + + it 'versions are returned in a correct order' do + expect(parsed_versions.shuffle.sort).to eq(parsed_versions) + end + end + end + + describe '.to_s' do + it { expect(@v1_0_0.to_s).to eq("1.0.0") } + it { expect(@v1_0_1_rc1.to_s).to eq("1.0.1-rc1") } + it { expect(@unknown.to_s).to eq("Unknown") } + end + + describe '.to_json' do + let(:correct_version) do + "{\"major\":1,\"minor\":0,\"patch\":1}" + end + + let(:unknown_version) do + "{\"major\":0,\"minor\":0,\"patch\":0}" + end + + it { expect(@v1_0_1.to_json).to eq(correct_version) } + it { expect(@v1_0_1_rc2.to_json).to eq(correct_version) } + it { expect(@unknown.to_json).to eq(unknown_version) } + end + + describe '.hash' do + it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) } + it { expect(described_class.parse("1.0.1-rc1", parse_suffix: true).hash).to eq(@v1_0_1_rc1.hash) } + end + + describe '.eql?' do + it { expect(described_class.parse("1.0.0").eql?(@v1_0_0)).to be_truthy } + it { expect(described_class.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc1)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc2)).to be_falsey } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1)).to be_falsey } + it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey } + it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy } + it { expect([@v1_0_0, @v1_1_0, @v1_0_0, @v1_0_1_rc1, @v1_0_1_rc1].uniq).to eq [@v1_0_0, @v1_1_0, @v1_0_1_rc1] } + end + + describe '.same_minor_version?' do + it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey } + it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy } + it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey } + it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey } + end + + describe '.without_patch' do + it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) } + it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1_rc1.without_patch).to eq(@v1_0_0) } + end + + describe 'MAX_VERSION_LENGTH' do + subject { described_class::MAX_VERSION_LENGTH } + + it { is_expected.to eq(128) } + end +end diff --git a/gems/gitlab-utils/spec/spec_helper.rb b/gems/gitlab-utils/spec/spec_helper.rb new file mode 100644 index 00000000000..5dc3859f77d --- /dev/null +++ b/gems/gitlab-utils/spec/spec_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails' +require 'rspec/mocks' +require 'rspec-benchmark' +require 'rspec-parameterized' + +require 'gitlab/rspec/all' +require 'gitlab/utils/all' + +RSpec.configure do |config| + config.include RSpec::Benchmark::Matchers + + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb index b39d2a02f02..c6ce0aa6160 100644 --- a/lib/gitlab/cluster/lifecycle_events.rb +++ b/lib/gitlab/cluster/lifecycle_events.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../utils' # Gitlab::Utils +require 'gitlab/utils/all' # Gitlab::Utils module Gitlab module Cluster diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index c4566a6dc2a..ca52d588113 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -87,6 +87,7 @@ module Gitlab payload['message'] = "#{message}: #{job_status}: #{payload['duration_s']} sec" payload['job_status'] = job_status payload['job_deferred_by'] = job['deferred_by'] if job['deferred'] + payload['deferred_count'] = job['deferred_count'] if job['deferred'] Gitlab::ExceptionLogFormatter.format!(job_exception, payload) if job_exception diff --git a/lib/gitlab/sidekiq_middleware/defer_jobs.rb b/lib/gitlab/sidekiq_middleware/defer_jobs.rb index 0a12667865c..6a652f39349 100644 --- a/lib/gitlab/sidekiq_middleware/defer_jobs.rb +++ b/lib/gitlab/sidekiq_middleware/defer_jobs.rb @@ -22,6 +22,8 @@ module Gitlab # Referred in job_logger's 'log_job_done' method to compute proper 'job_status' job['deferred'] = true job['deferred_by'] = deferred_by + job['deferred_count'] ||= 0 + job['deferred_count'] += 1 worker.class.perform_in(delay, *job['args']) counter.increment({ worker: worker.class.name }) diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb index b9800a4db73..f756d229ba1 100644 --- a/lib/gitlab/task_helpers.rb +++ b/lib/gitlab/task_helpers.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rainbow/ext/string' -require_relative 'utils/strong_memoize' +require 'gitlab/utils/all' # rubocop:disable Rails/Output module Gitlab diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb deleted file mode 100644 index dc0112c14d6..00000000000 --- a/lib/gitlab/utils.rb +++ /dev/null @@ -1,259 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Utils - extend self - DoubleEncodingError ||= Class.new(StandardError) - - def allowlisted?(absolute_path, allowlist) - path = absolute_path.downcase - - allowlist.map(&:downcase).any? do |allowed_path| - path.start_with?(allowed_path) - end - end - - def decode_path(encoded_path) - decoded = CGI.unescape(encoded_path) - if decoded != CGI.unescape(decoded) - raise DoubleEncodingError, "path #{encoded_path} is not allowed" - end - - decoded - end - - def force_utf8(str) - str.dup.force_encoding(Encoding::UTF_8) - end - - def ensure_utf8_size(str, bytes:) - raise ArgumentError, 'Empty string provided!' if str.empty? - raise ArgumentError, 'Negative string size provided!' if bytes < 0 - - truncated = str.each_char.each_with_object(+'') do |char, object| - if object.bytesize + char.bytesize > bytes - break object - else - object.concat(char) - end - end - - truncated + ('0' * (bytes - truncated.bytesize)) - end - - # Append path to host, making sure there's one single / in between - def append_path(host, path) - "#{host.to_s.sub(%r{\/+$}, '')}/#{remove_leading_slashes(path)}" - end - - def remove_leading_slashes(str) - str.to_s.sub(%r{^/+}, '') - end - - # A slugified version of the string, suitable for inclusion in URLs and - # domain names. Rules: - # - # * Lowercased - # * Anything not matching [a-z0-9-] is replaced with a - - # * Maximum length is 63 bytes - # * First/Last Character is not a hyphen - def slugify(str) - str.downcase - .gsub(/[^a-z0-9]/, '-')[0..62] - .gsub(/(\A-+|-+\z)/, '') - end - - # Converts newlines into HTML line break elements - def nlbr(str) - ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '
').html_safe - end - - def remove_line_breaks(str) - str.gsub(/\r?\n/, '') - end - - def to_boolean(value, default: nil) - value = value.to_s if [0, 1].include?(value) - - return value if [true, false].include?(value) - return true if value =~ /^(true|t|yes|y|1|on)$/i - return false if value =~ /^(false|f|no|n|0|off)$/i - - default - end - - def boolean_to_yes_no(bool) - if bool - 'Yes' - else - 'No' - end - end - - # Behaves like `which` on Linux machines: given PATH, try to resolve the given - # executable name to an absolute path, or return nil. - # - # which('ruby') #=> /usr/bin/ruby - def which(filename) - ENV['PATH']&.split(File::PATH_SEPARATOR)&.each do |path| - full_path = File.join(path, filename) - return full_path if File.executable?(full_path) - end - - nil - end - - def try_megabytes_to_bytes(size) - Integer(size).megabytes - rescue ArgumentError - size - end - - def bytes_to_megabytes(bytes) - bytes.to_f / Numeric::MEGABYTE - end - - def ms_to_round_sec(ms) - (ms.to_f / 1000).round(6) - end - - # Used in EE - # Accepts either an Array or a String and returns an array - def ensure_array_from_string(string_or_array) - return string_or_array if string_or_array.is_a?(Array) - - string_or_array.split(',').map(&:strip) - end - - def deep_indifferent_access(data) - case data - when Array - data.map(&method(:deep_indifferent_access)) - when Hash - data.with_indifferent_access - else - data - end - end - - def deep_symbolized_access(data) - case data - when Array - data.map(&method(:deep_symbolized_access)) - when Hash - data.deep_symbolize_keys - else - data - end - end - - def string_to_ip_object(str) - return unless str - - IPAddr.new(str) - rescue IPAddr::InvalidAddressError - end - - # A safe alternative to String#downcase! - # - # This will make copies of frozen strings but downcase unfrozen - # strings in place, reducing allocations. - def safe_downcase!(str) - if str.frozen? - str.downcase - else - str.downcase! || str - end - end - - # Converts a string to an Addressable::URI object. - # If the string is not a valid URI, it returns nil. - # Param uri_string should be a String object. - # This method returns an Addressable::URI object or nil. - def parse_url(uri_string) - Addressable::URI.parse(uri_string) - rescue Addressable::URI::InvalidURIError, TypeError - end - - def add_url_parameters(url, params) - uri = parse_url(url.to_s) - uri.query_values = uri.query_values.to_h.merge(params.to_h.stringify_keys) - uri.query_values = nil if uri.query_values.empty? - uri.to_s - end - - def removes_sensitive_data_from_url(uri_string) - uri = parse_url(uri_string) - - return unless uri - return uri_string unless uri.fragment - - stripped_params = CGI.parse(uri.fragment) - if stripped_params['access_token'] - stripped_params['access_token'] = 'filtered' - filtered_query = Addressable::URI.new - filtered_query.query_values = stripped_params - - uri.fragment = filtered_query.query - end - - uri.to_s - end - - # Invert a hash, collecting all keys that map to a given value in an array. - # - # Unlike `Hash#invert`, where the last encountered pair wins, and which has the - # type `Hash[k, v] => Hash[v, k]`, `multiple_key_invert` does not lose any - # information, has the type `Hash[k, v] => Hash[v, Array[k]]`, and the original - # hash can always be reconstructed. - # - # example: - # - # multiple_key_invert({ a: 1, b: 2, c: 1 }) - # # => { 1 => [:a, :c], 2 => [:b] } - # - def multiple_key_invert(hash) - hash.flat_map { |k, v| Array.wrap(v).zip([k].cycle) } - .group_by(&:first) - .transform_values { |kvs| kvs.map(&:last) } - end - - # This sort is stable (see https://en.wikipedia.org/wiki/Sorting_algorithm#Stability) - # contrary to the bare Ruby sort_by method. Using just sort_by leads to - # instability across different platforms (e.g., x86_64-linux and x86_64-darwin18) - # which in turn leads to different sorting results for the equal elements across - # these platforms. - # This method uses a list item's original index position to break ties. - def stable_sort_by(list) - list.sort_by.with_index { |x, idx| [yield(x), idx] } - end - - # Check for valid brackets (`[` and `]`) in a string using this aspects: - # * open brackets count == closed brackets count - # * (optionally) reject nested brackets via `allow_nested: false` - # * open / close brackets coherence, eg. ][[] -> invalid - def valid_brackets?(string = '', allow_nested: true) - # remove everything except brackets - brackets = string.remove(/[^\[\]]/) - - return true if brackets.empty? - # balanced counts check - return false if brackets.size.odd? - - unless allow_nested - # nested brackets check - return false if brackets.include?('[[') || brackets.include?(']]') - end - - # open / close brackets coherence check - untrimmed = brackets - loop do - trimmed = untrimmed.gsub('[]', '') - return true if trimmed.empty? - return false if trimmed == untrimmed - - untrimmed = trimmed - end - end - end -end diff --git a/lib/gitlab/utils/override.rb b/lib/gitlab/utils/override.rb index 1d02bcbb2d2..10370811bb5 100644 --- a/lib/gitlab/utils/override.rb +++ b/lib/gitlab/utils/override.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../utils' +require 'gitlab/utils/all' require_relative '../environment' module Gitlab diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb deleted file mode 100644 index 2b3841b8f09..00000000000 --- a/lib/gitlab/utils/strong_memoize.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Utils - module StrongMemoize - # Instead of writing patterns like this: - # - # def trigger_from_token - # return @trigger if defined?(@trigger) - # - # @trigger = Ci::Trigger.find_by_token(params[:token].to_s) - # end - # - # We could write it like: - # - # include Gitlab::Utils::StrongMemoize - # - # def trigger_from_token - # Ci::Trigger.find_by_token(params[:token].to_s) - # end - # strong_memoize_attr :trigger_from_token - # - # def enabled? - # Feature.enabled?(:some_feature) - # end - # strong_memoize_attr :enabled? - # - def strong_memoize(name) - key = ivar(name) - - if instance_variable_defined?(key) - instance_variable_get(key) - else - instance_variable_set(key, yield) - end - end - - # Works the same way as "strong_memoize" but takes - # a second argument - expire_in. This allows invalidate - # the data after specified number of seconds - def strong_memoize_with_expiration(name, expire_in) - key = ivar(name) - expiration_key = "#{key}_expired_at" - - if instance_variable_defined?(expiration_key) - expire_at = instance_variable_get(expiration_key) - clear_memoization(name) if Time.current > expire_at - end - - if instance_variable_defined?(key) - instance_variable_get(key) - else - value = instance_variable_set(key, yield) - instance_variable_set(expiration_key, Time.current + expire_in) - value - end - end - - def strong_memoize_with(name, *args) - container = strong_memoize(name) { {} } - - if container.key?(args) - container[args] - else - container[args] = yield - end - end - - def strong_memoized?(name) - key = ivar(StrongMemoize.normalize_key(name)) - instance_variable_defined?(key) - end - - def clear_memoization(name) - key = ivar(StrongMemoize.normalize_key(name)) - remove_instance_variable(key) if instance_variable_defined?(key) - end - - module StrongMemoizeClassMethods - def strong_memoize_attr(method_name) - member_name = StrongMemoize.normalize_key(method_name) - - StrongMemoize.send(:do_strong_memoize, self, method_name, member_name) # rubocop:disable GitlabSecurity/PublicSend - end - end - - def self.included(base) - base.singleton_class.prepend(StrongMemoizeClassMethods) - end - - private - - # Convert `"name"`/`:name` into `:@name` - # - # Depending on a type ensure that there's a single memory allocation - def ivar(name) - case name - when Symbol - name.to_s.prepend("@").to_sym - when String - :"@#{name}" - else - raise ArgumentError, "Invalid type of '#{name}'" - end - end - - class << self - def normalize_key(key) - return key unless key.end_with?('!', '?') - - # Replace invalid chars like `!` and `?` with allowed Unicode codeparts. - key.to_s.tr('!?', "\uFF01\uFF1F") - end - - private - - def do_strong_memoize(klass, method_name, member_name) - method = klass.instance_method(method_name) - - unless method.arity == 0 - raise <<~ERROR - Using `strong_memoize_attr` on methods with parameters is not supported. - - Use `strong_memoize_with` instead. - See https://docs.gitlab.com/ee/development/utilities.html#strongmemoize - ERROR - end - - # Methods defined within a class method are already public by default, so we don't need to - # explicitly make them public. - scope = %i[private protected].find do |scope| - klass.send("#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend - .include? method_name - end - - klass.define_method(method_name) do |&block| - strong_memoize(member_name) do - method.bind_call(self, &block) - end - end - - klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend - end - end - end - end -end diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb deleted file mode 100644 index 0351c9b30b3..00000000000 --- a/lib/gitlab/version_info.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - class VersionInfo - include Comparable - - attr_reader :major, :minor, :patch - - VERSION_REGEX = /(\d+)\.(\d+)\.(\d+)/.freeze - # To mitigate ReDoS, limit the length of the version string we're - # willing to check - MAX_VERSION_LENGTH = 128 - - def self.parse(str, parse_suffix: false) - if str.is_a?(self) - str - elsif str && str.length <= MAX_VERSION_LENGTH && m = str.match(VERSION_REGEX) - VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i, parse_suffix ? m.post_match : nil) - else - VersionInfo.new - end - end - - def initialize(major = 0, minor = 0, patch = 0, suffix = nil) - @major = major - @minor = minor - @patch = patch - @suffix_s = suffix.to_s - end - - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def <=>(other) - return unless other.is_a? VersionInfo - return unless valid? && other.valid? - - if other.major < @major - 1 - elsif @major < other.major - -1 - elsif other.minor < @minor - 1 - elsif @minor < other.minor - -1 - elsif other.patch < @patch - 1 - elsif @patch < other.patch - -1 - elsif @suffix_s.empty? && other.suffix.present? - 1 - elsif other.suffix.empty? && @suffix_s.present? - -1 - else - suffix <=> other.suffix - end - end - # rubocop:enable Metrics/CyclomaticComplexity - # rubocop:enable Metrics/PerceivedComplexity - - def to_s - if valid? - "%d.%d.%d%s" % [@major, @minor, @patch, @suffix_s] - else - 'Unknown' - end - end - - def to_json(*_args) - { major: @major, minor: @minor, patch: @patch }.to_json - end - - def suffix - @suffix ||= @suffix_s.strip.gsub('-', '.pre.').scan(/\d+|[a-z]+/i).map do |s| - /^\d+$/ =~ s ? s.to_i : s - end.freeze - end - - def valid? - @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 - end - - def hash - [self.class, to_s].hash - end - - def eql?(other) - (self <=> other) == 0 - end - - def same_minor_version?(other) - @major == other.major && @minor == other.minor - end - - def without_patch - self.class.new(@major, @minor, 0) - end - end -end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index 825388461bc..1a659a930ab 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -42,7 +42,7 @@ namespace :gettext do desc 'Lint all po files in `locale/' task lint: :environment do require 'simple_po_parser' - require 'gitlab/utils' + require 'gitlab/utils/all' require 'parallel' FastGettext.silence_errors diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a09096b6b92..e2b72fb0123 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1120,6 +1120,12 @@ msgstr "" msgid "%{spanStart}in%{spanEnd} %{errorFn}" msgstr "" +msgid "%{startDate} – %{dueDate}" +msgstr "" + +msgid "%{startDate} – No due date" +msgstr "" + msgid "%{start} to %{end}" msgstr "" @@ -17714,12 +17720,6 @@ msgstr "" msgid "Epics, issues, and merge requests" msgstr "" -msgid "Epics|%{startDate} – %{dueDate}" -msgstr "" - -msgid "Epics|%{startDate} – No due date" -msgstr "" - msgid "Epics|Add a new epic" msgstr "" @@ -17735,9 +17735,6 @@ msgstr "" msgid "Epics|Leave empty to inherit from milestone dates" msgstr "" -msgid "Epics|No start date – %{dueDate}" -msgstr "" - msgid "Epics|Remove epic" msgstr "" @@ -30792,6 +30789,9 @@ msgstr "" msgid "No starrers matched your search" msgstr "" +msgid "No start date – %{dueDate}" +msgstr "" + msgid "No suggestions found" msgstr "" diff --git a/metrics_server/dependencies.rb b/metrics_server/dependencies.rb index 233511eb505..c96fecd7cb7 100644 --- a/metrics_server/dependencies.rb +++ b/metrics_server/dependencies.rb @@ -11,11 +11,11 @@ require 'active_support/core_ext/numeric/bytes' require 'prometheus/client' require 'rack' +require 'gitlab/utils/all' + require_relative 'settings_overrides' require_relative '../lib/gitlab/daemon' -require_relative '../lib/gitlab/utils' -require_relative '../lib/gitlab/utils/strong_memoize' require_relative '../lib/prometheus/cleanup_multiproc_dir_service' require_relative '../lib/gitlab/metrics/prometheus' require_relative '../lib/gitlab/metrics' diff --git a/qa/Dockerfile b/qa/Dockerfile index e5308d78f83..213ec3450cb 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -40,6 +40,8 @@ WORKDIR /home/gitlab/qa # Install qa dependencies or fetch from cache if unchanged # COPY ./qa/Gemfile* /home/gitlab/qa/ +COPY ./vendor/gems/ /home/gitlab/vendor/gems/ +COPY ./gems/ /home/gitlab/gems/ RUN bundle config set --local without development \ && bundle install --retry=3 @@ -47,9 +49,7 @@ COPY ./config/initializers/0_inject_enterprise_edition_module.rb /home/gitlab/co COPY ./config/feature_flags /home/gitlab/config/feature_flags COPY ./config/bundler_setup.rb /home/gitlab/config/ COPY ./lib/gitlab_edition.rb /home/gitlab/lib/ -COPY ./lib/gitlab/utils.rb /home/gitlab/lib/gitlab/ COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/ -COPY ./gems /home/gitlab/ COPY ./qa /home/gitlab/qa diff --git a/qa/Gemfile b/qa/Gemfile index c907240333f..2a7f221f91f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -4,6 +4,7 @@ source 'https://rubygems.org' gem 'gitlab-qa', '~> 11', '>= 11.2.0', require: 'gitlab/qa' gem 'gitlab_quality-test_tooling', '~> 0.8.2', require: false +gem 'gitlab-utils', path: '../gems/gitlab-utils' gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.20.0' gem 'capybara', '~> 3.39.2' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 94eae5f80bb..861bb1e0def 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -1,6 +1,22 @@ +PATH + remote: ../gems/gitlab-utils + specs: + gitlab-utils (0.1.0) + actionview (>= 6.1.7.2) + activesupport (>= 6.1.7.2) + addressable (~> 2.8) + nokogiri (~> 1.15.2) + rake (~> 13.0) + GEM remote: https://rubygems.org/ specs: + actionview (6.1.7.2) + activesupport (= 6.1.7.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) activesupport (6.1.7.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -55,6 +71,7 @@ GEM confiner (0.4.0) gitlab (>= 4.17) zeitwerk (>= 2.5, < 3) + crass (1.0.6) debug_inspector (1.1.0) declarative (0.0.20) deprecation_toolkit (2.0.3) @@ -62,6 +79,7 @@ GEM diff-lcs (1.3) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) + erubi (1.12.0) excon (0.92.4) faker (3.2.0) i18n (>= 1.8.11, < 2) @@ -176,6 +194,9 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) + loofah (2.21.3) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) macaddr (1.7.2) systemu (~> 2.6.5) matrix (0.4.2) @@ -214,10 +235,15 @@ GEM byebug (~> 11.0) pry (>= 0.13, < 0.15) public_suffix (5.0.1) - racc (1.6.2) + racc (1.7.1) rack (2.2.3.1) rack-test (1.1.0) rack (>= 1.0, < 3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.1.1) @@ -324,6 +350,7 @@ DEPENDENCIES fog-core (= 2.1.0) fog-google (~> 1.19) gitlab-qa (~> 11, >= 11.2.0) + gitlab-utils! gitlab_quality-test_tooling (~> 0.8.2) influxdb-client (~> 2.9) knapsack (~> 4.0) diff --git a/qa/gdk/Dockerfile.gdk b/qa/gdk/Dockerfile.gdk index cdb693841cb..2b296535622 100644 --- a/qa/gdk/Dockerfile.gdk +++ b/qa/gdk/Dockerfile.gdk @@ -98,8 +98,8 @@ RUN set -eux; \ # Install gitlab gem dependencies # COPY --chown=gdk:gdk Gemfile Gemfile.lock ./gitlab/ -COPY --chown=gdk:gdk vendor/gems ./gitlab/vendor/gems -COPY --chown=gdk:gdk gems ./gitlab/gems +COPY --chown=gdk:gdk vendor/gems/ ./gitlab/vendor/gems/ +COPY --chown=gdk:gdk gems/ ./gitlab/gems/ RUN make .gitlab-bundle && rm -rf ${GEM_HOME}/cache # Install gitlab npm dependencies diff --git a/qa/qa.rb b/qa/qa.rb index f6fba30c079..0e3d343b861 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -2,8 +2,9 @@ Encoding.default_external = 'UTF-8' +require 'gitlab/utils/all' + require_relative '../lib/gitlab_edition' -require_relative '../lib/gitlab/utils' require_relative '../config/initializers/0_inject_enterprise_edition_module' require_relative 'lib/gitlab' diff --git a/qa/qa/scenario/test/integration/oauth.rb b/qa/qa/scenario/test/integration/oauth.rb new file mode 100644 index 00000000000..912156fbc29 --- /dev/null +++ b/qa/qa/scenario/test/integration/oauth.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class OAuth < Test::Instance::All + tags :oauth + end + end + end + end +end diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build index 6901593009a..cfa089b327e 100755 --- a/scripts/gitaly-test-build +++ b/scripts/gitaly-test-build @@ -1,6 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true +require_relative '../config/bundler_setup' require 'fileutils' require_relative '../spec/support/helpers/gitaly_setup' diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn index 475c7715bdd..9285b561ae0 100755 --- a/scripts/gitaly-test-spawn +++ b/scripts/gitaly-test-spawn @@ -3,6 +3,7 @@ # This script is used both in CI and in local development 'rspec' runs. +require_relative '../config/bundler_setup' require_relative '../spec/support/helpers/gitaly_setup' class GitalyTestSpawn diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov index 24be731549b..7db12839382 100755 --- a/scripts/merge-simplecov +++ b/scripts/merge-simplecov @@ -1,6 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true +require_relative '../config/bundler_setup' require_relative '../spec/simplecov_env' SimpleCovEnv.configure_profile SimpleCovEnv.configure_formatter diff --git a/scripts/setup-test-env b/scripts/setup-test-env index ae00b569ce3..1c39483bb7a 100755 --- a/scripts/setup-test-env +++ b/scripts/setup-test-env @@ -25,8 +25,8 @@ require_relative '../lib/system_check/helpers' require 'omniauth' require 'omniauth-github' require 'etc' +require 'gitlab/utils/all' require_relative '../lib/gitlab/access' -require_relative '../lib/gitlab/utils' unless defined?(License) # This is needed to allow use of `Gitlab::ImportSources.values` in `1_settings.rb`. diff --git a/sidekiq_cluster/cli.rb b/sidekiq_cluster/cli.rb index 0d24f70c37d..fc065d799d4 100644 --- a/sidekiq_cluster/cli.rb +++ b/sidekiq_cluster/cli.rb @@ -5,11 +5,11 @@ require_relative '../config/bundler_setup' require 'optparse' require 'logger' require 'time' +require 'gitlab/utils/all' # In environments where code is preloaded and cached such as `spring`, # we may run into "already initialized" warnings, hence the check. require_relative '../lib/gitlab' -require_relative '../lib/gitlab/utils' require_relative '../lib/gitlab/sidekiq_config/cli_methods' require_relative '../lib/gitlab/sidekiq_config/worker_matcher' require_relative '../lib/gitlab/sidekiq_logging/json_formatter' diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb index 6649e8f057c..7795fff5541 100644 --- a/spec/controllers/groups/uploads_controller_spec.rb +++ b/spec/controllers/groups/uploads_controller_spec.rb @@ -76,31 +76,17 @@ RSpec.describe Groups::UploadsController do context 'when uploader class does not match the upload' do let(:uploader_class) { FileUploader } - it 'responds with status 200 but logs a deprecation message' do - expect(Gitlab::AppJsonLogger).to receive(:info).with( - message: 'Deprecated usage of build_uploader_from_params', - uploader_class: uploader_class.name, - path: filename, - exists: true - ) - + it 'responds with status 404' do show_upload - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:not_found) end end context 'when filename does not match' do let(:invalid_filename) { 'invalid_filename.jpg' } - it 'responds with status 404 and logs a deprecation message' do - expect(Gitlab::AppJsonLogger).to receive(:info).with( - message: 'Deprecated usage of build_uploader_from_params', - uploader_class: uploader_class.name, - path: invalid_filename, - exists: false - ) - + it 'responds with status 404' do get :show, params: params.merge(secret: secret, filename: invalid_filename) expect(response).to have_gitlab_http_status(:not_found) diff --git a/spec/deprecation_warnings.rb b/spec/deprecation_warnings.rb index 45fed5fecca..abdd13ee8e7 100644 --- a/spec/deprecation_warnings.rb +++ b/spec/deprecation_warnings.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../lib/gitlab/utils' +require 'gitlab/utils/all' return if Gitlab::Utils.to_boolean(ENV['SILENCE_DEPRECATIONS'], default: false) # Enable deprecation warnings by default and make them more visible diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 03c8919912c..47a90efab1e 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -18,13 +18,12 @@ RSpec.configure(&:disable_monkey_patching!) require 'active_support/all' require 'pry' +require 'gitlab/utils/all' require_relative 'rails_autoload' require_relative '../config/settings' require_relative 'support/rspec' require_relative '../lib/gitlab' -require_relative '../lib/gitlab/utils' -require_relative '../lib/gitlab/utils/strong_memoize' require_relative 'simplecov_env' SimpleCovEnv.start! diff --git a/spec/features/markdown/sandboxed_mermaid_spec.rb b/spec/features/markdown/sandboxed_mermaid_spec.rb index f8a535191da..87696eb82e5 100644 --- a/spec/features/markdown/sandboxed_mermaid_spec.rb +++ b/spec/features/markdown/sandboxed_mermaid_spec.rb @@ -16,8 +16,9 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js, feature_category: :team_plann MERMAID end - let_it_be(:expected) do - %(