diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml
index 9a6a6eb6720..4fcc1090686 100644
--- a/.rubocop_todo/gitlab/bounded_contexts.yml
+++ b/.rubocop_todo/gitlab/bounded_contexts.yml
@@ -2568,6 +2568,7 @@ Gitlab/BoundedContexts:
- 'ee/app/graphql/resolvers/vulnerabilities_grade_resolver.rb'
- 'ee/app/graphql/resolvers/vulnerabilities_resolver.rb'
- 'ee/app/graphql/resolvers/vulnerability_severities_count_resolver.rb'
+ - 'ee/app/graphql/resolvers/vulnerability_filterable.rb'
- 'ee/app/graphql/subscriptions/ai_completion_response.rb'
- 'ee/app/graphql/types/access_levels/group_type.rb'
- 'ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb'
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 09f403fccd4..9ac594b90ff 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -193,19 +193,6 @@ Layout/ArgumentAlignment:
- 'lib/api/ci/resource_groups.rb'
- 'lib/api/ci/runner.rb'
- 'lib/api/ci/runners.rb'
- - 'lib/api/ci/triggers.rb'
- - 'lib/api/commit_statuses.rb'
- - 'lib/api/commits.rb'
- - 'lib/api/concerns/packages/debian_distribution_endpoints.rb'
- - 'lib/api/concerns/packages/npm_endpoints.rb'
- - 'lib/api/container_repositories.rb'
- - 'lib/api/dependency_proxy.rb'
- - 'lib/api/deploy_keys.rb'
- - 'lib/api/deploy_tokens.rb'
- - 'lib/api/deployments.rb'
- - 'lib/api/entities/application.rb'
- - 'lib/api/entities/application_statistics.rb'
- - 'lib/api/entities/branch.rb'
- 'lib/api/entities/npm_package.rb'
- 'lib/api/entities/nuget/dependency_group.rb'
- 'lib/api/entities/nuget/package_metadata.rb'
@@ -219,19 +206,6 @@ Layout/ArgumentAlignment:
- 'lib/api/entities/pull_mirror.rb'
- 'lib/api/entities/release.rb'
- 'lib/api/entities/resource_access_token.rb'
- - 'lib/api/merge_requests.rb'
- - 'lib/api/metrics/dashboard/annotations.rb'
- - 'lib/api/metrics/user_starred_dashboards.rb'
- - 'lib/api/milestone_responses.rb'
- - 'lib/api/notes.rb'
- - 'lib/api/nuget_project_packages.rb'
- - 'lib/api/pages.rb'
- - 'lib/api/pages_domains.rb'
- - 'lib/api/pagination_params.rb'
- - 'lib/api/personal_access_tokens.rb'
- - 'lib/api/project_container_repositories.rb'
- - 'lib/api/project_export.rb'
- - 'lib/api/project_import.rb'
- 'lib/api/tags.rb'
- 'lib/api/terraform/state.rb'
- 'lib/api/topics.rb'
diff --git a/.rubocop_todo/layout/multiline_operation_indentation.yml b/.rubocop_todo/layout/multiline_operation_indentation.yml
deleted file mode 100644
index 5eba8cb412a..00000000000
--- a/.rubocop_todo/layout/multiline_operation_indentation.yml
+++ /dev/null
@@ -1,14 +0,0 @@
----
-# Cop supports --autocorrect.
-Layout/MultilineOperationIndentation:
- Exclude:
- - 'app/policies/project_policy.rb'
- - 'app/serializers/deploy_keys/deploy_key_entity.rb'
- - 'app/services/ci/create_downstream_pipeline_service.rb'
- - 'app/services/git/branch_hooks_service.rb'
- - 'app/services/groups/transfer_service.rb'
- - 'app/services/issues/update_service.rb'
- - 'app/services/labels/promote_service.rb'
- - 'app/services/labels/transfer_service.rb'
- - 'app/services/members/approve_access_request_service.rb'
- - 'app/services/webauthn/authenticate_service.rb'
diff --git a/.rubocop_todo/lint/ambiguous_operator_precedence.yml b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
index cd513753b67..9bfe179acc1 100644
--- a/.rubocop_todo/lint/ambiguous_operator_precedence.yml
+++ b/.rubocop_todo/lint/ambiguous_operator_precedence.yml
@@ -21,18 +21,6 @@ Lint/AmbiguousOperatorPrecedence:
- 'ee/app/services/geo/registry_consistency_service.rb'
- 'ee/app/services/vulnerabilities/create_service.rb'
- 'ee/lib/gitlab/expiring_subscription_message.rb'
- - 'lib/banzai/filter/references/user_reference_filter.rb'
- - 'lib/banzai/filter_array.rb'
- - 'lib/extracts_ref.rb'
- - 'lib/gitlab/chaos.rb'
- - 'lib/gitlab/ci/config/normalizer/number_strategy.rb'
- - 'lib/gitlab/console.rb'
- - 'lib/gitlab/database/background_migration/batch_metrics.rb'
- - 'lib/gitlab/database/background_migration/batched_migration.rb'
- - 'lib/gitlab/database/migrations/background_migration_helpers.rb'
- - 'lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb'
- - 'lib/gitlab/database/postgres_hll/buckets.rb'
- - 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb'
- 'spec/lib/gitlab/conan_token_spec.rb'
- 'spec/lib/gitlab/database/background_migration/batched_job_spec.rb'
- 'spec/lib/gitlab/database/batch_count_spec.rb'
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index be23731459f..3c74580b4b5 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-v17.1.0-rc6
+v17.1.0-rc7
diff --git a/Gemfile b/Gemfile
index 8d1679886cc..8708f4a228a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -140,7 +140,7 @@ gem 'rack-cors', '~> 2.0.1', require: 'rack/cors' # rubocop:todo Gemfile/Missing
gem 'graphql', '~> 2.3.3', feature_category: :api
gem 'graphql-docs', '~> 4.0.0', group: [:development, :test], feature_category: :api
gem 'graphiql-rails', '~> 1.8.0', feature_category: :api
-gem 'apollo_upload_server', '~> 2.1.5', feature_category: :api
+gem 'apollo_upload_server', '~> 2.1.6', feature_category: :api
gem 'graphlient', '~> 0.6.0', feature_category: :importers # Used by BulkImport feature (group::import)
# Generate Fake data
@@ -536,7 +536,7 @@ group :test do
gem 'capybara', '~> 3.40' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'capybara-screenshot', '~> 1.0.26' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'selenium-webdriver', '~> 4.20', '>= 4.20.1' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'selenium-webdriver', '~> 4.21', '>= 4.21.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'graphlyte', '~> 1.0.0' # rubocop:todo Gemfile/MissingFeatureCategory
@@ -682,9 +682,10 @@ gem 'telesignenterprise', '~> 2.2' # rubocop:todo Gemfile/MissingFeatureCategory
# BufferedIO patch
# Updating this version will require updating scripts/allowed_warnings.txt
gem 'net-protocol', '~> 0.1.3' # rubocop:todo Gemfile/MissingFeatureCategory
-# Lock this until we make DNS rebinding work with the updated net-http:
-# https://gitlab.com/gitlab-org/gitlab/-/issues/413528
-gem 'net-http', '= 0.1.1' # rubocop:todo Gemfile/MissingFeatureCategory
+
+# This is locked to 0.4.1 because we patch Net::HTTP#connect in
+# gems/gitlab-http/lib/net_http/connect_patch.rb.
+gem 'net-http', '= 0.4.1', feature_category: :shared
gem 'duo_api', '~> 1.3' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 34788a9115c..bdffbf04811 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -21,7 +21,7 @@
{"name":"aliyun-sdk","version":"0.8.0","platform":"ruby","checksum":"65915d3f9b528082253d1f9ad0e4d13d6b552933fe49251c68c6915cd4d75b9d"},
{"name":"amatch","version":"0.4.1","platform":"ruby","checksum":"d3ff15226a2e627c72802e94579db829e5e10c96cf89d329494caec5889145f7"},
{"name":"android_key_attestation","version":"0.3.0","platform":"ruby","checksum":"467eb01a99d2bb48ef9cf24cc13712669d7056cba5a52d009554ff037560570b"},
-{"name":"apollo_upload_server","version":"2.1.5","platform":"ruby","checksum":"0f66bea96bdf7ce8b7278712ebafc8a26b82864ea6541213b58d9b3f673413a5"},
+{"name":"apollo_upload_server","version":"2.1.6","platform":"ruby","checksum":"dcec4072258e6518b0b82e03b485efbddde946813543c14184fc81952d6bcdb2"},
{"name":"app_store_connect","version":"0.29.0","platform":"ruby","checksum":"01d7a923825a4221892099acb5a72f86f6ee7d8aa95815d3c459ba6816ea430f"},
{"name":"arr-pm","version":"0.0.12","platform":"ruby","checksum":"fdff482f75239239201f4d667d93424412639aad0b3b0ad4d827e7c637e0ad39"},
{"name":"asciidoctor","version":"2.0.18","platform":"ruby","checksum":"bbd1e1d16deed8db94bf9624b9f4474fac32d9ca7225d377f076c08d9adde387"},
@@ -395,7 +395,7 @@
{"name":"nap","version":"1.1.0","platform":"ruby","checksum":"949691660f9d041d75be611bb2a8d2fd559c467537deac241f4097d9b5eea576"},
{"name":"neighbor","version":"0.3.2","platform":"ruby","checksum":"b795bbcc24b1b9ae82d9f7e97a3461b0b3607d24a85a7acbed776bd498e7eba8"},
{"name":"nenv","version":"0.3.0","platform":"ruby","checksum":"d9de6d8fb7072228463bf61843159419c969edb34b3cef51832b516ae7972765"},
-{"name":"net-http","version":"0.1.1","platform":"ruby","checksum":"75a4e109b6f9af32fad0e98a6180c47aceb415927ca3bd70c8fc3e7dbbabbe86"},
+{"name":"net-http","version":"0.4.1","platform":"ruby","checksum":"a96efc5ea18bcb9715e24dda4159d10f67ff0345c8a980d04630028055b2c282"},
{"name":"net-http-persistent","version":"4.0.1","platform":"ruby","checksum":"2752f4cce05fd1c45e0537c6f3a98fa5a4899efd5f88e63c104ed5f05cbddef9"},
{"name":"net-imap","version":"0.3.4","platform":"ruby","checksum":"a82a59e2a429433dc54cae5a8b2979ffe49da8c66085740811bfa337dc3729b5"},
{"name":"net-ldap","version":"0.17.1","platform":"ruby","checksum":"52571b55f9157120833ac1667f2969ce0139251811d0a9b64657c1c135069cf9"},
@@ -617,7 +617,7 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
-{"name":"selenium-webdriver","version":"4.20.1","platform":"ruby","checksum":"560ca00d45bed16d661089da674290ce81564949888daa1f8659fe77fd39a2ac"},
+{"name":"selenium-webdriver","version":"4.21.1","platform":"ruby","checksum":"c30b64014532fc5156c60797985f839f36adbe60ff4653e7112b008dc1c83263"},
{"name":"semver_dialects","version":"2.0.2","platform":"ruby","checksum":"60059c9f416f931b5212d862fad2879d6b9affb8e0b9afb0d91b793639c116fe"},
{"name":"sentry-rails","version":"5.17.3","platform":"ruby","checksum":"017771c42d739c0ad2213a581ca9d005cf543227bc13662cd1ca9909f2429459"},
{"name":"sentry-ruby","version":"5.17.3","platform":"ruby","checksum":"61791a4b0bb0f95cd87aceeaa1efa6d4ab34d64236c9d5df820478adfe2fbbfc"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 1f9f0838cf0..6ba258f78aa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -48,6 +48,7 @@ PATH
concurrent-ruby (~> 1.2)
httparty (~> 0.21.0)
ipaddress (~> 0.8.3)
+ net-http (= 0.4.1)
railties (~> 7)
PATH
@@ -278,7 +279,7 @@ GEM
mize
tins (~> 1.0)
android_key_attestation (0.3.0)
- apollo_upload_server (2.1.5)
+ apollo_upload_server (2.1.6)
actionpack (>= 6.1.6)
graphql (>= 1.8)
app_store_connect (0.29.0)
@@ -1115,8 +1116,7 @@ GEM
neighbor (0.3.2)
activerecord (>= 6.1)
nenv (0.3.0)
- net-http (0.1.1)
- net-protocol
+ net-http (0.4.1)
uri
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
@@ -1643,7 +1643,7 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- selenium-webdriver (4.20.1)
+ selenium-webdriver (4.21.1)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@@ -1912,7 +1912,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 10.0)
addressable (~> 2.8)
akismet (~> 3.0)
- apollo_upload_server (~> 2.1.5)
+ apollo_upload_server (~> 2.1.6)
app_store_connect
arr-pm (~> 0.0.12)
asciidoctor (~> 2.0.18)
@@ -2095,7 +2095,7 @@ DEPENDENCIES
minitest (~> 5.11.0)
multi_json (~> 1.14.1)
neighbor (~> 0.3.2)
- net-http (= 0.1.1)
+ net-http (= 0.4.1)
net-ldap (~> 0.17.1)
net-ntp
net-protocol (~> 0.1.3)
@@ -2203,7 +2203,7 @@ DEPENDENCIES
sanitize (~> 6.0.2)
sd_notify (~> 0.1.0)
seed-fu (~> 2.3.7)
- selenium-webdriver (~> 4.20, >= 4.20.1)
+ selenium-webdriver (~> 4.21, >= 4.21.1)
semver_dialects (~> 2.0, >= 2.0.2)
sentry-rails (~> 5.17.3)
sentry-ruby (~> 5.17.3)
diff --git a/app/assets/javascripts/groups/components/new_group_form.vue b/app/assets/javascripts/groups/components/new_edit_form.vue
similarity index 78%
rename from app/assets/javascripts/groups/components/new_group_form.vue
rename to app/assets/javascripts/groups/components/new_edit_form.vue
index e0954ac9bcc..b21dada1fda 100644
--- a/app/assets/javascripts/groups/components/new_group_form.vue
+++ b/app/assets/javascripts/groups/components/new_edit_form.vue
@@ -1,5 +1,5 @@
@@ -129,9 +125,9 @@ export default {
-
{{ $options.i18n.modalContent }}
+ +{{ $options.i18n.modalConfirm }}
+{{ projectPath }}
+
+
-
-
-
+

Reference-style:
-
-
-![alt text1][logo]
-
+![alt text1][logo]
[logo]: img/markdown_logo.png "Title Text"
-

@@ -528,6 +521,12 @@ For example
You can also use the `img` HTML tag instead of Markdown and set its `height` and
`width` parameters.
+When you paste a higher resolution PNG image into a Markdown text box [in GitLab 17.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/419913),
+dimensions are always appended. The dimensions are automatically adjusted to
+accommodate for retina (and other higher-resolution) displays. For instance,
+a 144ppi image is resized to 50% of its dimensions, whereas a 96ppi image is
+resized to 75% of its dimensions.
+
### Audio
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#audio).
diff --git a/doc/user/packages/container_registry/delete_container_registry_images.md b/doc/user/packages/container_registry/delete_container_registry_images.md
index c97550f8b21..1b4211ce4df 100644
--- a/doc/user/packages/container_registry/delete_container_registry_images.md
+++ b/doc/user/packages/container_registry/delete_container_registry_images.md
@@ -64,8 +64,8 @@ information, see the following endpoints:
NOTE:
GitLab CI/CD doesn't provide a built-in way to remove your container images. This example uses a
-third-party tool called [reg](https://github.com/genuinetools/reg) that talks to the GitLab Registry API.
-For assistance with this third-party tool, see [the issue queue for reg](https://github.com/genuinetools/reg/issues).
+third-party tool called [`regctl`](https://github.com/regclient/regclient) that talks to the GitLab Registry API.
+For assistance with this third-party tool, see [the issue queue for regclient](https://github.com/regclient/regclient/issues).
The following example defines two stages: `build`, and `clean`. The `build_image` job builds a container
image for the branch, and the `delete_image` job deletes it. The `reg` executable is downloaded and used to
@@ -96,27 +96,25 @@ build_image:
- main
delete_image:
- before_script:
- - curl --fail --show-error --location "https://github.com/genuinetools/reg/releases/download/v$REG_VERSION/reg-linux-amd64" --output ./reg
- - echo "$REG_SHA256 ./reg" | sha256sum -c -
- - chmod a+x ./reg
- image: curlimages/curl:7.86.0
- script:
- - ./reg rm -d --auth-url $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $IMAGE_TAG
stage: clean
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
- REG_SHA256: ade837fc5224acd8c34732bf54a94f579b47851cc6a7fd5899a98386b782e228
- REG_VERSION: 0.16.1
- only:
- - branches
- except:
- - main
+ REGCTL_VERSION: v0.6.1
+ rules:
+ - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
+ image: alpine:latest
+ script:
+ - apk update
+ - apk add curl
+ - curl --location "https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64" > /usr/bin/regctl
+ - chmod 755 /usr/bin/regctl
+ - regctl registry login ${CI_REGISTRY} -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD}
+ - regctl tag rm $IMAGE
```
NOTE:
-You can download the latest `reg` release from [the releases page](https://github.com/genuinetools/reg/releases), then update
-the code example by changing the `REG_SHA256` and `REG_VERSION` variables defined in the `delete_image` job.
+You can download the latest `regctl` release from [the releases page](https://github.com/regclient/regclient/releasess), then update
+the code example by changing the `REGCTL_VERSION` variable defined in the `delete_image` job.
## Use a cleanup policy
diff --git a/doc/user/packages/workflows/build_packages.md b/doc/user/packages/workflows/build_packages.md
index 895953a832c..db7e4decd4c 100644
--- a/doc/user/packages/workflows/build_packages.md
+++ b/doc/user/packages/workflows/build_packages.md
@@ -56,7 +56,7 @@ Prerequisites:
- You must install Conan version 1.x. Support for Conan version 2 is proposed in [epic 8258](https://gitlab.com/groups/gitlab-org/-/epics/8258).
Download the Conan package manager to your local development environment by
-following the instructions at [conan.io](https://conan.io/download).
+following the instructions at [conan.io](https://conan.io/downloads).
When installation is complete, verify you can use Conan in your terminal by
running:
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index fdaf912a3ba..ebc4d0d6240 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -20,9 +20,7 @@ DETAILS:
> - Enabled in GitLab 16.7 as a [beta](../../policy/experiment-beta-support.md#beta) feature.
> - `product_analytics_dashboards` [enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/398653) by default in GitLab 16.11.
> - [Enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/444345) in GitLab 16.11.
-
-FLAG:
-The availability of this feature is controlled by feature flags. For more information, see the history.
+> - Feature flag `product_analytics_dashboards` [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/454059) in GitLab 17.1.
The product analytics feature empowers you to track user behavior and gain insights into how your
applications are used and how users interact with your product.
diff --git a/doc/user/project/img/issue_boards_multiple_v13_6.png b/doc/user/project/img/issue_boards_multiple_v13_6.png
deleted file mode 100644
index 18ff5e2bc66..00000000000
Binary files a/doc/user/project/img/issue_boards_multiple_v13_6.png and /dev/null differ
diff --git a/doc/user/project/repository/mirror/bidirectional.md b/doc/user/project/repository/mirror/bidirectional.md
index 9e50d2c4c58..d6f62a19d6d 100644
--- a/doc/user/project/repository/mirror/bidirectional.md
+++ b/doc/user/project/repository/mirror/bidirectional.md
@@ -172,8 +172,6 @@ settings:
- Use the `unknown_git` user as the commit author, if the GitLab user doesn't exist in
Perforce Helix.
-Read about [Git Fusion settings on Perforce.com](https://www.perforce.com/manuals/git-fusion/Content/Git-Fusion/section_vss_bdw_w3.html#section_zdp_zz1_3l).
-
## Related topics
- [Troubleshooting](troubleshooting.md) for repository mirroring.
diff --git a/gems/gitlab-housekeeper/README.md b/gems/gitlab-housekeeper/README.md
index 642ace44831..449dae41ba8 100644
--- a/gems/gitlab-housekeeper/README.md
+++ b/gems/gitlab-housekeeper/README.md
@@ -4,6 +4,10 @@ Check out [the original
blueprint](https://docs.gitlab.com/ee/architecture/blueprints/gitlab_housekeeper/)
for the motivation behind the `gitlab-housekeeper`.
+Also watch [this walkthrough video](https://youtu.be/KNJPVx8izAc) for an
+overview on how to create your first Keep as well as the philosophy behind
+`gitlab-housekeeper`.
+
This is a gem which can be run locally or in CI to do static and dynamic
analysis of the GitLab codebase and, using a list of predefined "keeps", it will
automatically create merge requests for things that developers would have
@@ -169,3 +173,19 @@ Some best practices to consider when using a once-off keep:
1. Consider adding a link back to this MR in the description of your generated
MRs. This allows reviewers to understand where this work comes from and can
also help if they want to contribute improvements to an ongoing group of MRs.
+
+## Using Housekeeper in other projects
+
+Right now we do not publish housekeeper to RubyGems. We have published it once
+to hold the name but it's not up to date.
+
+In order to use Housekeeper in another project you would need to add the
+following to your `Gemfile` and run `bundle install`:
+
+```
+gem 'gitlab-housekeeper', git: 'https://gitlab.com/gitlab-org/gitlab.git', branch: 'master', glob: 'gems/gitlab-housekeeper/*.gemspec'
+```
+
+After that you can just run `bundle exec gitlab-housekeeper`. Housekeeper
+defaults to loading all keeps in the `./keeps` directory so you would also
+create that directory in your project and put your keeps there.
diff --git a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb
index 5c9c91f1748..e7e960373be 100644
--- a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb
+++ b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'active_support'
+require 'active_support/core_ext'
require 'active_support/core_ext/string'
require 'gitlab/housekeeper/logger'
require 'gitlab/housekeeper/keep'
diff --git a/gems/gitlab-http/Gemfile.lock b/gems/gitlab-http/Gemfile.lock
index 149a6805f05..afdf6f3dce2 100644
--- a/gems/gitlab-http/Gemfile.lock
+++ b/gems/gitlab-http/Gemfile.lock
@@ -23,6 +23,7 @@ PATH
concurrent-ruby (~> 1.2)
httparty (~> 0.21.0)
ipaddress (~> 0.8.3)
+ net-http (= 0.4.1)
railties (~> 7)
GEM
@@ -82,6 +83,8 @@ GEM
mini_mime (1.1.2)
minitest (5.18.1)
multi_xml (0.6.0)
+ net-http (0.4.1)
+ uri
nokogiri (1.15.4)
racc (~> 1.4)
parallel (1.23.0)
@@ -165,10 +168,12 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
+ uri (0.13.0)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
+ webrick (1.8.1)
zeitwerk (2.6.8)
PLATFORMS
@@ -183,6 +188,7 @@ DEPENDENCIES
rubocop (~> 1.50.2)
rubocop-rspec (~> 2.22)
webmock (~> 3.18.1)
+ webrick (~> 1.8)
BUNDLED WITH
2.4.14
diff --git a/gems/gitlab-http/gitlab-http.gemspec b/gems/gitlab-http/gitlab-http.gemspec
index 27366246bb1..2378945cbcc 100644
--- a/gems/gitlab-http/gitlab-http.gemspec
+++ b/gems/gitlab-http/gitlab-http.gemspec
@@ -24,10 +24,13 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'httparty', '~> 0.21.0'
spec.add_runtime_dependency 'ipaddress', '~> 0.8.3'
spec.add_runtime_dependency "railties", "~> 7"
+ # See lib/net_http/connect_patch.rb
+ spec.add_runtime_dependency "net-http", "= 0.4.1"
spec.add_development_dependency 'gitlab-styles', '~> 10.1.0'
spec.add_development_dependency 'rspec-rails', '~> 6.0.3'
spec.add_development_dependency "rubocop", "~> 1.50.2"
spec.add_development_dependency "rubocop-rspec", "~> 2.22"
spec.add_development_dependency 'webmock', '~> 3.18.1'
+ spec.add_development_dependency 'webrick', '~> 1.8'
end
diff --git a/gems/gitlab-http/lib/gitlab/http_v2/patches.rb b/gems/gitlab-http/lib/gitlab/http_v2/patches.rb
index 3d26fbc6447..cd89b2e1b44 100644
--- a/gems/gitlab-http/lib/gitlab/http_v2/patches.rb
+++ b/gems/gitlab-http/lib/gitlab/http_v2/patches.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
require_relative "../../hostname_override_patch"
-require_relative "../../net_http/protocol_patch"
+require_relative "../../net_http/connect_patch"
require_relative "../../net_http/response_patch"
require_relative "../../httparty/response_patch"
diff --git a/gems/gitlab-http/lib/net_http/connect_patch.rb b/gems/gitlab-http/lib/net_http/connect_patch.rb
new file mode 100644
index 00000000000..e737f0fcedf
--- /dev/null
+++ b/gems/gitlab-http/lib/net_http/connect_patch.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+# This patches Net::HTTP#connect to handle the hostname override patch,
+# which is needed for Server Side Request Forgery (SSRF)
+# protection. This stopped working in net-http v0.2.2 due to
+# https://github.com/ruby/net-http/pull/36.
+# https://github.com/ruby/net-http/issues/141 is outstanding to make
+# this less hacky, but for now we restore the previous behavior by
+# setting the SNI hostname with the hostname override, if available.
+require 'net/http'
+
+module Net
+ class HTTP < Protocol
+ # rubocop:disable Cop/LineBreakAroundConditionalBlock -- This is upstream code
+ # rubocop:disable Layout/ArgumentAlignment -- This is upstream code
+ # rubocop:disable Layout/AssignmentIndentation -- This is upstream code
+ # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is upstream code
+ # rubocop:disable Layout/MultilineOperationIndentation -- This is upstream code
+ # rubocop:disable Layout/SpaceInsideBlockBraces -- This is upstream code
+ # rubocop:disable Lint/UnusedBlockArgument -- This is upstream code
+ # rubocop:disable Metrics/AbcSize -- This is upstream code
+ # rubocop:disable Metrics/CyclomaticComplexity -- This is upstream code
+ # rubocop:disable Metrics/PerceivedComplexity -- This is upstream code
+ # rubocop:disable Naming/RescuedExceptionsVariableName -- This is upstream code
+ # rubocop:disable Style/AndOr -- This is upstream code
+ # rubocop:disable Style/BlockDelimiters -- This is upstream code
+ # rubocop:disable Style/EmptyLiteral -- This is upstream code
+ # rubocop:disable Style/IfUnlessModifier -- This is upstream code
+ # rubocop:disable Style/LineEndConcatenation -- This is upstream code
+ # rubocop:disable Style/MultilineIfThen -- This is upstream code
+ # rubocop:disable Style/Next -- This is upstream code
+ # rubocop:disable Style/RescueStandardError -- This is upstream code
+ # rubocop:disable Style/StringConcatenation -- This is upstream code
+ def connect
+ if use_ssl?
+ # reference early to load OpenSSL before connecting,
+ # as OpenSSL may take time to load.
+ @ssl_context = OpenSSL::SSL::SSLContext.new
+ end
+
+ if proxy? then
+ conn_addr = proxy_address
+ conn_port = proxy_port
+ else
+ conn_addr = conn_address
+ conn_port = port
+ end
+
+ debug "opening connection to #{conn_addr}:#{conn_port}..."
+ s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
+ begin
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
+ rescue => e
+ raise e, "Failed to open TCP connection to " +
+ "#{conn_addr}:#{conn_port} (#{e.message})"
+ end
+ }
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ debug "opened"
+ if use_ssl?
+ if proxy?
+ plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \
+ "Host: #{@address}:#{@port}\r\n"
+ if proxy_user
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
+ end
+ buf << "\r\n"
+ plain_sock.write(buf)
+ HTTPResponse.read_new(plain_sock).value
+ # assuming nothing left in buffers after successful CONNECT response
+ end
+
+ ssl_parameters = Hash.new
+ iv_list = instance_variables
+ SSL_IVNAMES.each_with_index do |ivname, i|
+ if iv_list.include?(ivname)
+ value = instance_variable_get(ivname)
+ unless value.nil?
+ ssl_parameters[SSL_ATTRIBUTES[i]] = value
+ end
+ end
+ end
+ @ssl_context.set_params(ssl_parameters)
+ unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby
+ @ssl_context.session_cache_mode =
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
+ end
+ if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+ end
+
+ # Still do the post_connection_check below even if connecting
+ # to IP address
+ verify_hostname = @ssl_context.verify_hostname
+
+ # This hack would not be needed with https://github.com/ruby/net-http/issues/141
+ address_to_verify = hostname_override || @address
+
+ # Server Name Indication (SNI) RFC 3546/6066
+ case address_to_verify
+ when Resolv::IPv4::Regex, Resolv::IPv6::Regex
+ # don't set SNI, as IP addresses in SNI is not valid
+ # per RFC 6066, section 3.
+
+ # Avoid openssl warning
+ @ssl_context.verify_hostname = false
+ else
+ ssl_host_address = address_to_verify
+ end
+
+ debug "starting SSL for #{conn_addr}:#{conn_port}..."
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
+ s.sync_close = true
+ s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address
+
+ if @ssl_session and
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+ s.session = @ssl_session
+ end
+ ssl_socket_connect(s, @open_timeout)
+ if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname
+ s.post_connection_check(@address)
+ end
+ debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
+ end
+ @socket = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ @last_communicated = nil
+ on_connect
+ rescue => exception
+ if s
+ debug "Conn close because of connect error #{exception}"
+ s.close
+ end
+ raise
+ end
+ private :connect
+ # rubocop:enable Cop/LineBreakAroundConditionalBlock
+ # rubocop:enable Layout/ArgumentAlignment
+ # rubocop:enable Layout/AssignmentIndentation
+ # rubocop:enable Layout/LineEndStringConcatenationIndentation
+ # rubocop:enable Layout/MultilineOperationIndentation
+ # rubocop:enable Layout/SpaceInsideBlockBraces
+ # rubocop:enable Lint/UnusedBlockArgument
+ # rubocop:enable Metrics/AbcSize
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/PerceivedComplexity
+ # rubocop:enable Naming/RescuedExceptionsVariableName
+ # rubocop:enable Style/AndOr
+ # rubocop:enable Style/BlockDelimiters
+ # rubocop:enable Style/EmptyLiteral
+ # rubocop:enable Style/IfUnlessModifier
+ # rubocop:enable Style/LineEndConcatenation
+ # rubocop:enable Style/MultilineIfThen
+ # rubocop:enable Style/Next
+ # rubocop:enable Style/RescueStandardError
+ # rubocop:enable Style/StringConcatenation
+ end
+end
diff --git a/gems/gitlab-http/lib/net_http/protocol_patch.rb b/gems/gitlab-http/lib/net_http/protocol_patch.rb
deleted file mode 100644
index 8231423e1a5..00000000000
--- a/gems/gitlab-http/lib/net_http/protocol_patch.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-# Monkey patch Net::HTTP to fix missing URL decoding for username and password in proxy settings
-#
-# See proposed upstream fix https://github.com/ruby/net-http/pull/5
-# See Ruby-lang issue https://bugs.ruby-lang.org/issues/17542
-# See issue on GitLab https://gitlab.com/gitlab-org/gitlab/-/issues/289836
-
-require 'net/http'
-
-# This file can be removed once Ruby 3.0 is no longer supported:
-# https://gitlab.com/gitlab-org/gitlab/-/issues/396223
-return if Gem::Version.new(Net::HTTP::VERSION) >= Gem::Version.new('0.2.0')
-
-module Net
- class HTTP < Protocol
- def proxy_user
- if environment_variable_is_multiuser_safe? && @proxy_from_env
- user = proxy_uri&.user
- CGI.unescape(user) unless user.nil?
- else
- @proxy_user
- end
- end
-
- def proxy_pass
- if environment_variable_is_multiuser_safe? && @proxy_from_env
- pass = proxy_uri&.password
- CGI.unescape(pass) unless pass.nil?
- else
- @proxy_pass
- end
- end
-
- def environment_variable_is_multiuser_safe?
- ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE
- end
- end
-end
diff --git a/gems/gitlab-http/spec/gitlab/http_v2/net_http_connect_patch_spec.rb b/gems/gitlab-http/spec/gitlab/http_v2/net_http_connect_patch_spec.rb
new file mode 100644
index 00000000000..548c33058be
--- /dev/null
+++ b/gems/gitlab-http/spec/gitlab/http_v2/net_http_connect_patch_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'webrick'
+require 'webrick/https'
+
+RSpec.describe 'Net::HTTP#connect DNS rebinding tests', feature_category: :shared do
+ let(:host) { 'localhost' }
+ let(:host_ip) { '127.0.0.1' }
+ let(:rack_app) do
+ proc do |_env|
+ ['200', { 'Content-Type' => 'text/plain' }, ['Hello, world!']]
+ end
+ end
+
+ let!(:http_server) do
+ Class.new do
+ attr_accessor :sni_hostname
+
+ def initialize
+ @server = WEBrick::HTTPServer.new(
+ Port: 0,
+ SSLEnable: true,
+ SSLCertName: [%w[CN localhost]],
+ SSLServerNameCallback: proc { |args| sni_callback(*args) },
+ Logger: WEBrick::Log.new('/dev/null'),
+ AccessLog: []
+ )
+
+ @server.mount_proc '/' do |_req, res|
+ res.body = 'Hello, world!'
+ end
+
+ Thread.new { @server.start }
+ end
+
+ def port
+ @server.config[:Port]
+ end
+
+ def shutdown
+ @server.shutdown
+ end
+
+ def sni_callback(sslsocket, hostname = nil)
+ @sni_hostname = hostname
+ @server.ssl_servername_callback(sslsocket, hostname)
+ end
+ end.new
+ end
+
+ describe '#connect' do
+ before do
+ WebMock.allow_net_connect!
+ end
+
+ after do
+ WebMock.disable_net_connect! # rubocop:disable RSpec/WebMockEnable -- method not available in gem
+ http_server.shutdown
+ end
+
+ shared_examples 'GET request' do
+ it 'makes a successful HTTPS connection' do
+ http = Net::HTTP.new(http_host, http_server.port)
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ http.hostname_override = hostname_override if hostname_override
+
+ request = Net::HTTP::Get.new('/')
+
+ response = http.start { http.request(request) }
+ expect(response.code).to eq('200')
+ expect(response.body).to include('Hello, world!')
+ expect(http_server.sni_hostname).to eq(expected_sni)
+ end
+ end
+
+ context 'with hostname' do
+ let(:http_host) { host }
+ let(:expected_sni) { host }
+ let(:hostname_override) { nil }
+
+ it_behaves_like 'GET request'
+ end
+
+ context 'with IP address' do
+ let(:http_host) { host_ip }
+ let(:expected_sni) { nil }
+ let(:hostname_override) { nil }
+
+ it_behaves_like 'GET request'
+ end
+
+ context 'with hostname override' do
+ let(:http_host) { host_ip }
+ let(:hostname_override) { host }
+ let(:expected_sni) { host }
+
+ it_behaves_like 'GET request'
+ end
+ end
+end
diff --git a/gems/gitlab-http/spec/gitlab/http_v2/net_http_patch_spec.rb b/gems/gitlab-http/spec/gitlab/http_v2/net_http_patch_spec.rb
deleted file mode 100644
index f34b0d98403..00000000000
--- a/gems/gitlab-http/spec/gitlab/http_v2/net_http_patch_spec.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require 'net/http'
-
-RSpec.describe 'Net::HTTP patch proxy user and password encoding', feature_category: :shared do
- let(:net_http) { Net::HTTP.new('hostname.example') }
-
- before do
- # This file can be removed once Ruby 3.0 is no longer supported:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/396223
- skip if Gem::Version.new(Net::HTTP::VERSION) >= Gem::Version.new('0.2.0')
- end
-
- describe '#proxy_user' do
- subject { net_http.proxy_user }
-
- it { is_expected.to eq(nil) }
-
- context 'with http_proxy env' do
- let(:http_proxy) { 'http://proxy.example:8000' }
-
- before do
- stub_env('http_proxy', http_proxy)
- end
-
- it { is_expected.to eq(nil) }
-
- context 'and user:password authentication' do
- let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' }
-
- context 'when on multiuser safe platform' do
- # linux, freebsd, darwin are considered multi user safe platforms
- # See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178
-
- before do
- allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true)
- end
-
- it { is_expected.to eq 'Y\\X' }
- end
-
- context 'when not on multiuser safe platform' do
- before do
- allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false)
- end
-
- it { is_expected.to be_nil }
- end
- end
- end
- end
-
- describe '#proxy_pass' do
- subject { net_http.proxy_pass }
-
- it { is_expected.to eq(nil) }
-
- context 'with http_proxy env' do
- let(:http_proxy) { 'http://proxy.example:8000' }
-
- before do
- stub_env('http_proxy', http_proxy)
- end
-
- it { is_expected.to eq(nil) }
-
- context 'and user:password authentication' do
- let(:http_proxy) { 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' }
-
- context 'when on multiuser safe platform' do
- # linux, freebsd, darwin are considered multi user safe platforms
- # See https://github.com/ruby/net-http/blob/v0.1.1/lib/net/http.rb#L1174-L1178
-
- before do
- allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(true)
- end
-
- it { is_expected.to eq 'R%S] ?X' }
- end
-
- context 'when not on multiuser safe platform' do
- before do
- allow(net_http).to receive(:environment_variable_is_multiuser_safe?).and_return(false)
- end
-
- it { is_expected.to be_nil }
- end
- end
- end
- end
-end
diff --git a/lib/api/ci/triggers.rb b/lib/api/ci/triggers.rb
index 2ff5fae671a..e1fa0061a6c 100644
--- a/lib/api/ci/triggers.rb
+++ b/lib/api/ci/triggers.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project',
- documentation: { example: 18 }
+ documentation: { example: 18 }
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Trigger a GitLab project pipeline' do
@@ -26,11 +26,11 @@ module API
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false,
- documentation: { example: 'develop' }
+ documentation: { example: 'develop' }
requires :token, type: String, desc: 'The unique token of trigger or job token',
- documentation: { example: '6d056f63e50fe6f8c5f8f4aa10edb7' }
+ documentation: { example: '6d056f63e50fe6f8c5f8f4aa10edb7' }
optional :variables, type: Hash, desc: 'The list of variables to be injected into build',
- documentation: { example: { VAR1: "value1", VAR2: "value2" } }
+ documentation: { example: { VAR1: "value1", VAR2: "value2" } }
end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20758')
@@ -111,7 +111,7 @@ module API
end
params do
requires :description, type: String, desc: 'The trigger token description',
- documentation: { example: 'my trigger token description' }
+ documentation: { example: 'my trigger token description' }
end
post ':id/triggers' do
authenticate!
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 62b2885f955..62badec6570 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -59,21 +59,21 @@ module API
end
params do
requires :sha, type: String, desc: 'The commit hash',
- documentation: { example: '18f3e63d05582537db6d183d9d557be09e1f90c8' }
+ documentation: { example: '18f3e63d05582537db6d183d9d557be09e1f90c8' }
requires :state, type: String, desc: 'The state of the status',
- values: %w[pending running success failed canceled],
- documentation: { example: 'pending' }
+ values: %w[pending running success failed canceled],
+ documentation: { example: 'pending' }
optional :ref, type: String, desc: 'The ref',
- documentation: { example: 'develop' }
+ documentation: { example: 'develop' }
optional :target_url, type: String, desc: 'The target URL to associate with this status',
- documentation: { example: 'https://gitlab.example.com/janedoe/gitlab-foss/builds/91' }
+ documentation: { example: 'https://gitlab.example.com/janedoe/gitlab-foss/builds/91' }
optional :description, type: String, desc: 'A short description of the status'
optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems',
- documentation: { example: 'coverage', default: 'default' }
+ documentation: { example: 'coverage', default: 'default' }
optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems',
- documentation: { example: 'coverage', default: 'default' }
+ documentation: { example: 'coverage', default: 'default' }
optional :coverage, type: Float, desc: 'The total code coverage',
- documentation: { example: 100.0 }
+ documentation: { example: 100.0 }
optional :pipeline_id, type: Integer, desc: 'An existing pipeline ID, when multiple pipelines on the same commit SHA have been triggered'
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 0612b41edf8..433eb53e5b3 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -63,25 +63,25 @@ module API
end
params do
optional :ref_name,
- type: String,
- desc: 'The name of a repository branch or tag, if not given the default branch is used',
- documentation: { example: 'v1.1.0' }
+ type: String,
+ desc: 'The name of a repository branch or tag, if not given the default branch is used',
+ documentation: { example: 'v1.1.0' }
optional :since,
- type: DateTime,
- desc: 'Only commits after or on this date will be returned',
- documentation: { example: '2021-09-20T11:50:22.001' }
+ type: DateTime,
+ desc: 'Only commits after or on this date will be returned',
+ documentation: { example: '2021-09-20T11:50:22.001' }
optional :until,
- type: DateTime,
- desc: 'Only commits before or on this date will be returned',
- documentation: { example: '2021-09-20T11:50:22.001' }
+ type: DateTime,
+ desc: 'Only commits before or on this date will be returned',
+ documentation: { example: '2021-09-20T11:50:22.001' }
optional :path,
- type: String,
- desc: 'The file path',
- documentation: { example: 'README.md' }
+ type: String,
+ desc: 'The file path',
+ documentation: { example: 'README.md' }
optional :author,
- type: String,
- desc: 'Search commits by commit author',
- documentation: { example: 'John Smith' }
+ type: String,
+ desc: 'Search commits by commit author',
+ documentation: { example: 'John Smith' }
optional :all, type: Boolean, desc: 'Every commit will be returned'
optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges'
@@ -108,16 +108,16 @@ module API
author = params[:author]
commits = user_project.repository.commits(ref,
- path: path,
- limit: limit,
- offset: offset,
- before: before,
- after: after,
- all: all,
- first_parent: first_parent,
- order: order,
- author: author,
- trailers: params[:trailers])
+ path: path,
+ limit: limit,
+ offset: offset,
+ before: before,
+ after: after,
+ all: all,
+ first_parent: first_parent,
+ order: order,
+ author: author,
+ trailers: params[:trailers])
serializer = with_stats ? Entities::CommitWithStats : Entities::Commit
@@ -143,49 +143,49 @@ module API
end
params do
requires :branch,
- type: String,
- desc: 'Name of the branch to commit into. To create a new branch, also provide either `start_branch` or `start_sha`, and optionally `start_project`.',
- allow_blank: false,
- documentation: { example: 'master' }
+ type: String,
+ desc: 'Name of the branch to commit into. To create a new branch, also provide either `start_branch` or `start_sha`, and optionally `start_project`.',
+ allow_blank: false,
+ documentation: { example: 'master' }
requires :commit_message,
- type: String,
- desc: 'Commit message',
- documentation: { example: 'initial commit' }
+ type: String,
+ desc: 'Commit message',
+ documentation: { example: 'initial commit' }
requires :actions,
- type: Array,
- desc: 'Actions to perform in commit' do
+ type: Array,
+ desc: 'Actions to perform in commit' do
requires :action,
- type: String,
- desc: 'The action to perform, `create`, `delete`, `move`, `update`, `chmod`', values: %w[create update move delete chmod].freeze,
- allow_blank: false
+ type: String,
+ desc: 'The action to perform, `create`, `delete`, `move`, `update`, `chmod`', values: %w[create update move delete chmod].freeze,
+ allow_blank: false
requires :file_path,
- type: String,
- desc: 'Full path to the file.',
- documentation: { example: 'lib/class.rb' }
+ type: String,
+ desc: 'Full path to the file.',
+ documentation: { example: 'lib/class.rb' }
given action: ->(action) { action == 'move' } do
requires :previous_path,
- type: String,
- desc: 'Original full path to the file being moved.',
- documentation: { example: 'lib/class.rb' }
+ type: String,
+ desc: 'Original full path to the file being moved.',
+ documentation: { example: 'lib/class.rb' }
end
given action: ->(action) { %w[create move].include? action } do
optional :content,
- type: String,
- desc: 'File content',
- documentation: { example: 'Some file content' }
+ type: String,
+ desc: 'File content',
+ documentation: { example: 'Some file content' }
end
given action: ->(action) { action == 'update' } do
requires :content,
- type: String,
- desc: 'File content',
- documentation: { example: 'Some file content' }
+ type: String,
+ desc: 'File content',
+ documentation: { example: 'Some file content' }
end
optional :encoding, type: String, desc: '`text` or `base64`', default: 'text', values: %w[text base64]
given action: ->(action) { %w[update move delete].include? action } do
optional :last_commit_id,
- type: String,
- desc: 'Last known file commit id',
- documentation: { example: '2695effb5807a22ff3d138d593fd856244e155e7' }
+ type: String,
+ desc: 'Last known file commit id',
+ documentation: { example: '2695effb5807a22ff3d138d593fd856244e155e7' }
end
given action: ->(action) { action == 'chmod' } do
requires :execute_filemode, type: Boolean, desc: 'When `true/false` enables/disables the execute flag on the file.'
@@ -193,27 +193,27 @@ module API
end
optional :start_branch,
- type: String,
- desc: 'Name of the branch to start the new branch from',
- documentation: { example: 'staging' }
+ type: String,
+ desc: 'Name of the branch to start the new branch from',
+ documentation: { example: 'staging' }
optional :start_sha,
- type: String,
- desc: 'SHA of the commit to start the new branch from',
- documentation: { example: '2695effb5807a22ff3d138d593fd856244e155e7' }
+ type: String,
+ desc: 'SHA of the commit to start the new branch from',
+ documentation: { example: '2695effb5807a22ff3d138d593fd856244e155e7' }
mutually_exclusive :start_branch, :start_sha
optional :start_project,
- types: [Integer, String],
- desc: 'The ID or path of the project to start the new branch from',
- documentation: { example: 1 }
+ types: [Integer, String],
+ desc: 'The ID or path of the project to start the new branch from',
+ documentation: { example: 1 }
optional :author_email,
- type: String,
- desc: 'Author email for commit',
- documentation: { example: 'janedoe@example.com' }
+ type: String,
+ desc: 'Author email for commit',
+ documentation: { example: 'janedoe@example.com' }
optional :author_name,
- type: String,
- desc: 'Author name for commit',
- documentation: { example: 'Jane Doe' }
+ type: String,
+ desc: 'Author name for commit',
+ documentation: { example: 'Jane Doe' }
optional :stats, type: Boolean, default: true, desc: 'Include commit stats'
optional :force, type: Boolean, default: false, desc: 'When `true` overwrites the target branch with a new commit based on the `start_branch` or `start_sha`'
end
@@ -342,15 +342,15 @@ module API
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
requires :branch,
- type: String,
- desc: 'The name of the branch',
- allow_blank: false,
- documentation: { example: 'master' }
+ type: String,
+ desc: 'The name of the branch',
+ allow_blank: false,
+ documentation: { example: 'master' }
optional :dry_run, type: Boolean, default: false, desc: "Does not commit any changes"
optional :message,
- type: String,
- desc: 'A custom commit message to use for the picked commit',
- documentation: { example: 'Initial commit' }
+ type: String,
+ desc: 'A custom commit message to use for the picked commit',
+ documentation: { example: 'Initial commit' }
end
post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize_push_to_branch!(params[:branch])
@@ -400,10 +400,10 @@ module API
params do
requires :sha, type: String, desc: 'Commit SHA to revert'
requires :branch,
- type: String,
- desc: 'Target branch name',
- allow_blank: false,
- documentation: { example: 'master' }
+ type: String,
+ desc: 'Target branch name',
+ allow_blank: false,
+ documentation: { example: 'master' }
optional :dry_run, type: Boolean, default: false, desc: "Does not commit any changes"
end
post ':id/repository/commits/:sha/revert', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
@@ -478,18 +478,18 @@ module API
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to post a comment'
requires :note,
- type: String,
- desc: 'The text of the comment',
- documentation: { example: 'Nice code!' }
+ type: String,
+ desc: 'The text of the comment',
+ documentation: { example: 'Nice code!' }
optional :path,
- type: String,
- desc: 'The file path',
- documentation: { example: 'doc/update/5.4-to-6.0.md' }
+ type: String,
+ desc: 'The file path',
+ documentation: { example: 'doc/update/5.4-to-6.0.md' }
given :path do
requires :line,
- type: Integer,
- desc: 'The line number',
- documentation: { example: 11 }
+ type: Integer,
+ desc: 'The line number',
+ documentation: { example: 11 }
requires :line_type, type: String, values: %w[new old], default: 'new', desc: 'The type of the line'
end
end
diff --git a/lib/api/concerns/packages/debian_distribution_endpoints.rb b/lib/api/concerns/packages/debian_distribution_endpoints.rb
index 7aad3a56422..725c4c9f71f 100644
--- a/lib/api/concerns/packages/debian_distribution_endpoints.rb
+++ b/lib/api/concerns/packages/debian_distribution_endpoints.rb
@@ -33,15 +33,15 @@ module API
optional :valid_time_duration_seconds, type: Integer, desc: 'The duration before the Release file should be considered expired by the client', documentation: { example: 604800 }
optional :components, type: Array[String],
- coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- regexp: Gitlab::Regex.debian_component_regex,
- desc: 'The list of Components',
- documentation: { example: 'main' }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
+ regexp: Gitlab::Regex.debian_component_regex,
+ desc: 'The list of Components',
+ documentation: { example: 'main' }
optional :architectures, type: Array[String],
- coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- regexp: Gitlab::Regex.debian_architecture_regex,
- desc: 'The list of Architectures',
- documentation: { example: 'amd64' }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
+ regexp: Gitlab::Regex.debian_architecture_regex,
+ desc: 'The list of Architectures',
+ documentation: { example: 'amd64' }
end
end
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index 69d23c408ab..24503f2e33e 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -83,7 +83,7 @@ module API
tags %w[npm_packages]
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true,
- authenticate_non_public: true
+ authenticate_non_public: true
get 'dist-tags', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
@@ -198,7 +198,7 @@ module API
use :package_name
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true,
- authenticate_non_public: true
+ authenticate_non_public: true
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
available_packages =
diff --git a/lib/api/container_repositories.rb b/lib/api/container_repositories.rb
index b6b5fe10332..1d4d36ac3cf 100644
--- a/lib/api/container_repositories.rb
+++ b/lib/api/container_repositories.rb
@@ -35,11 +35,11 @@ module API
authorize!(:read_container_image, repository)
present repository,
- with: Entities::ContainerRegistry::Repository,
- tags: params[:tags],
- tags_count: params[:tags_count],
- size: params[:size],
- user: current_user
+ with: Entities::ContainerRegistry::Repository,
+ tags: params[:tags],
+ tags_count: params[:tags_count],
+ size: params[:size],
+ user: current_user
end
end
end
diff --git a/lib/api/dependency_proxy.rb b/lib/api/dependency_proxy.rb
index 9ab96d4463a..5e6d33fbd98 100644
--- a/lib/api/dependency_proxy.rb
+++ b/lib/api/dependency_proxy.rb
@@ -13,7 +13,7 @@ module API
params do
requires :id, types: [String, Integer],
- desc: 'The ID or URL-encoded path of the group owned by the authenticated user'
+ desc: 'The ID or URL-encoded path of the group owned by the authenticated user'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Purge the dependency proxy for a group' do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 1cf1228b210..c238300af74 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -199,7 +199,7 @@ module API
end
post ":id/deploy_keys/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared_params).execute
+ current_user, declared_params).execute
if key
present key, with: Entities::DeployKey
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index 975a65af285..78f55d188bc 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -87,10 +87,10 @@ module API
params do
requires :name, type: String, desc: "New deploy token's name"
requires :scopes,
- type: Array[String],
- coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
- desc: 'Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`.'
+ type: Array[String],
+ coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
+ values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
+ desc: 'Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`.'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`).'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
end
@@ -194,10 +194,10 @@ module API
params do
requires :name, type: String, desc: "New deploy token's name"
requires :scopes,
- type: Array[String],
- coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
- desc: 'Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`'
+ type: Array[String],
+ coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
+ values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
+ desc: 'Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`)'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index 1468164081c..ad67038584f 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -176,9 +176,9 @@ module API
end
params do
requires :status,
- type: String,
- desc: 'The new status of the deployment. One of `running`, `success`, `failed`, or `canceled`',
- values: %w[running success failed canceled]
+ type: String,
+ desc: 'The new status of the deployment. One of `running`, `success`, `failed`, or `canceled`',
+ values: %w[running success failed canceled]
end
route_setting :authentication, job_token_allowed: true
put ':id/deployments/:deployment_id' do
diff --git a/lib/api/entities/application.rb b/lib/api/entities/application.rb
index c3d8f9667c3..4eacb7d8c15 100644
--- a/lib/api/entities/application.rb
+++ b/lib/api/entities/application.rb
@@ -5,8 +5,8 @@ module API
class Application < Grape::Entity
expose :id
expose :uid, as: :application_id,
- documentation: { type: 'string',
- example: '5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737' }
+ documentation: { type: 'string',
+ example: '5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737' }
expose :name, as: :application_name, documentation: { type: 'string', example: 'MyApplication' }
expose :redirect_uri, as: :callback_url, documentation: { type: 'string', example: 'https://redirect.uri' }
expose :confidential, documentation: { type: 'boolean', example: true }
diff --git a/lib/api/entities/application_statistics.rb b/lib/api/entities/application_statistics.rb
index 7e75ef23675..128da3eb3c1 100644
--- a/lib/api/entities/application_statistics.rb
+++ b/lib/api/entities/application_statistics.rb
@@ -7,37 +7,37 @@ module API
include CountHelper
expose :forks,
- documentation: { type: 'integer', example: 6, desc: 'Approximate number of repo forks' } do |counts|
+ documentation: { type: 'integer', example: 6, desc: 'Approximate number of repo forks' } do |counts|
approximate_fork_count_with_delimiters(counts)
end
expose :issues,
- documentation: { type: 'integer', example: 121, desc: 'Approximate number of issues' } do |counts|
+ documentation: { type: 'integer', example: 121, desc: 'Approximate number of issues' } do |counts|
approximate_count_with_delimiters(counts, ::Issue)
end
expose :merge_requests,
- documentation: { type: 'integer', example: 49, desc: 'Approximate number of merge requests' } do |counts|
+ documentation: { type: 'integer', example: 49, desc: 'Approximate number of merge requests' } do |counts|
approximate_count_with_delimiters(counts, ::MergeRequest)
end
expose :notes,
- documentation: { type: 'integer', example: 6, desc: 'Approximate number of notes' } do |counts|
+ documentation: { type: 'integer', example: 6, desc: 'Approximate number of notes' } do |counts|
approximate_count_with_delimiters(counts, ::Note)
end
expose :snippets,
- documentation: { type: 'integer', example: 4, desc: 'Approximate number of snippets' } do |counts|
+ documentation: { type: 'integer', example: 4, desc: 'Approximate number of snippets' } do |counts|
approximate_count_with_delimiters(counts, ::Snippet)
end
expose :ssh_keys,
- documentation: { type: 'integer', example: 11, desc: 'Approximate number of SSH keys' } do |counts|
+ documentation: { type: 'integer', example: 11, desc: 'Approximate number of SSH keys' } do |counts|
approximate_count_with_delimiters(counts, ::Key)
end
expose :milestones,
- documentation: { type: 'integer', example: 3, desc: 'Approximate number of milestones' } do |counts|
+ documentation: { type: 'integer', example: 3, desc: 'Approximate number of milestones' } do |counts|
approximate_count_with_delimiters(counts, ::Milestone)
end
@@ -46,17 +46,17 @@ module API
end
expose :projects,
- documentation: { type: 'integer', example: 4, desc: 'Approximate number of projects' } do |counts|
+ documentation: { type: 'integer', example: 4, desc: 'Approximate number of projects' } do |counts|
approximate_count_with_delimiters(counts, ::Project)
end
expose :groups,
- documentation: { type: 'integer', example: 1, desc: 'Approximate number of projects' } do |counts|
+ documentation: { type: 'integer', example: 1, desc: 'Approximate number of projects' } do |counts|
approximate_count_with_delimiters(counts, ::Group)
end
expose :active_users,
- documentation: { type: 'integer', example: 21, desc: 'Number of active users' } do |_|
+ documentation: { type: 'integer', example: 21, desc: 'Number of active users' } do |_|
number_with_delimiter(::User.active.count)
end
end
diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb
index 01eaf5c8d31..7a6a3da6e69 100644
--- a/lib/api/entities/branch.rb
+++ b/lib/api/entities/branch.rb
@@ -12,10 +12,10 @@ module API
end
expose :merged,
- documentation: {
- type: 'boolean',
- example: true
- } do |repo_branch, options|
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
if options[:merged_branch_names]
options[:merged_branch_names].include?(repo_branch.name)
else
@@ -24,50 +24,50 @@ module API
end
expose :protected,
- documentation: {
- type: 'boolean',
- example: true
- } do |repo_branch, options|
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
::ProtectedBranch.protected?(options[:project], repo_branch.name)
end
expose :developers_can_push,
- documentation: {
- type: 'boolean',
- example: true
- } do |repo_branch, options|
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
end
expose :developers_can_merge,
- documentation: {
- type: 'boolean',
- example: true
- } do |repo_branch, options|
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
end
expose :can_push,
- documentation: {
- type: 'boolean',
- example: true
- } do |repo_branch, options|
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
Gitlab::UserAccess.new(options[:current_user], container: options[:project]).can_push_to_branch?(repo_branch.name)
end
expose :default,
- documentation: {
- type: 'boolean',
- example: true
- } do |repo_branch, options|
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
options[:project].default_branch == repo_branch.name
end
expose :web_url,
- documentation: {
- type: 'string',
- example: 'https://gitlab.example.com/Commit921/the-dude/-/tree/master'
- } do |repo_branch|
+ documentation: {
+ type: 'string',
+ example: 'https://gitlab.example.com/Commit921/the-dude/-/tree/master'
+ } do |repo_branch|
project_tree_url(options[:project], repo_branch.name)
end
end
diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb
index aacef907861..9ed98dc3868 100644
--- a/lib/api/generic_packages.rb
+++ b/lib/api/generic_packages.rb
@@ -161,7 +161,7 @@ module API
def encoded_file_name
file_name = [declared_params[:path], declared_params[:file_name]].compact.join('/')
- URI.encode_uri_component(file_name)
+ declared_params[:path].present? ? URI.encode_uri_component(file_name) : file_name
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index e704e5d5f52..52ef67d1f9e 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -182,8 +182,8 @@ module API
params do
use :merge_requests_params
optional :non_archived, type: Boolean,
- default: true,
- desc: 'Returns merge requests from non archived projects only.'
+ default: true,
+ desc: 'Returns merge requests from non archived projects only.'
end
get ":id/merge_requests", feature_category: :code_review_workflow, urgency: :low do
validate_search_rate_limit! if declared_params[:search].present?
@@ -211,26 +211,26 @@ module API
params :optional_params do
optional :assignee_id, type: Integer, desc: 'Assignee user ID.'
optional :assignee_ids, type: Array[Integer],
- coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
- desc: 'The IDs of the users to assign the merge request to, as a comma-separated list. Set to 0 or provide an empty value to unassign all assignees.',
- documentation: { is_array: true }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
+ desc: 'The IDs of the users to assign the merge request to, as a comma-separated list. Set to 0 or provide an empty value to unassign all assignees.',
+ documentation: { is_array: true }
optional :reviewer_ids, type: Array[Integer],
- coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
- desc: 'The IDs of the users to review the merge request, as a comma-separated list. Set to 0 or provide an empty value to unassign all reviewers.',
- documentation: { is_array: true }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
+ desc: 'The IDs of the users to review the merge request, as a comma-separated list. Set to 0 or provide an empty value to unassign all reviewers.',
+ documentation: { is_array: true }
optional :description, type: String, desc: 'Description of the merge request. Limited to 1,048,576 characters.'
optional :labels, type: Array[String],
- coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
- desc: 'Comma-separated label names for a merge request. Set to an empty string to unassign all labels.',
- documentation: { is_array: true }
+ coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
+ desc: 'Comma-separated label names for a merge request. Set to an empty string to unassign all labels.',
+ documentation: { is_array: true }
optional :add_labels, type: Array[String],
- coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
- desc: 'Comma-separated label names to add to a merge request.',
- documentation: { is_array: true }
+ coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
+ desc: 'Comma-separated label names to add to a merge request.',
+ documentation: { is_array: true }
optional :remove_labels, type: Array[String],
- coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
- desc: 'Comma-separated label names to remove from a merge request.',
- documentation: { is_array: true }
+ coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
+ desc: 'Comma-separated label names to remove from a merge request.',
+ documentation: { is_array: true }
optional :milestone_id, type: Integer, desc: 'The global ID of a milestone to assign the merge reques to.'
optional :remove_source_branch, type: Boolean, desc: 'Flag indicating if a merge request should remove the source branch when merging.'
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch.'
@@ -259,9 +259,9 @@ module API
use :merge_requests_params
optional :iids, type: Array[Integer],
- coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
- desc: 'Returns the request having the given `iid`.',
- documentation: { is_array: true }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce,
+ desc: 'Returns the request having the given `iid`.',
+ documentation: { is_array: true }
end
get ":id/merge_requests", feature_category: :code_review_workflow, urgency: :low do
authorize! :read_merge_request, user_project
@@ -305,7 +305,7 @@ module API
requires :source_branch, type: String, desc: 'The source branch.'
requires :target_branch, type: String, desc: 'The target branch.'
optional :target_project_id, type: Integer,
- desc: 'The target project of the merge request defaults to the :id of the project.'
+ desc: 'The target project of the merge request defaults to the :id of the project.'
use :optional_params
end
post ":id/merge_requests", feature_category: :code_review_workflow, urgency: :low do
@@ -442,10 +442,10 @@ module API
params do
requires :commits, type: Array[String],
- coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- allow_blank: false,
- desc: 'The context commits’ SHA.',
- documentation: { is_array: true }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
+ allow_blank: false,
+ desc: 'The context commits’ SHA.',
+ documentation: { is_array: true }
end
desc 'Create merge request context commits' do
detail 'Create a list of merge request context commits.'
@@ -479,10 +479,10 @@ module API
params do
requires :commits, type: Array[String],
- coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
- allow_blank: false,
- desc: 'The context commits’ SHA.',
- documentation: { is_array: true }
+ coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce,
+ allow_blank: false,
+ desc: 'The context commits’ SHA.',
+ documentation: { is_array: true }
end
desc 'Delete merge request context commits' do
detail 'Delete a list of merge request context commits.'
@@ -604,10 +604,10 @@ module API
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request.'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch.'
optional :state_event, type: String,
- values: %w[close reopen],
- desc: 'New state (close/reopen).'
+ values: %w[close reopen],
+ desc: 'New state (close/reopen).'
optional :discussion_locked, type: Boolean,
- desc: 'Flag indicating if the merge request’s discussion is locked. If the discussion is locked only project members can add, edit or resolve comments.'
+ desc: 'Flag indicating if the merge request’s discussion is locked. If the discussion is locked only project members can add, edit or resolve comments.'
use :optional_params
at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
@@ -648,9 +648,9 @@ module API
optional :merge_commit_message, type: String, desc: 'Custom merge commit message.'
optional :squash_commit_message, type: String, desc: 'Custom squash commit message.'
optional :should_remove_source_branch, type: Boolean,
- desc: 'If `true`, removes the source branch.'
+ desc: 'If `true`, removes the source branch.'
optional :merge_when_pipeline_succeeds, type: Boolean,
- desc: 'If `true`, the merge request is merged when the pipeline succeeds.'
+ desc: 'If `true`, the merge request is merged when the pipeline succeeds.'
optional :sha, type: String, desc: 'If present, then this SHA must match the HEAD of the source branch, otherwise the merge fails.'
optional :squash, type: Grape::API::Boolean, desc: 'If `true`, the commits are squashed into a single commit on merge.'
diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb
index 0acc66e4197..9aa9ffcf28c 100644
--- a/lib/api/metrics/dashboard/annotations.rb
+++ b/lib/api/metrics/dashboard/annotations.rb
@@ -27,15 +27,15 @@ module API
resource annotations_source[:resource] do
params do
requires :starting_at, type: DateTime,
- desc: 'Date time string, ISO 8601 formatted, such as 2016-03-11T03:45:40Z.'\
- 'Timestamp marking start point of annotation.'
+ desc: 'Date time string, ISO 8601 formatted, such as 2016-03-11T03:45:40Z.'\
+ 'Timestamp marking start point of annotation.'
optional :ending_at, type: DateTime,
- desc: 'Date time string, ISO 8601 formatted, such as 2016-03-11T03:45:40Z.'\
- 'Timestamp marking end point of annotation.'\
- 'When not supplied, an annotation displays as a single event at the start point.'
+ desc: 'Date time string, ISO 8601 formatted, such as 2016-03-11T03:45:40Z.'\
+ 'Timestamp marking end point of annotation.'\
+ 'When not supplied, an annotation displays as a single event at the start point.'
requires :dashboard_path, type: String, coerce_with: ->(val) { CGI.unescape(val) },
- desc: 'ID of the dashboard which needs to be annotated.'\
- 'Treated as a CGI-escaped path, and automatically un-escaped.'
+ desc: 'ID of the dashboard which needs to be annotated.'\
+ 'Treated as a CGI-escaped path, and automatically un-escaped.'
requires :description, type: String, desc: 'Description of the annotation.'
end
diff --git a/lib/api/metrics/user_starred_dashboards.rb b/lib/api/metrics/user_starred_dashboards.rb
index eeb1efb9001..fdd509010ff 100644
--- a/lib/api/metrics/user_starred_dashboards.rb
+++ b/lib/api/metrics/user_starred_dashboards.rb
@@ -21,7 +21,7 @@ module API
params do
requires :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
- desc: 'URL-encoded path to file defining the dashboard which should be marked as favorite'
+ desc: 'URL-encoded path to file defining the dashboard which should be marked as favorite'
end
post ':id/metrics/user_starred_dashboards' do
@@ -40,7 +40,7 @@ module API
params do
optional :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
- desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
+ desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
end
delete ':id/metrics/user_starred_dashboards' do
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index 3ef30a5fcd3..9250bcf6eef 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -16,7 +16,7 @@ module API
params :list_params do
optional :state, type: String, values: %w[active closed all], default: 'all',
- desc: 'Return "active", "closed", or "all" milestones'
+ desc: 'Return "active", "closed", or "all" milestones'
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IIDs of the milestones'
optional :title, type: String, desc: 'The title of the milestones'
optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
@@ -32,7 +32,7 @@ module API
requires :milestone_id, type: Integer, desc: 'The milestone ID number'
optional :title, type: String, desc: 'The title of the milestone'
optional :state_event, type: String, values: %w[close activate],
- desc: 'The state event of the milestone '
+ desc: 'The state event of the milestone '
use :optional_params
at_least_one_of :title, :description, :start_date, :due_date, :state_event
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 138b2fddc83..ff2dedf31bd 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -26,11 +26,11 @@ module API
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
- desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
+ desc: 'Return notes ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return notes sorted in `asc` or `desc` order.'
+ desc: 'Return notes sorted in `asc` or `desc` order.'
optional :activity_filter, type: String, values: UserPreference::NOTES_FILTERS.stringify_keys.keys, default: 'all_notes',
- desc: 'The type of notables which are returned.'
+ desc: 'The type of notables which are returned.'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb
index e25b47397a7..eabe7ad161a 100644
--- a/lib/api/nuget_project_packages.rb
+++ b/lib/api/nuget_project_packages.rb
@@ -201,7 +201,7 @@ module API
end
get 'index', format: :json, urgency: :low do
present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])),
- with: ::API::Entities::Nuget::PackagesVersions
+ with: ::API::Entities::Nuget::PackagesVersions
end
desc 'The NuGet Content Service - content request' do
@@ -382,7 +382,7 @@ module API
params do
requires :project_id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project',
- regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
+ regexp: ::API::Concerns::Packages::Nuget::PrivateEndpoints::POSITIVE_INTEGER_REGEX
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':project_id/packages/nuget/v2' do
@@ -399,8 +399,8 @@ module API
params do
requires :id, as: :package_name, type: String, allow_blank: false, coerce_with: ->(val) { val.delete("'") },
- desc: 'The NuGet package name', regexp: Gitlab::Regex.nuget_package_name_regex,
- documentation: { example: 'mynugetpkg' }
+ desc: 'The NuGet package name', regexp: Gitlab::Regex.nuget_package_name_regex,
+ documentation: { example: 'mynugetpkg' }
end
get 'FindPackagesById\(\)', urgency: :low do
present_odata_entry
@@ -419,9 +419,9 @@ module API
params do
requires :$filter, as: :package_name, type: String, allow_blank: false,
- coerce_with: ->(val) { val.match(/tolower\(Id\) eq '(.+?)'/)&.captures&.first },
- desc: 'The NuGet package name', regexp: Gitlab::Regex.nuget_package_name_regex,
- documentation: { example: 'mynugetpkg' }
+ coerce_with: ->(val) { val.match(/tolower\(Id\) eq '(.+?)'/)&.captures&.first },
+ desc: 'The NuGet package name', regexp: Gitlab::Regex.nuget_package_name_regex,
+ documentation: { example: 'mynugetpkg' }
end
get 'Packages\(\)', urgency: :low do
present_odata_entry
@@ -439,9 +439,9 @@ module API
end
params do
requires :package_name, type: String, allow_blank: false, desc: 'The NuGet package name',
- regexp: Gitlab::Regex.nuget_package_name_regex, documentation: { example: 'mynugetpkg' }
+ regexp: Gitlab::Regex.nuget_package_name_regex, documentation: { example: 'mynugetpkg' }
requires :package_version, type: String, allow_blank: false, desc: 'The NuGet package version',
- regexp: Gitlab::Regex.nuget_version_regex, documentation: { example: '1.3.0.17' }
+ regexp: Gitlab::Regex.nuget_version_regex, documentation: { example: '1.3.0.17' }
end
get 'Packages\(Id=\'*package_name\',Version=\'*package_version\'\)', urgency: :low do
present_odata_entry
diff --git a/lib/api/pages.rb b/lib/api/pages.rb
index 8f038c175bd..995e2abfd84 100644
--- a/lib/api/pages.rb
+++ b/lib/api/pages.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, types: [String, Integer],
- desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
+ desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Unpublish pages' do
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index db156186458..01b93537896 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -99,7 +99,7 @@ module API
optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
optional :auto_ssl_enabled, allow_blank: false, type: Boolean, default: false,
- desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
+ desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
all_or_none_of :user_provided_certificate, :user_provided_key
end
@@ -127,7 +127,7 @@ module API
optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
optional :auto_ssl_enabled, allow_blank: true, type: Boolean,
- desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
+ desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
end
put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb
index c3505780396..14071b4d4f1 100644
--- a/lib/api/pagination_params.rb
+++ b/lib/api/pagination_params.rb
@@ -19,7 +19,7 @@ module API
params :pagination do
optional :page, type: Integer, default: 1, desc: 'Current page number', documentation: { example: 1 }
optional :per_page, type: Integer, default: 20,
- desc: 'Number of items per page', except_values: [0], documentation: { example: 20 }
+ desc: 'Number of items per page', except_values: [0], documentation: { example: 20 }
end
def verify_pagination_params!
diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb
index 8bcacc7f32f..32f7726ca94 100644
--- a/lib/api/personal_access_tokens.rb
+++ b/lib/api/personal_access_tokens.rb
@@ -26,17 +26,17 @@ module API
params do
optional :user_id, type: Integer, desc: 'Filter PATs by User ID', documentation: { example: 2 }
optional :revoked, type: Boolean, desc: 'Filter PATs where revoked state matches parameter',
- documentation: { example: false }
+ documentation: { example: false }
optional :state, type: String, desc: 'Filter PATs which are either active or not',
- values: %w[active inactive], documentation: { example: 'active' }
+ values: %w[active inactive], documentation: { example: 'active' }
optional :created_before, type: DateTime, desc: 'Filter PATs which were created before given datetime',
- documentation: { example: '2022-01-01' }
+ documentation: { example: '2022-01-01' }
optional :created_after, type: DateTime, desc: 'Filter PATs which were created after given datetime',
- documentation: { example: '2021-01-01' }
+ documentation: { example: '2021-01-01' }
optional :last_used_before, type: DateTime, desc: 'Filter PATs which were used before given datetime',
- documentation: { example: '2021-01-01' }
+ documentation: { example: '2021-01-01' }
optional :last_used_after, type: DateTime, desc: 'Filter PATs which were used after given datetime',
- documentation: { example: '2022-01-01' }
+ documentation: { example: '2022-01-01' }
optional :search, type: String, desc: 'Filters PATs by its name', documentation: { example: 'token' }
use :pagination
@@ -74,9 +74,9 @@ module API
end
params do
optional :expires_at,
- type: Date,
- desc: "The expiration date of the token",
- documentation: { example: '2021-01-31' }
+ type: Date,
+ desc: "The expiration date of the token",
+ documentation: { example: '2021-01-31' }
end
post ':id/rotate' do
token = PersonalAccessToken.find_by_id(params[:id])
diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb
index 9e1b81005b0..db112c54673 100644
--- a/lib/api/project_container_repositories.rb
+++ b/lib/api/project_container_repositories.rb
@@ -215,7 +215,7 @@ module API
def obtain_new_cleanup_container_lease
Gitlab::ExclusiveLease
.new("container_repository:cleanup_tags:#{repository.id}",
- timeout: 1.hour)
+ timeout: 1.hour)
.try_obtain
end
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index 25848d91550..b7e76b19dd5 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -75,7 +75,7 @@ module API
optional :upload, type: Hash do
optional :url, type: String, desc: 'The URL to upload the project'
optional :http_method, type: String, default: 'PUT', values: %w[PUT POST],
- desc: 'HTTP method to upload the exported project'
+ desc: 'HTTP method to upload the exported project'
end
end
post ':id/export' do
@@ -97,8 +97,8 @@ module API
else
begin
user_project.add_export_job(current_user: current_user,
- after_export_strategy: export_strategy,
- params: project_export_params)
+ after_export_strategy: export_strategy,
+ params: project_export_params)
rescue Project::ExportLimitExceeded => e
render_api_error!(e.message, 400)
end
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index 83b4e15ac27..c7fd491d5b7 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -59,8 +59,8 @@ module API
optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace."
optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it'
optional :override_params,
- type: Hash,
- desc: 'New project params to override values in the export' do
+ type: Hash,
+ desc: 'New project params to override values in the export' do
use :optional_project_params
end
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index 3fcb36c4714..3ece0d6b4bb 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -113,7 +113,7 @@ module Banzai
def link_to_group(group, namespace, link_content: nil)
url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(group: namespace.id)
- content = link_content || Group.reference_prefix + group
+ content = link_content || (Group.reference_prefix + group)
link_tag(url, data, content, namespace.full_name)
end
@@ -121,7 +121,7 @@ module Banzai
def link_to_user(user, namespace, link_content: nil)
url = urls.user_url(user, only_path: context[:only_path])
data = data_attribute(user: namespace.owner_id)
- content = link_content || User.reference_prefix + user
+ content = link_content || (User.reference_prefix + user)
link_tag(url, data, content, namespace.owner_name)
end
@@ -151,7 +151,7 @@ module Banzai
data = data_attribute(project: project.id, author: author.try(:id))
end
- content = link_content || User.reference_prefix + 'all'
+ content = link_content || (User.reference_prefix + 'all')
link_tag(url, data, content, 'All Project and Group Members')
end
end
diff --git a/lib/banzai/filter_array.rb b/lib/banzai/filter_array.rb
index 818af4643a7..331255b251b 100644
--- a/lib/banzai/filter_array.rb
+++ b/lib/banzai/filter_array.rb
@@ -7,7 +7,7 @@ module Banzai
# If the preceding value does not exist, the new value is added to the end
# of the Array.
def insert_after(after_value, value)
- i = index(after_value) || length - 1
+ i = index(after_value) || (length - 1)
insert(i + 1, value)
end
diff --git a/lib/extracts_ref.rb b/lib/extracts_ref.rb
index afa6e7804c1..355fd8486ff 100644
--- a/lib/extracts_ref.rb
+++ b/lib/extracts_ref.rb
@@ -144,7 +144,7 @@ module ExtractsRef
allowed_params = params.permit(:id, :ref, :path)
id = [allowed_params[:id] || allowed_params[:ref]]
- id << "/" + allowed_params[:path] unless allowed_params[:path].blank?
+ id << ("/" + allowed_params[:path]) unless allowed_params[:path].blank?
id.join
end
diff --git a/lib/gitlab/background_migration/backfill_boards_epic_user_preferences_group_id.rb b/lib/gitlab/background_migration/backfill_boards_epic_user_preferences_group_id.rb
new file mode 100644
index 00000000000..7af9cdad5f6
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_boards_epic_user_preferences_group_id.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop: disable Migration/BackgroundMigrationBaseClass -- BackfillDesiredShardingKeyJob inherits from BatchedMigrationJob.
+ class BackfillBoardsEpicUserPreferencesGroupId < BackfillDesiredShardingKeyJob
+ operation_name :backfill_boards_epic_user_preferences_group_id
+ feature_category :portfolio_management
+ end
+ # rubocop: enable Migration/BackgroundMigrationBaseClass
+ end
+end
diff --git a/lib/gitlab/chaos.rb b/lib/gitlab/chaos.rb
index 1b4a647d16f..dcbf0d60007 100644
--- a/lib/gitlab/chaos.rb
+++ b/lib/gitlab/chaos.rb
@@ -11,7 +11,7 @@ module Gitlab
retainer = []
# Add `n` 1mb chunks of memory to the retainer array
- memory_mb.times { retainer << "x" * 1.megabyte }
+ memory_mb.times { retainer << ("x" * 1.megabyte) }
duration_left = [start_time + duration_s - Time.now, 0].max
Kernel.sleep(duration_left)
diff --git a/lib/gitlab/ci/config/normalizer/number_strategy.rb b/lib/gitlab/ci/config/normalizer/number_strategy.rb
index 4754e7b46d4..b5b51f22638 100644
--- a/lib/gitlab/ci/config/normalizer/number_strategy.rb
+++ b/lib/gitlab/ci/config/normalizer/number_strategy.rb
@@ -7,7 +7,7 @@ module Gitlab
class NumberStrategy
class << self
def applies_to?(config)
- config.is_a?(Integer) || config.is_a?(Hash) && config.key?(:number)
+ config.is_a?(Integer) || (config.is_a?(Hash) && config.key?(:number))
end
def build_from(job_name, config)
diff --git a/lib/gitlab/console.rb b/lib/gitlab/console.rb
index c3c34bb0f61..8babc09f3fe 100644
--- a/lib/gitlab/console.rb
+++ b/lib/gitlab/console.rb
@@ -32,7 +32,7 @@ module Gitlab
else
boot_time_seconds = Gitlab::Metrics::BootTimeTracker.instance.startup_time
booted_in = "[ booted in %.2fs ]" % [boot_time_seconds]
- puts '-' * (80 - booted_in.length) + booted_in
+ puts ('-' * (80 - booted_in.length)) + booted_in
end
end
end
diff --git a/lib/gitlab/database/background_migration/batch_metrics.rb b/lib/gitlab/database/background_migration/batch_metrics.rb
index 14fe0c14c24..68176a80de2 100644
--- a/lib/gitlab/database/background_migration/batch_metrics.rb
+++ b/lib/gitlab/database/background_migration/batch_metrics.rb
@@ -21,7 +21,7 @@ module Gitlab
count = yield
- timings_for_label(label) << monotonic_time - start_time
+ timings_for_label(label) << (monotonic_time - start_time)
affected_rows_for_label(label) << count if instrument_affected_rows && count.is_a?(Integer)
end
diff --git a/lib/gitlab/database/background_migration/batched_migration.rb b/lib/gitlab/database/background_migration/batched_migration.rb
index 077a37f0a94..7a49c2a39cf 100644
--- a/lib/gitlab/database/background_migration/batched_migration.rb
+++ b/lib/gitlab/database/background_migration/batched_migration.rb
@@ -208,11 +208,11 @@ module Gitlab
efficiencies = jobs.map(&:time_efficiency).reject(&:nil?).each_with_index
dividend = efficiencies.reduce(0) do |total, (job_eff, i)|
- total + job_eff * (1 - alpha)**i
+ total + (job_eff * ((1 - alpha)**i))
end
divisor = efficiencies.reduce(0) do |total, (job_eff, i)|
- total + (1 - alpha)**i
+ total + ((1 - alpha)**i)
end
return if divisor == 0
diff --git a/lib/gitlab/database/migrations/background_migration_helpers.rb b/lib/gitlab/database/migrations/background_migration_helpers.rb
index 25e75a10bb3..902a746f601 100644
--- a/lib/gitlab/database/migrations/background_migration_helpers.rb
+++ b/lib/gitlab/database/migrations/background_migration_helpers.rb
@@ -81,7 +81,7 @@ module Gitlab
# `SingleDatabaseWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
# spread the work over time.
- final_delay = initial_delay + delay_interval * index
+ final_delay = initial_delay + (delay_interval * index)
full_job_arguments = [start_id, end_id] + other_job_arguments
track_in_database(job_class_name, full_job_arguments) if track_jobs
@@ -90,7 +90,7 @@ module Gitlab
batch_counter += 1
end
- duration = initial_delay + delay_interval * batch_counter
+ duration = initial_delay + (delay_interval * batch_counter)
say <<~SAY
Scheduled #{batch_counter} #{job_class_name} jobs with a maximum of #{batch_size} records per batch and an interval of #{delay_interval} seconds.
@@ -133,7 +133,7 @@ module Gitlab
jobs = Gitlab::Database::BackgroundMigrationJob.pending.where(class_name: job_class_name)
jobs.each_batch(of: batch_size) do |job_batch|
job_batch.each do |job|
- final_delay = initial_delay + delay_interval * job_counter
+ final_delay = initial_delay + (delay_interval * job_counter)
migrate_in(final_delay, job_class_name, job.arguments, coordinator: job_coordinator)
@@ -141,7 +141,7 @@ module Gitlab
end
end
- duration = initial_delay + delay_interval * job_counter
+ duration = initial_delay + (delay_interval * job_counter)
say <<~SAY
Scheduled #{job_counter} #{job_class_name} jobs with an interval of #{delay_interval} seconds.
@@ -226,8 +226,8 @@ module Gitlab
self.allowed_gitlab_schemas if self.respond_to?(:allowed_gitlab_schemas)
end
- def with_migration_context(&block)
- Gitlab::ApplicationContext.with_context(caller_id: self.class.to_s, &block)
+ def with_migration_context(&)
+ Gitlab::ApplicationContext.with_context(caller_id: self.class.to_s, &)
end
def track_in_database(class_name, arguments)
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index f7e6a07d8f9..ef4c9eb29e8 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -59,7 +59,7 @@ module Gitlab
max_id = Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do
- define_batchable_model(table_name, connection: connection).maximum(column_name) || partition_size * PARTITION_BUFFER
+ define_batchable_model(table_name, connection: connection).maximum(column_name) || (partition_size * PARTITION_BUFFER)
end
end
@@ -100,7 +100,7 @@ module Gitlab
max_date ||= Date.today + 1.month
Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do
- min_date ||= connection.select_one(<<~SQL)['minimum'] || max_date - 1.month
+ min_date ||= connection.select_one(<<~SQL)['minimum'] || (max_date - 1.month)
SELECT date_trunc('MONTH', MIN(#{column_name})) AS minimum
FROM #{table_name}
SQL
@@ -469,7 +469,7 @@ module Gitlab
lower_bound = min_id
upper_bound = min_id + partition_size
- end_id = max_id + PARTITION_BUFFER * partition_size # Adds a buffer of 6 partitions
+ end_id = max_id + (PARTITION_BUFFER * partition_size) # Adds a buffer of 6 partitions
while lower_bound < end_id
create_range_partition_safely("#{table_name}_#{lower_bound}", table_name, lower_bound, upper_bound)
diff --git a/lib/gitlab/database/postgres_hll/buckets.rb b/lib/gitlab/database/postgres_hll/buckets.rb
index 3f64eee030e..e86036440f3 100644
--- a/lib/gitlab/database/postgres_hll/buckets.rb
+++ b/lib/gitlab/database/postgres_hll/buckets.rb
@@ -60,7 +60,7 @@ module Gitlab
num_zero_buckets = TOTAL_BUCKETS - buckets.size
num_uniques = (
- ((TOTAL_BUCKETS**2) * (0.7213 / (1 + 1.079 / TOTAL_BUCKETS))) /
+ ((TOTAL_BUCKETS**2) * (0.7213 / (1 + (1.079 / TOTAL_BUCKETS)))) /
(num_zero_buckets + buckets.values.sum { |bucket_hash| 2**(-1 * bucket_hash) })
).to_i
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index 301e90e3171..31d5de7da64 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -52,8 +52,17 @@ module Gitlab
)
end
+ # Ignore failures during target branch creation so we still create the merge request itself.
def create_target_branch
@project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha)
+ rescue StandardError => err
+ Gitlab::Import::Logger.warn(
+ message: 'Import warning: Failed to create target branch',
+ target_branch: @merge_request.target_branch,
+ diff_head_sha: @diff_head_sha,
+ merge_request_iid: @merge_request.iid,
+ error: err.message
+ )
end
def branch_exists?(branch_name)
diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb
index ca4ff909510..8d86d0ec286 100644
--- a/lib/gitlab/internal_events.rb
+++ b/lib/gitlab/internal_events.rb
@@ -40,7 +40,7 @@ module Gitlab
send_application_instrumentation_event(event_name, additional_properties, kwargs) if send_snowplow_event
if Feature.enabled?(:early_access_program, kwargs[:user], type: :wip)
- create_early_access_program_event(event_name, category, additional_properties[:label], kwargs[:user])
+ create_early_access_program_event(event_name, category, additional_properties[:label], kwargs)
end
rescue StandardError => e
extra = {}
@@ -189,8 +189,9 @@ module Gitlab
gitlab_sdk_client.track(event_name, tracked_attributes)
end
- def create_early_access_program_event(event_name, category, event_label, user)
- return if user.nil? || !user.user_preference.early_access_event_tracking?
+ def create_early_access_program_event(event_name, category, event_label, kwargs)
+ user, namespace = kwargs.values_at(:user, :namespace)
+ return if user.nil? || !namespace&.namespace_settings&.early_access_program_participant?
::EarlyAccessProgram::TrackingEvent.create(
user: user, event_name: event_name.to_s, event_label: event_label, category: category
diff --git a/lib/gitlab/middleware/path_traversal_check.rb b/lib/gitlab/middleware/path_traversal_check.rb
index dbe3db2750d..44b08ff2cb7 100644
--- a/lib/gitlab/middleware/path_traversal_check.rb
+++ b/lib/gitlab/middleware/path_traversal_check.rb
@@ -4,7 +4,7 @@ module Gitlab
module Middleware
class PathTraversalCheck
PATH_TRAVERSAL_MESSAGE = 'Potential path traversal attempt detected'
- EXCLUDED_QUERY_PARAM_NAMES = %w[search term name filter filter_projects].freeze
+ EXCLUDED_QUERY_PARAM_NAMES = %w[search search_title term name filter filter_projects].freeze
def initialize(app)
@app = app
diff --git a/lib/unnested_in_filters/rewriter.rb b/lib/unnested_in_filters/rewriter.rb
index b3c96eb7816..4baa805f796 100644
--- a/lib/unnested_in_filters/rewriter.rb
+++ b/lib/unnested_in_filters/rewriter.rb
@@ -13,7 +13,7 @@ module UnnestedInFilters
end
def to_sql
- "unnest(#{serialized_values}::#{sql_type}[]) AS #{table_name}(#{column_name})"
+ "#{serialized_values} AS #{table_name}(#{column_name})"
end
def as_predicate
@@ -36,7 +36,11 @@ module UnnestedInFilters
end
def serialized_values
- values.is_a?(Arel::Nodes::SelectStatement) ? "ARRAY(#{serialized_arel_value})" : serialized_array_values
+ if values.is_a?(Arel::Nodes::SelectStatement)
+ "(#{serialized_arel_value})"
+ else
+ "unnest(#{serialized_array_values}::#{sql_type}[])"
+ end
end
def serialized_arel_value
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bc97952115a..a915637671d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6037,6 +6037,9 @@ msgstr ""
msgid "Analytics|Something went wrong."
msgstr ""
+msgid "Analytics|Start by choosing a measure"
+msgstr ""
+
msgid "Analytics|Start by choosing a metric"
msgstr ""
@@ -9575,9 +9578,6 @@ msgstr ""
msgid "BulkImport|Direct transfer"
msgstr ""
-msgid "BulkImport|Direct transfer history"
-msgstr ""
-
msgid "BulkImport|Direct transfer maximum download file size (MiB)"
msgstr ""
@@ -9623,9 +9623,6 @@ msgstr ""
msgid "BulkImport|Import without projects"
msgstr ""
-msgid "BulkImport|Importing projects is a %{docsLinkStart}Beta%{docsLinkEnd} feature."
-msgstr ""
-
msgid "BulkImport|Importing the group failed."
msgstr ""
@@ -9644,6 +9641,12 @@ msgstr ""
msgid "BulkImport|Maximum download file size when importing from source GitLab instances by direct transfer."
msgstr ""
+msgid "BulkImport|Migration details"
+msgstr ""
+
+msgid "BulkImport|Migration history"
+msgstr ""
+
msgid "BulkImport|Name already exists."
msgstr ""
@@ -14428,6 +14431,9 @@ msgstr ""
msgid "Contribution analytics"
msgstr ""
+msgid "Contribution type"
+msgstr ""
+
msgid "ContributionAnalytics|%{createdCount} created, %{closedCount} closed."
msgstr ""
@@ -24343,6 +24349,9 @@ msgstr ""
msgid "GoogleArtifactRegistry|Image"
msgstr ""
+msgid "GoogleArtifactRegistry|Instructions"
+msgstr ""
+
msgid "GoogleArtifactRegistry|Interact with them from your CI/CD pipeline. %{link_start}Learn more%{link_end}."
msgstr ""
@@ -24370,9 +24379,6 @@ msgstr ""
msgid "GoogleArtifactRegistry|Project ID: %{projectId}"
msgstr ""
-msgid "GoogleArtifactRegistry|Replace %{codeStart}your_access_token%{codeEnd} with a new %{linkStart}personal access token%{linkEnd} with the %{strongStart}read_api%{strongEnd} scope. This token gets information from your Google Cloud IAM integration in GitLab."
-msgstr ""
-
msgid "GoogleArtifactRegistry|Replace %{codeStart}your_google_cloud_project_id%{codeEnd} with your Google Cloud project ID."
msgstr ""
@@ -25576,6 +25582,9 @@ msgstr ""
msgid "Groups|Changing group URL can have unintended side effects."
msgstr ""
+msgid "Groups|Changing group URL can have unintended side effects. %{linkStart}Learn more%{linkEnd}."
+msgstr ""
+
msgid "Groups|Checking group URL availability..."
msgstr ""
@@ -26920,7 +26929,7 @@ msgstr ""
msgid "Import|Partially completed"
msgstr ""
-msgid "Import|See failures"
+msgid "Import|Show errors"
msgstr ""
msgid "Import|The repository could not be imported."
@@ -39579,6 +39588,9 @@ msgstr ""
msgid "ProductAnalytics|Manage your own analytics provider to process, store, and query analytics data."
msgstr ""
+msgid "ProductAnalytics|Measure"
+msgstr ""
+
msgid "ProductAnalytics|Measure All tracked Events"
msgstr ""
@@ -40518,10 +40530,10 @@ msgstr ""
msgid "ProjectList|Yours"
msgstr ""
-msgid "ProjectMaintenance| To ensure that a full backup is available in case changes need to be restored, you should make an %{docs_link_start}export of the project%{docs_link_end}."
+msgid "ProjectMaintenance|Blob IDs to remove"
msgstr ""
-msgid "ProjectMaintenance|Blob IDs to remove"
+msgid "ProjectMaintenance|Blobs removed"
msgstr ""
msgid "ProjectMaintenance|Cancel"
@@ -40533,6 +40545,12 @@ msgstr ""
msgid "ProjectMaintenance|Enter multiple entries on separate lines."
msgstr ""
+msgid "ProjectMaintenance|Enter the following to confirm:"
+msgstr ""
+
+msgid "ProjectMaintenance|Go to housekeeping"
+msgstr ""
+
msgid "ProjectMaintenance|Housekeeping will need to be triggered manually afterwards to remove old versions of the file."
msgstr ""
@@ -40551,6 +40569,15 @@ msgstr ""
msgid "ProjectMaintenance|Removing blobs by ID cannot be undone. Are you sure you want to continue?"
msgstr ""
+msgid "ProjectMaintenance|Run housekeeping to remove old versions from repository."
+msgstr ""
+
+msgid "ProjectMaintenance|Something went wrong while removing blobs."
+msgstr ""
+
+msgid "ProjectMaintenance|To ensure that a full backup is available in case changes need to be restored, you should make an %{docs_link_start}export of the project%{docs_link_end}."
+msgstr ""
+
msgid "ProjectMaintenance|Yes, remove blobs"
msgstr ""
@@ -53344,6 +53371,9 @@ msgstr ""
msgid "This code snippet contains everything reflected in the configuration form. Copy and paste it into %{linkStart}.gitlab-ci.yml%{linkEnd} file and save your changes. Future %{scanType} scans will use these settings."
msgstr ""
+msgid "This command is used for explaining vulnerabilities and can only be invoked from a vulnerability detail page."
+msgstr ""
+
msgid "This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost."
msgstr ""
@@ -53458,6 +53488,9 @@ msgstr ""
msgid "This epic does not exist or you don't have sufficient permission."
msgstr ""
+msgid "This feature is not enabled yet."
+msgstr ""
+
msgid "This feature is only allowed in groups or projects that enable this feature."
msgstr ""
@@ -57659,6 +57692,9 @@ msgstr ""
msgid "Visibility level"
msgstr ""
+msgid "Visibility level not allowed"
+msgstr ""
+
msgid "Visibility level:"
msgstr ""
@@ -57701,6 +57737,15 @@ msgstr ""
msgid "VisibilityLevel|The project can be accessed without any authentication."
msgstr ""
+msgid "VisibilityLevel|This visibility level has been restricted by your administrator."
+msgstr ""
+
+msgid "VisibilityLevel|This visibility level is not allowed because a child of %{group_name} has a less restrictive visibility level. %{learn_more_link_start}Learn more%{learn_more_link_end}."
+msgstr ""
+
+msgid "VisibilityLevel|This visibility level is not allowed because the parent group has a more restrictive visibility level."
+msgstr ""
+
msgid "VisibilityLevel|Unknown"
msgstr ""
@@ -57725,6 +57770,9 @@ msgstr ""
msgid "Vulnerability Report"
msgstr ""
+msgid "Vulnerability explanation currently only supports vulnerabilities reported by SAST."
+msgstr ""
+
msgid "Vulnerability remediated. Review before resolving."
msgstr ""
diff --git a/package.json b/package.json
index b149ae6ae5c..56df29d70d3 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.99.0",
- "@gitlab/ui": "80.14.1",
+ "@gitlab/ui": "80.15.1",
"@gitlab/web-ide": "^0.0.1-dev-20240508140740",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-1",
diff --git a/scripts/setup/generate-as-if-foss-env.rb b/scripts/setup/generate-as-if-foss-env.rb
index 3dda14bb0ef..d00ecadf189 100755
--- a/scripts/setup/generate-as-if-foss-env.rb
+++ b/scripts/setup/generate-as-if-foss-env.rb
@@ -6,34 +6,53 @@ require 'gitlab' unless Object.const_defined?(:Gitlab)
require 'set' # rubocop:disable Lint/RedundantRequireStatement -- Ruby 3.1 and earlier needs this. Drop this line after Ruby 3.2+ is only supported.
class GenerateAsIfFossEnv
- # rubocop:disable Style/WordArray -- Probably a bug? It already is
- FOSS_JOBS = Set.new(%w[
- build-assets-image
- build-qa-image
- compile-production-assets
- compile-storybook
- compile-test-assets
- detect-tests
- eslint
- generate-apollo-graphql-schema
- graphql-schema-dump
- rspec-predictive:pipeline-generate
- rspec:predictive:trigger
- rspec:predictive:trigger\ single-db
- rspec:predictive:trigger\ single-db-ci-connection
- rubocop
- qa:internal
- qa:selectors
- static-analysis
- ]).freeze
- # rubocop:enable Style/WordArray
+ PARALLEL = %r{(?: \d+/\d+)?}
+ PG_JOB = %r{\S+ pg\d+}
+
+ # Map job names to environment variables. One job can match multiple variables.
+ # For example: "rspec unit 1/2" returns `ENABLE_RSPEC` and `ENABLE_RSPEC_UNIT`.
+ JOB_VARIABLES = {
+ 'build-assets-image' => 'ENABLE_BUILD_ASSETS_IMAGE',
+ 'build-qa-image' => 'ENABLE_BUILD_QA_IMAGE',
+ 'compile-production-assets' => 'ENABLE_COMPILE_PRODUCTION_ASSETS',
+ 'compile-storybook' => 'ENABLE_COMPILE_STORYBOOK',
+ 'compile-test-assets' => 'ENABLE_COMPILE_TEST_ASSETS',
+ 'detect-tests' => 'ENABLE_DETECT_TESTS',
+ 'eslint' => 'ENABLE_ESLINT',
+ 'generate-apollo-graphql-schema' => 'ENABLE_GENERATE_APOLLO_GRAPHQL_SCHEMA',
+ 'graphql-schema-dump' => 'ENABLE_GRAPHQL_SCHEMA_DUMP',
+ 'rspec-predictive:pipeline-generate' => 'ENABLE_RSPEC_PREDICTIVE_PIPELINE_GENERATE',
+ 'rspec:predictive:trigger' => 'ENABLE_RSPEC_PREDICTIVE_TRIGGER',
+ 'rspec:predictive:trigger single-db' => 'ENABLE_RSPEC_PREDICTIVE_TRIGGER_SINGLE_DB',
+ 'rspec:predictive:trigger single-db-ci-connection' => 'ENABLE_RSPEC_PREDICTIVE_TRIGGER_SINGLE_DB_CI_CONNECTION',
+ 'rubocop' => 'ENABLE_RUBOCOP',
+ 'qa:internal' => 'ENABLE_QA_INTERNAL',
+ 'qa:selectors' => 'ENABLE_QA_SELECTORS',
+ 'static-analysis' => 'ENABLE_STATIC_ANALYSIS',
+ /^cache-assets\b/ => 'ENABLE_CACHE_ASSETS',
+ # Jest
+ /^jest#{PARALLEL}/ => 'ENABLE_JEST',
+ /^jest-integration#{PARALLEL}/ => 'ENABLE_JEST_INTEGRATION',
+ /^jest predictive#{PARALLEL}/ => 'ENABLE_JEST_PREDICTIVE',
+ # RSpec
+ /^rspec/ => 'ENABLE_RSPEC',
+ /^rspec(?:-all)? frontend_fixture/ => 'ENABLE_RSPEC_FRONTEND_FIXTURE',
+ /^rspec unit/ => 'ENABLE_RSPEC_UNIT',
+ /^rspec fast_spec_helper/ => 'ENABLE_RSPEC_FAST_SPEC_HELPER',
+ /^rspec migration/ => 'ENABLE_RSPEC_MIGRATION',
+ /^rspec background_migration/ => 'ENABLE_RSPEC_BACKGROUND_MIGRATION',
+ /^rspec integration/ => 'ENABLE_RSPEC_INTEGRATION',
+ /^rspec system/ => 'ENABLE_RSPEC_SYSTEM',
+ /^rspec #{PG_JOB} praefect\b/ => 'ENABLE_RSPEC_PRAEFECT',
+ /^rspec #{PG_JOB} single-db\b/ => 'ENABLE_RSPEC_SINGLE_DB',
+ /^rspec #{PG_JOB} single-db-ci-connection\b/ => 'ENABLE_RSPEC_SINGLE_DB_CI_CONNECTION',
+ /^rspec #{PG_JOB} single-redis\b/ => 'ENABLE_RSPEC_SINGLE_REDIS'
+ }.freeze
def initialize
@client = Gitlab.client(
endpoint: ENV['CI_API_V4_URL'],
private_token: ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] || '')
- @rspec_jobs = Set.new
- @other_jobs = Set.new
end
def variables
@@ -48,23 +67,26 @@ class GenerateAsIfFossEnv
private
- attr_reader :client, :rspec_jobs, :other_jobs
+ attr_reader :client
def generate_variables
- scan_jobs
-
{
START_AS_IF_FOSS: 'true',
RUBY_VERSION: ENV['RUBY_VERSION'],
FIND_CHANGES_MERGE_REQUEST_PROJECT_PATH: ENV['CI_MERGE_REQUEST_PROJECT_PATH'],
- FIND_CHANGES_MERGE_REQUEST_IID: ENV['CI_MERGE_REQUEST_IID']
- }.merge(rspec_variables).merge(other_jobs_variables)
+ FIND_CHANGES_MERGE_REQUEST_IID: ENV['CI_MERGE_REQUEST_IID'],
+ **variables_from_jobs
+ }
end
- def scan_jobs
+ def variables_from_jobs
+ variable_set = Set.new
+
each_job do |job|
- detect_rspec(job) || detect_other_jobs(job)
+ variable_set.merge(variables_from(job.name))
end
+
+ variable_set.to_h { |v| [v.to_sym, 'true'] }
end
def each_job
@@ -75,44 +97,8 @@ class GenerateAsIfFossEnv
end
end
- def detect_rspec(job)
- rspec_type = job.name[%r{^rspec(?:-all)? ([\w\-]+)}, 1]
-
- return unless rspec_type
-
- rspec_kind = job.name[%r{pg\d+ ([\w\-]+)(?: \d+/\d+)?$}, 1]
- rspec_jobs << rspec_type
- rspec_jobs << rspec_kind if rspec_kind
- end
-
- def detect_other_jobs(job)
- # rubocop:disable Lint/AssignmentInCondition -- More clear without this cop
- if FOSS_JOBS.member?(job.name)
- other_jobs << job.name
- elsif jest_type = job.name[%r{^(jest(?:-\w+| predictive)?)(?: \d+/\d+)?$}, 1]
- other_jobs << jest_type
- elsif cache_assets_type = job.name[%r{^(cache-assets)\b}, 1]
- other_jobs << cache_assets_type
- end
- # rubocop:enable Lint/AssignmentInCondition
- end
-
- def rspec_variables
- return {} if rspec_jobs.empty?
-
- rspec_jobs.inject({ ENABLE_RSPEC: 'true' }) do |result, rspec|
- result.merge("ENABLE_RSPEC_#{job_name_to_variable_name(rspec)}": 'true')
- end
- end
-
- def other_jobs_variables
- other_jobs.inject({}) do |result, job_name|
- result.merge("ENABLE_#{job_name_to_variable_name(job_name)}": 'true')
- end
- end
-
- def job_name_to_variable_name(name)
- name.upcase.tr('-: ', '_')
+ def variables_from(job_name)
+ JOB_VARIABLES.select { |match, _| match === job_name }.map(&:last)
end
end
diff --git a/spec/features/groups/import_export/migration_history_spec.rb b/spec/features/groups/import_export/migration_history_spec.rb
index 87b38221ad6..be265bdbc38 100644
--- a/spec/features/groups/import_export/migration_history_spec.rb
+++ b/spec/features/groups/import_export/migration_history_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Import/Export - GitLab migration history', :js, feature_category
wait_for_requests
- expect(page).to have_content 'Direct transfer history'
+ expect(page).to have_content 'Migration history'
expect(page.find('tbody')).to have_css('tr', count: 2)
end
end
diff --git a/spec/features/groups/settings/general_visibility_levels_spec.rb b/spec/features/groups/settings/general_visibility_levels_spec.rb
new file mode 100644
index 00000000000..b3eead86ffc
--- /dev/null
+++ b/spec/features/groups/settings/general_visibility_levels_spec.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'General settings visibility levels', :js, :aggregate_failures, feature_category: :groups_and_projects do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:user) { create(:user).tap { |user| group.add_owner(user) } }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'with parent group internal' do
+ let_it_be(:parent_group) { create(:group, :internal) }
+ let_it_be(:group) { create(:group, :internal, parent: parent_group) }
+ let_it_be(:user) { create(:user).tap { |user| group.add_owner(user) } }
+
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Visibility level')
+
+ expect(page).to have_field("Private", checked: false, disabled: false)
+ expect(page).to have_field("Internal", checked: true, disabled: false)
+ expect(page).to have_field("Public", checked: false, disabled: true)
+
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Private')
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Internal')
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Public',
+ popover_content: 'This visibility level is not allowed ' \
+ 'because the parent group has a more restrictive visibility level.'
+ )
+ end
+ end
+
+ context 'with internal child project in group' do
+ let_it_be(:project) { create(:project, :internal, group: group) }
+
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_field("Private", checked: false, disabled: true)
+ expect(page).to have_field("Internal", checked: false, disabled: false)
+ expect(page).to have_field("Public", checked: true, disabled: false)
+
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Private',
+ popover_content: "This visibility level is not allowed " \
+ "because a child of #{group.name} has a less restrictive visibility level. Learn more."
+ )
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Internal')
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Public')
+ end
+ end
+
+ context 'without restricted visibility levels' do
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Visibility level')
+
+ expect(page).to have_field("Private", checked: false, disabled: false)
+ expect(page).to have_field("Internal", checked: false, disabled: false)
+ expect(page).to have_field("Public", checked: true, disabled: false)
+
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Private')
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Internal')
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Public')
+ end
+ end
+
+ context 'with restricted visibility level public' do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_field("Private", checked: false, disabled: false)
+ expect(page).to have_field("Internal", checked: false, disabled: false)
+ expect(page).to have_field("Public", checked: true, disabled: true)
+
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Private')
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Internal')
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Public',
+ popover_content: 'This visibility level has been restricted by your administrator.'
+ )
+ end
+
+ context 'with private project in group' do
+ let_it_be(:project) { create(:project, :private, group: group) }
+
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_field("Private", checked: false, disabled: false)
+ expect(page).to have_field("Internal", checked: false, disabled: false)
+ expect(page).to have_field("Public", checked: true, disabled: true)
+
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Private')
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Internal')
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Public',
+ popover_content: 'This visibility level has been restricted by your administrator.'
+ )
+ end
+ end
+
+ context 'with public project in group' do
+ let_it_be(:project) { create(:project, :public, group: group) }
+
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_field("Private", checked: false, disabled: true)
+ expect(page).to have_field("Internal", checked: false, disabled: true)
+ expect(page).to have_field("Public", checked: true, disabled: true)
+
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Private',
+ popover_content: "This visibility level is not allowed " \
+ "because a child of #{group.name} has a less restrictive visibility level. Learn more."
+ )
+
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Internal',
+ popover_content: "This visibility level is not allowed " \
+ "because a child of #{group.name} has a less restrictive visibility level. Learn more."
+ )
+
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Public',
+ popover_content: 'This visibility level has been restricted by your administrator.'
+ )
+ end
+ end
+ end
+
+ context 'with multiple restricted visibility levels "Public" and "Private"' do
+ let_it_be(:project) { create(:project, :internal, group: group) }
+
+ before do
+ stub_application_setting(
+ restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]
+ )
+ end
+
+ it 'shows each visibility level in correct field state' do
+ visit edit_group_path(group)
+
+ expect(page).to have_field("Private", checked: false, disabled: true)
+ expect(page).to have_field("Internal", checked: false, disabled: false)
+ expect(page).to have_field("Public", checked: true, disabled: true)
+
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Private',
+ popover_content: 'This visibility level has been restricted by your administrator.'
+ )
+
+ expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text: 'Internal')
+
+ expect_popover_for_disallowed_visibility_level(
+ visibility_level_label_text: 'Public',
+ popover_content: 'This visibility level has been restricted by your administrator.'
+ )
+ end
+ end
+
+ def expect_popover_for_disallowed_visibility_level(visibility_level_label_text:, popover_content:)
+ # Checking that a popover content is not visible before hovering
+ expect(page).not_to have_content(popover_content)
+
+ within('label', text: visibility_level_label_text) do
+ find('[data-testid=visibility-level-not-allowed-popover]').hover
+ end
+
+ page.within('.gl-popover') do
+ expect(page).to have_content('Visibility level not allowed')
+ expect(page).to have_content(popover_content)
+ end
+
+ # Move cursor to another element to hide the popover
+ find('label', text: visibility_level_label_text).hover
+ end
+
+ def expect_no_popover_for_disallowed_visibility_level(visibility_level_label_text:)
+ within('label', text: visibility_level_label_text) do
+ expect(page).not_to have_selector('[data-testid=visibility-level-not-allowed-popover]')
+ end
+
+ expect(page).not_to have_selector('.gl-popover')
+ end
+end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index cf9c0012e29..14e5c016f76 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
end
end
- it 'shows a message if multiple levels are restricted' do
+ it 'disables the radio button for visibility levels "Private" and "Internal"' do
stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::PUBLIC)
stub_application_setting(
restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
@@ -78,16 +78,20 @@ RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
visit new_project_path
click_link 'Create blank project'
- expect(page).to have_content 'Other visibility settings have been disabled by the administrator.'
+ expect(page).to have_field("Private", checked: false, disabled: true)
+ expect(page).to have_field("Internal", checked: false, disabled: true)
+ expect(page).to have_field("Public", checked: true, disabled: false)
end
- it 'shows a message if all levels are restricted' do
+ it 'disables all radio button for visibility levels' do
stub_application_setting(restricted_visibility_levels: Gitlab::VisibilityLevel.values)
visit new_project_path
click_link 'Create blank project'
- expect(page).to have_content 'Visibility settings have been disabled by the administrator.'
+ expect(page).to have_field("Private", checked: true, disabled: true)
+ expect(page).to have_field("Internal", checked: false, disabled: true)
+ expect(page).to have_field("Public", checked: false, disabled: true)
end
end
diff --git a/spec/frontend/groups/components/new_group_form_spec.js b/spec/frontend/groups/components/new_edit_form_spec.js
similarity index 77%
rename from spec/frontend/groups/components/new_group_form_spec.js
rename to spec/frontend/groups/components/new_edit_form_spec.js
index b300302acb9..82c02c9bab3 100644
--- a/spec/frontend/groups/components/new_group_form_spec.js
+++ b/spec/frontend/groups/components/new_edit_form_spec.js
@@ -1,7 +1,7 @@
import { GlLink, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
-import NewGroupForm from '~/groups/components/new_group_form.vue';
+import NewEditForm from '~/groups/components/new_edit_form.vue';
import GroupPathField from '~/groups/components/group_path_field.vue';
import VisibilityLevelRadioButtons from '~/visibility_level/components/visibility_level_radio_buttons.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -10,10 +10,16 @@ import {
VISIBILITY_LEVEL_PUBLIC_INTEGER,
GROUP_VISIBILITY_LEVEL_DESCRIPTIONS,
} from '~/visibility_level/constants';
-import { FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_VISIBILITY_LEVEL } from '~/groups/constants';
+import {
+ FORM_FIELD_NAME,
+ FORM_FIELD_PATH,
+ FORM_FIELD_ID,
+ FORM_FIELD_VISIBILITY_LEVEL,
+} from '~/groups/constants';
+import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-describe('NewGroupForm', () => {
+describe('NewEditForm', () => {
let wrapper;
const defaultPropsData = {
@@ -32,7 +38,7 @@ describe('NewGroupForm', () => {
};
const createComponent = ({ propsData = {} } = {}) => {
- wrapper = mountExtended(NewGroupForm, {
+ wrapper = mountExtended(NewEditForm, {
attachTo: document.body,
propsData: {
...defaultPropsData,
@@ -97,6 +103,41 @@ describe('NewGroupForm', () => {
});
});
+ describe('when editing a group', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ initialFormValues: {
+ [FORM_FIELD_ID]: 5,
+ [FORM_FIELD_NAME]: 'Foo bar',
+ [FORM_FIELD_PATH]: 'foo-bar',
+ [FORM_FIELD_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PUBLIC_INTEGER,
+ },
+ },
+ });
+ });
+
+ it('renders `Group ID` field', () => {
+ expect(wrapper.findByLabelText('Group ID').element.value).toBe('5');
+ });
+
+ it('renders alert about changing URL', () => {
+ const alert = wrapper.findByTestId('changing-url-alert');
+
+ expect(alert.text()).toBe('Changing group URL can have unintended side effects. Learn more.');
+ expect(alert.findComponent(HelpPageLink).props()).toEqual({
+ href: 'user/group/manage',
+ anchor: 'change-a-groups-path',
+ });
+ });
+
+ it('does not modify `Group URL` when typing in `Group name`', async () => {
+ await findNameField().setValue('Foo bar baz');
+
+ expect(findPathField().props('value')).toBe('foo-bar');
+ });
+ });
+
describe('when form is submitted without filling in required fields', () => {
beforeEach(async () => {
createComponent();
@@ -196,4 +237,14 @@ describe('NewGroupForm', () => {
expect(findSubmitButton().props('loading')).toBe(false);
});
+
+ describe('when `submitButtonText` prop is set', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { submitButtonText: 'Save changes' } });
+ });
+
+ it('uses it for submit button', () => {
+ expect(wrapper.findByRole('button', { name: 'Save changes' }).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js b/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js
index d80d870809f..7b88c1d4449 100644
--- a/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js
@@ -28,7 +28,7 @@ describe('import history link', () => {
},
});
- expect(findGlLink().text()).toBe('View details');
+ expect(findGlLink().text()).toBe('Migration details >');
expect(findGlLink().attributes('href')).toBe('/import/174/history');
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_status_spec.js b/spec/frontend/import_entities/import_groups/components/import_status_spec.js
index 5cef7d694ce..eb580884a68 100644
--- a/spec/frontend/import_entities/import_groups/components/import_status_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_status_spec.js
@@ -11,17 +11,12 @@ describe('Group import status component', () => {
status: STATUSES.FINISHED,
};
- const mockDetailsPath = '/:id/failures/:entity_id';
-
const createComponent = ({ props } = {}) => {
wrapper = shallowMount(ImportStatus, {
propsData: {
...defaultProps,
...props,
},
- provide: {
- detailsPath: mockDetailsPath,
- },
});
};
@@ -75,24 +70,24 @@ describe('Group import status component', () => {
});
});
- describe('details link', () => {
+ describe('failures link', () => {
it('does not render by default', () => {
createComponent();
expect(findGlLink().exists()).toBe(false);
});
- it('renders with correct link when import is partial', () => {
+ it('renders with correct link when failuresHref is passed', () => {
+ const mockFailuresHref = '/failures/11';
+
createComponent({
props: {
- id: 2,
- entityId: 11,
- hasFailures: true,
+ failuresHref: mockFailuresHref,
status: STATUSES.FINISHED,
},
});
- expect(findGlLink().attributes('href')).toBe('/2/failures/11');
+ expect(findGlLink().attributes('href')).toBe(mockFailuresHref);
});
});
});
diff --git a/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
index 3ad02d3851d..38d4afb5ffb 100644
--- a/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
+++ b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
@@ -1,11 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemberActivity with a member that does not have all of the fields renders \`User created\` field 1`] = `
-
-
-
- Access granted:
-
+
+
+
Aug 06, 2020
@@ -14,27 +26,59 @@ exports[`MemberActivity with a member that does not have all of the fields rende
`;
exports[`MemberActivity with a member that has all fields renders \`User created\`, \`Access granted\`, and \`Last activity\` fields 1`] = `
-
-
-
- User created:
-
+
+
+
Mar 10, 2022
-
-
- Access granted:
-
+
+
Jul 17, 2020
-
-
- Last activity:
-
+
+
Mar 15, 2022
diff --git a/spec/frontend/organizations/activity/components/app_spec.js b/spec/frontend/organizations/activity/components/app_spec.js
index 9686c8fea3b..21a15710966 100644
--- a/spec/frontend/organizations/activity/components/app_spec.js
+++ b/spec/frontend/organizations/activity/components/app_spec.js
@@ -1,18 +1,33 @@
-import { GlEmptyState, GlPagination, GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlPagination, GlLoadingIcon, GlFilteredSearchToken } from '@gitlab/ui';
import AxiosMockAdapter from 'axios-mock-adapter';
import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
import OrganizationsActivityApp from '~/organizations/activity/components/app.vue';
+import {
+ CONTRIBUTION_TYPE_FILTER_TYPE,
+ RECENT_SEARCHES_STORAGE_KEY,
+ FILTERED_SEARCH_NAMESPACE,
+} from '~/organizations/activity/filters';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import ContributionEvents from '~/contribution_events/components/contribution_events.vue';
import { createAlert } from '~/alert';
import waitForPromises from 'helpers/wait_for_promises';
+import {
+ MOCK_ALL_EVENT,
+ MOCK_EVENT_TYPES,
+ MOCK_SELECTED_CONTRIBUTION_TYPE,
+ MOCK_CONTRIBUTION_TYPE_VALUE,
+} from '../mock_data';
jest.mock('~/alert');
const defaultProps = {
organizationActivityPath: '/-/organizations/default/activity.json',
+ organizationActivityEventTypes: MOCK_EVENT_TYPES,
+ organizationActivityAllEvent: MOCK_ALL_EVENT,
};
describe('OrganizationsActivityApp', () => {
@@ -37,6 +52,7 @@ describe('OrganizationsActivityApp', () => {
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
const findGlPagination = () => wrapper.findComponent(GlPagination);
const findContributionEvents = () => wrapper.findComponent(ContributionEvents);
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
describe('mounted', () => {
beforeEach(() => {
@@ -50,6 +66,7 @@ describe('OrganizationsActivityApp', () => {
params: {
offset: 0,
limit: DEFAULT_PER_PAGE,
+ event_filter: MOCK_ALL_EVENT,
},
});
});
@@ -64,6 +81,8 @@ describe('OrganizationsActivityApp', () => {
createComponent();
await waitForPromises();
+
+ axios.get.mockClear();
});
it('when new page is fetched, calls API with correct params, and does not set page until after call resolves', async () => {
@@ -73,6 +92,7 @@ describe('OrganizationsActivityApp', () => {
params: {
offset: 2 * DEFAULT_PER_PAGE,
limit: DEFAULT_PER_PAGE,
+ event_filter: MOCK_ALL_EVENT,
},
});
@@ -80,6 +100,56 @@ describe('OrganizationsActivityApp', () => {
await waitForPromises();
expect(findGlPagination().props('value')).toBe(3);
});
+
+ it('when filter is updated with empty value, calls API with event_filter: all', () => {
+ findFilteredSearch().vm.$emit('onFilter', []);
+
+ expect(axios.get).toHaveBeenCalledWith(defaultProps.organizationActivityPath, {
+ params: {
+ offset: 0,
+ limit: DEFAULT_PER_PAGE,
+ event_filter: MOCK_ALL_EVENT,
+ },
+ });
+ });
+
+ it(`when filter is updated with ${MOCK_SELECTED_CONTRIBUTION_TYPE.type}: ${MOCK_CONTRIBUTION_TYPE_VALUE.data}, calls API with event_filter: ${MOCK_CONTRIBUTION_TYPE_VALUE.data}`, () => {
+ findFilteredSearch().vm.$emit('onFilter', [MOCK_SELECTED_CONTRIBUTION_TYPE]);
+
+ expect(axios.get).toHaveBeenCalledWith(defaultProps.organizationActivityPath, {
+ params: {
+ offset: 0,
+ limit: DEFAULT_PER_PAGE,
+ event_filter: MOCK_CONTRIBUTION_TYPE_VALUE.data,
+ },
+ });
+ });
+ });
+
+ describe('filtered search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders with correct params and available tokens', () => {
+ const expectedTokens = [
+ {
+ title: 'Contribution type',
+ icon: 'comparison',
+ type: CONTRIBUTION_TYPE_FILTER_TYPE,
+ unique: true,
+ token: GlFilteredSearchToken,
+ operators: OPERATORS_IS,
+ options: MOCK_EVENT_TYPES,
+ },
+ ];
+
+ expect(findFilteredSearch().props('recentSearchesStorageKey')).toBe(
+ RECENT_SEARCHES_STORAGE_KEY,
+ );
+ expect(findFilteredSearch().props('namespace')).toBe(FILTERED_SEARCH_NAMESPACE);
+ expect(findFilteredSearch().props('tokens')).toStrictEqual(expectedTokens);
+ });
});
describe('when activity API request is loading', () => {
@@ -95,6 +165,10 @@ describe('OrganizationsActivityApp', () => {
expect(findGlLoadingIcon().exists()).toBe(true);
});
+ it('renders filtered search bar', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
it('does not render pagination', () => {
expect(findGlPagination().exists()).toBe(false);
});
@@ -118,6 +192,10 @@ describe('OrganizationsActivityApp', () => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
+ it('renders filtered search bar', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
it('does not render pagination', () => {
expect(findGlPagination().exists()).toBe(false);
});
@@ -141,6 +219,10 @@ describe('OrganizationsActivityApp', () => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
+ it('renders filtered search bar', () => {
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
it('renders pagination', () => {
expect(findGlPagination().exists()).toBe(true);
});
diff --git a/spec/frontend/organizations/activity/filters_spec.js b/spec/frontend/organizations/activity/filters_spec.js
new file mode 100644
index 00000000000..d75c39d56a0
--- /dev/null
+++ b/spec/frontend/organizations/activity/filters_spec.js
@@ -0,0 +1,24 @@
+import { convertTokensToFilter } from '~/organizations/activity/filters';
+
+import {
+ MOCK_CONTRIBUTION_TYPE_VALUE,
+ MOCK_SEARCH_TOKEN,
+ MOCK_EMPTY_CONTRIBUTION_TYPE,
+ MOCK_SELECTED_CONTRIBUTION_TYPE,
+} from './mock_data';
+
+describe('Organizations Activity Filters', () => {
+ describe('convertTokensToFilter', () => {
+ it.each`
+ description | tokens | result
+ ${'no tokens'} | ${[]} | ${null}
+ ${'only search token'} | ${[MOCK_SEARCH_TOKEN]} | ${null}
+ ${'empty contribution type token'} | ${[MOCK_EMPTY_CONTRIBUTION_TYPE]} | ${undefined}
+ ${'valid contribution type token'} | ${[MOCK_SELECTED_CONTRIBUTION_TYPE]} | ${MOCK_CONTRIBUTION_TYPE_VALUE.data}
+ ${'search and empty contribution type token'} | ${[MOCK_SEARCH_TOKEN, MOCK_EMPTY_CONTRIBUTION_TYPE]} | ${undefined}
+ ${'search and valid contribution type token'} | ${[MOCK_SEARCH_TOKEN, MOCK_SELECTED_CONTRIBUTION_TYPE]} | ${MOCK_CONTRIBUTION_TYPE_VALUE.data}
+ `('returns $result with $description', ({ tokens, result }) => {
+ expect(convertTokensToFilter(tokens)).toBe(result);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/activity/mock_data.js b/spec/frontend/organizations/activity/mock_data.js
new file mode 100644
index 00000000000..b25ccf9c32c
--- /dev/null
+++ b/spec/frontend/organizations/activity/mock_data.js
@@ -0,0 +1,44 @@
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { CONTRIBUTION_TYPE_FILTER_TYPE } from '~/organizations/activity/filters';
+
+export const MOCK_ALL_EVENT = 'all';
+
+export const MOCK_EVENT_TYPES = [
+ {
+ title: 'Push events',
+ value: 'push',
+ },
+ {
+ title: 'Merge events',
+ value: 'merged',
+ },
+ {
+ title: 'Issue events',
+ value: 'issue',
+ },
+ {
+ title: 'Comments',
+ value: 'comments',
+ },
+ {
+ title: 'Wiki',
+ value: 'wiki',
+ },
+ {
+ title: 'Designs',
+ value: 'designs',
+ },
+ {
+ title: 'Team',
+ value: 'team',
+ },
+];
+
+export const MOCK_CONTRIBUTION_TYPE_VALUE = { data: 'comments', operator: '=' };
+
+export const MOCK_SEARCH_TOKEN = { type: FILTERED_SEARCH_TERM, value: '' };
+export const MOCK_EMPTY_CONTRIBUTION_TYPE = { type: CONTRIBUTION_TYPE_FILTER_TYPE, value: '' };
+export const MOCK_SELECTED_CONTRIBUTION_TYPE = {
+ type: CONTRIBUTION_TYPE_FILTER_TYPE,
+ value: MOCK_CONTRIBUTION_TYPE_VALUE,
+};
diff --git a/spec/frontend/organizations/groups/edit/components/app_spec.js b/spec/frontend/organizations/groups/edit/components/app_spec.js
index ecc0865d430..9322fbd3cad 100644
--- a/spec/frontend/organizations/groups/edit/components/app_spec.js
+++ b/spec/frontend/organizations/groups/edit/components/app_spec.js
@@ -1,14 +1,35 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import App from '~/organizations/groups/edit/components/app.vue';
+import {
+ VISIBILITY_LEVEL_INTERNAL_INTEGER,
+ VISIBILITY_LEVEL_PRIVATE_INTEGER,
+ VISIBILITY_LEVEL_PUBLIC_INTEGER,
+} from '~/visibility_level/constants';
+import NewEditForm from '~/groups/components/new_edit_form.vue';
describe('OrganizationGroupsEditApp', () => {
let wrapper;
const defaultProvide = {
group: {
+ id: 1,
fullName: 'Mock namespace / Foo bar',
+ name: 'Foo bar',
+ path: 'foo-bar',
},
+ basePath: 'https://gitlab.com',
+ groupsAndProjectsOrganizationPath: '/-/organizations/carrot/groups_and_projects?display=groups',
+ groupsOrganizationPath: '/-/organizations/default/groups',
+ availableVisibilityLevels: [
+ VISIBILITY_LEVEL_PRIVATE_INTEGER,
+ VISIBILITY_LEVEL_INTERNAL_INTEGER,
+ VISIBILITY_LEVEL_PUBLIC_INTEGER,
+ ],
+ restrictedVisibilityLevels: [],
+ defaultVisibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER,
+ pathMaxlength: 10,
+ pathPattern: 'mockPattern',
};
const createComponent = () => {
@@ -20,6 +41,8 @@ describe('OrganizationGroupsEditApp', () => {
});
};
+ const findForm = () => wrapper.findComponent(NewEditForm);
+
it('renders page title', () => {
createComponent();
@@ -27,4 +50,20 @@ describe('OrganizationGroupsEditApp', () => {
wrapper.findByRole('heading', { name: 'Edit group: Mock namespace / Foo bar' }).exists(),
).toBe(true);
});
+
+ it('renders form and passes correct props', () => {
+ createComponent();
+
+ expect(findForm().props()).toEqual({
+ loading: false,
+ basePath: defaultProvide.basePath,
+ cancelPath: defaultProvide.groupsAndProjectsOrganizationPath,
+ pathMaxlength: defaultProvide.pathMaxlength,
+ pathPattern: defaultProvide.pathPattern,
+ availableVisibilityLevels: defaultProvide.availableVisibilityLevels,
+ restrictedVisibilityLevels: defaultProvide.restrictedVisibilityLevels,
+ initialFormValues: defaultProvide.group,
+ submitButtonText: 'Save changes',
+ });
+ });
});
diff --git a/spec/frontend/organizations/groups/new/components/app_spec.js b/spec/frontend/organizations/groups/new/components/app_spec.js
index 16fa18d625c..224511a98bb 100644
--- a/spec/frontend/organizations/groups/new/components/app_spec.js
+++ b/spec/frontend/organizations/groups/new/components/app_spec.js
@@ -6,7 +6,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import App from '~/organizations/groups/new/components/app.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
-import NewGroupForm from '~/groups/components/new_group_form.vue';
+import NewEditForm from '~/groups/components/new_edit_form.vue';
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import { createAlert } from '~/alert';
import {
@@ -28,7 +28,6 @@ describe('OrganizationGroupsNewApp', () => {
basePath: 'https://gitlab.com',
groupsAndProjectsOrganizationPath: '/-/organizations/carrot/groups_and_projects?display=groups',
groupsOrganizationPath: '/-/organizations/default/groups',
- mattermostEnabled: false,
availableVisibilityLevels: [
VISIBILITY_LEVEL_PRIVATE_INTEGER,
VISIBILITY_LEVEL_INTERNAL_INTEGER,
@@ -52,7 +51,7 @@ describe('OrganizationGroupsNewApp', () => {
const findAllParagraphs = () => wrapper.findAll('p');
const findAllLinks = () => wrapper.findAllComponents(GlLink);
- const findForm = () => wrapper.findComponent(NewGroupForm);
+ const findForm = () => wrapper.findComponent(NewEditForm);
const submitForm = async () => {
findForm().vm.$emit('submit', {
@@ -104,6 +103,7 @@ describe('OrganizationGroupsNewApp', () => {
path: '',
visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER,
},
+ submitButtonText: 'Create group',
});
});
@@ -116,7 +116,7 @@ describe('OrganizationGroupsNewApp', () => {
await submitForm();
});
- it('sets `NewGroupForm` `loading` prop to `true`', async () => {
+ it('sets `NewEditForm` `loading` prop to `true`', async () => {
expect(findForm().props('loading')).toBe(true);
await waitForPromises();
});
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index 7ff4ae68c6a..aa7aaa10093 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -24,7 +24,7 @@ describe('BulkImportsHistoryApp', () => {
};
const DUMMY_RESPONSE = [
{
- id: 1,
+ id: 361,
bulk_import_id: 1,
status: 'finished',
entity_type: 'group',
@@ -44,7 +44,7 @@ describe('BulkImportsHistoryApp', () => {
},
},
{
- id: 2,
+ id: 843,
bulk_import_id: 2,
status: 'failed',
entity_type: 'project',
@@ -74,14 +74,15 @@ describe('BulkImportsHistoryApp', () => {
let wrapper;
let mock;
+ const mockDetailsPath = '/import/:id/history/:entity_id/failures';
const mockRealtimeChangesPath = '/import/realtime_changes.json';
- function createComponent({ shallow = true, provide, props = {} } = {}) {
+ function createComponent({ shallow = true, props = {} } = {}) {
const mountFn = shallow ? shallowMount : mount;
wrapper = mountFn(BulkImportsHistoryApp, {
provide: {
+ detailsPath: mockDetailsPath,
realtimeChangesPath: mockRealtimeChangesPath,
- ...provide,
},
propsData: {
...props,
@@ -256,17 +257,14 @@ describe('BulkImportsHistoryApp', () => {
it('renders failed import status with details link', async () => {
createComponent({
shallow: false,
- provide: {
- detailsPath: '/mock-details',
- },
});
await waitForPromises();
const failedImportStatus = findImportStatusAt(1);
const failedImportStatusLink = failedImportStatus.find('a');
expect(failedImportStatus.text()).toContain('Failed');
- expect(failedImportStatusLink.text()).toBe('See failures');
- expect(failedImportStatusLink.attributes('href')).toContain('/mock-details');
+ expect(failedImportStatusLink.text()).toBe('Show errors >');
+ expect(failedImportStatusLink.attributes('href')).toBe('/import/2/history/843/failures');
});
it('renders import stats', () => {
diff --git a/spec/frontend/projects/settings/repository/maintenance/mock_data.js b/spec/frontend/projects/settings/repository/maintenance/mock_data.js
new file mode 100644
index 00000000000..47363caa797
--- /dev/null
+++ b/spec/frontend/projects/settings/repository/maintenance/mock_data.js
@@ -0,0 +1,21 @@
+export const TEST_HEADER_HEIGHT = '123px';
+export const TEST_PROJECT_PATH = 'project/path';
+export const TEST_BLOB_ID = '96803162678c7c1ff1e130424b95f28f84ec99cf';
+
+export const REMOVE_MUTATION_SUCCESS = {
+ data: {
+ projectBlobsRemove: {
+ errors: [],
+ __typename: 'projectBlobsRemovePayload',
+ },
+ },
+};
+
+export const REMOVE_MUTATION_FAIL = {
+ data: {
+ projectBlobsRemove: {
+ errors: ['Some error'],
+ __typename: 'projectBlobsRemovePayload',
+ },
+ },
+};
diff --git a/spec/frontend/projects/settings/repository/maintenance/remove_blobs_spec.js b/spec/frontend/projects/settings/repository/maintenance/remove_blobs_spec.js
index 8385efb1631..cd11167b02c 100644
--- a/spec/frontend/projects/settings/repository/maintenance/remove_blobs_spec.js
+++ b/spec/frontend/projects/settings/repository/maintenance/remove_blobs_spec.js
@@ -1,20 +1,44 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDrawer, GlFormTextarea, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
+import { createAlert, VARIANT_WARNING } from '~/alert';
import RemoveBlobs from '~/projects/settings/repository/maintenance/remove_blobs.vue';
+import removeBlobsMutation from '~/projects/settings/repository/maintenance/graphql/mutations/remove_blobs.mutation.graphql';
+import {
+ TEST_HEADER_HEIGHT,
+ TEST_PROJECT_PATH,
+ TEST_BLOB_ID,
+ REMOVE_MUTATION_SUCCESS,
+ REMOVE_MUTATION_FAIL,
+} from './mock_data';
+
+Vue.use(VueApollo);
jest.mock('~/lib/utils/dom_utils');
-
-const TEST_HEADER_HEIGHT = '123px';
+jest.mock('~/alert');
describe('Remove blobs', () => {
let wrapper;
+ let mutationMock;
- const createComponent = () => {
+ const createMockApolloProvider = (resolverMock) => {
+ return createMockApollo([[removeBlobsMutation, resolverMock]]);
+ };
+
+ const createComponent = (mutationResponse = REMOVE_MUTATION_SUCCESS) => {
+ mutationMock = jest.fn().mockResolvedValue(mutationResponse);
getContentWrapperHeight.mockReturnValue(TEST_HEADER_HEIGHT);
- wrapper = shallowMountExtended(RemoveBlobs);
+ wrapper = shallowMountExtended(RemoveBlobs, {
+ apolloProvider: createMockApolloProvider(mutationMock),
+ provide: {
+ projectPath: TEST_PROJECT_PATH,
+ },
+ });
};
const findDrawerTrigger = () => wrapper.findByTestId('drawer-trigger');
@@ -52,7 +76,7 @@ describe('Remove blobs', () => {
});
expect(findModal().text()).toBe(
- 'Removing blobs by ID cannot be undone. Are you sure you want to continue?',
+ 'Removing blobs by ID cannot be undone. Are you sure you want to continue? Enter the following to confirm: project/path',
);
});
});
@@ -73,12 +97,19 @@ describe('Remove blobs', () => {
});
describe('adding blob IDs', () => {
- beforeEach(() => findTextarea().vm.$emit('input', '1234'));
+ beforeEach(() => findTextarea().vm.$emit('input', TEST_BLOB_ID));
- it('enables the primary action when blob IDs are added', () => {
+ it('enables the primary action when valid blob IDs are added', () => {
expect(removeBlobsButton().props('disabled')).toBe(false);
});
+ it('disables the primary action when invalid blob IDs are added', async () => {
+ findTextarea().vm.$emit('input', 'invalid');
+ await nextTick();
+
+ expect(removeBlobsButton().props('disabled')).toBe(true);
+ });
+
describe('confirmation modal', () => {
beforeEach(() => removeBlobsButton().vm.$emit('click'));
@@ -86,11 +117,58 @@ describe('Remove blobs', () => {
expect(findModal().props('visible')).toBe(true);
});
- it('closes the drawer when removal is confirmed', async () => {
- findModal().vm.$emit('primary');
- await nextTick();
+ describe('removal confirmed (success)', () => {
+ beforeEach(() => findModal().vm.$emit('primary'));
- expect(findDrawer().props('open')).toBe(false);
+ it('disables user input while loading', () => {
+ expect(findTextarea().attributes('disabled')).toBe('true');
+ expect(removeBlobsButton().props('loading')).toBe(true);
+ });
+
+ it('calls the remove mutation', () => {
+ expect(mutationMock).toHaveBeenCalledWith({
+ blobOids: [TEST_BLOB_ID],
+ projectPath: TEST_PROJECT_PATH,
+ });
+ });
+
+ it('closes the drawer when removal is confirmed', async () => {
+ await waitForPromises();
+
+ expect(findDrawer().props('open')).toBe(false);
+ });
+
+ it('generates a housekeeping alert', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Run housekeeping to remove old versions from repository.',
+ primaryButton: { clickHandler: expect.any(Function), text: 'Go to housekeeping' },
+ title: 'Blobs removed',
+ variant: VARIANT_WARNING,
+ });
+ });
+ });
+
+ describe('removal confirmed (fail)', () => {
+ beforeEach(async () => {
+ createComponent(REMOVE_MUTATION_FAIL);
+
+ // Simulates the workflow (open drawer → add blobId → click remove → confirm remove)
+ findDrawerTrigger().vm.$emit('click');
+ findTextarea().vm.$emit('input', TEST_BLOB_ID);
+ removeBlobsButton().vm.$emit('click');
+ findModal().vm.$emit('primary');
+
+ await waitForPromises();
+ });
+
+ it('generates an error alert upon failed mutation', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong while removing blobs.',
+ captureError: true,
+ });
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 9e3d6970db7..ea27d75853e 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -346,14 +346,25 @@ describe('WorkItemDetail component', () => {
});
});
- it('shows empty state with an error message when the work item query was unsuccessful', async () => {
- const errorHandler = jest.fn().mockRejectedValue('Oops');
- createComponent({ handler: errorHandler });
- await waitForPromises();
+ describe('when the work item query is unsuccessful', () => {
+ beforeEach(() => {
+ const errorHandler = jest.fn().mockRejectedValue('Oops');
+ createComponent({ handler: errorHandler });
+ return waitForPromises();
+ });
- expect(errorHandler).toHaveBeenCalled();
- expect(findEmptyState().props('description')).toBe(i18n.fetchError);
- expect(findWorkItemTitle().exists()).toBe(false);
+ it('shows empty state with an error message', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props('description')).toBe(i18n.fetchError);
+ });
+
+ it('does not render work item UI elements', () => {
+ expect(findWorkItemType().exists()).toBe(false);
+ expect(findWorkItemTitle().exists()).toBe(false);
+ expect(findCreatedUpdated().exists()).toBe(false);
+ expect(findWorkItemActions().exists()).toBe(false);
+ expect(findWorkItemTwoColumnViewContainer().exists()).toBe(false);
+ });
});
it('shows an error message when WorkItemTitle emits an `error` event', async () => {
diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb
index 41f402c699b..3f5604912ad 100644
--- a/spec/helpers/organizations/organization_helper_spec.rb
+++ b/spec/helpers/organizations/organization_helper_spec.rb
@@ -45,6 +45,10 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
allow_next_instance_of(Organizations::OrganizationAssociationCounter) do |finder|
allow(finder).to receive(:execute).and_return(stubbed_results)
end
+ allow(helper).to receive(:restricted_visibility_levels).and_return([])
+ allow(helper).to receive(:groups_organization_path)
+ .with(organization)
+ .and_return(groups_organization_path)
end
shared_examples 'includes that the user can create a group' do |method|
@@ -282,10 +286,6 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
allow(helper).to receive(:groups_and_projects_organization_path)
.with(organization, { display: 'groups' })
.and_return(groups_and_projects_organization_path)
- allow(helper).to receive(:groups_organization_path)
- .with(organization)
- .and_return(groups_organization_path)
- allow(helper).to receive(:restricted_visibility_levels).and_return([])
stub_application_setting(default_group_visibility: Gitlab::VisibilityLevel::PUBLIC)
end
@@ -295,7 +295,6 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
'base_path' => root_url,
'groups_and_projects_organization_path' => groups_and_projects_organization_path,
'groups_organization_path' => groups_organization_path,
- 'mattermost_enabled' => false,
'available_visibility_levels' => [
Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
@@ -313,12 +312,33 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
describe '#organization_groups_edit_app_data' do
let_it_be(:group) { build_stubbed(:group, organization: organization) }
+ before do
+ allow(helper).to receive(:groups_and_projects_organization_path)
+ .with(organization, { display: 'groups' })
+ .and_return(groups_and_projects_organization_path)
+ end
+
it 'returns expected json' do
- expect(Gitlab::Json.parse(helper.organization_groups_edit_app_data(group))).to eq(
+ expect(Gitlab::Json.parse(helper.organization_groups_edit_app_data(organization, group))).to eq(
{
'group' => {
- 'full_name' => group.full_name
- }
+ 'id' => group.id,
+ 'full_name' => group.full_name,
+ 'name' => group.name,
+ 'path' => group.path,
+ "visibility_level" => group.visibility_level
+ },
+ 'base_path' => root_url,
+ 'groups_and_projects_organization_path' => groups_and_projects_organization_path,
+ 'groups_organization_path' => groups_organization_path,
+ 'available_visibility_levels' => [
+ Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC
+ ],
+ 'restricted_visibility_levels' => [],
+ 'path_maxlength' => ::Namespace::URL_MAX_LENGTH,
+ 'path_pattern' => Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS
}
)
end
@@ -361,16 +381,54 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
end
describe '#organization_activity_app_data' do
+ let_it_be(:expected_event_types) do
+ [
+ {
+ 'title' => 'Comments',
+ 'value' => EventFilter::COMMENTS
+ },
+ {
+ 'title' => 'Designs',
+ 'value' => EventFilter::DESIGNS
+ },
+ {
+ 'title' => 'Issue events',
+ 'value' => EventFilter::ISSUE
+ },
+ {
+ 'title' => 'Merge events',
+ 'value' => EventFilter::MERGED
+ },
+ {
+ 'title' => 'Push events',
+ 'value' => EventFilter::PUSH
+ },
+ {
+ 'title' => 'Team',
+ 'value' => EventFilter::TEAM
+ },
+ {
+ 'title' => 'Wiki',
+ 'value' => EventFilter::WIKI
+ }
+ ]
+ end
+
before do
allow(helper).to receive(:activity_organization_path)
.with(organization, { format: :json })
.and_return(activity_organization_path)
+
+ allow(helper).to receive(:organization_activity_event_types)
+ .and_return(expected_event_types)
end
it 'returns expected data object' do
expect(Gitlab::Json.parse(helper.organization_activity_app_data(organization))).to eq(
{
- 'organization_activity_path' => activity_organization_path
+ 'organization_activity_path' => activity_organization_path,
+ 'organization_activity_event_types' => expected_event_types,
+ 'organization_activity_all_event' => EventFilter::ALL
}
)
end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index c2027c35ab3..7a90a79d56e 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -152,6 +152,75 @@ RSpec.describe VisibilityLevelHelper, feature_category: :system_access do
end
end
+ describe '#disallowed_visibility_level_by_parent?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:parent_group) { create(:group, parent_group_visibility_level) }
+ let(:group) { build(:group, :private, parent: parent_group) }
+
+ subject { helper.disallowed_visibility_level_by_parent?(group, visibility_level) }
+
+ where(:parent_group_visibility_level, :visibility_level, :expected) do
+ :public | Gitlab::VisibilityLevel::PUBLIC | false
+ :public | Gitlab::VisibilityLevel::INTERNAL | false
+ :public | Gitlab::VisibilityLevel::PRIVATE | false
+ :internal | Gitlab::VisibilityLevel::PUBLIC | true
+ :internal | Gitlab::VisibilityLevel::INTERNAL | false
+ :internal | Gitlab::VisibilityLevel::PRIVATE | false
+ :private | Gitlab::VisibilityLevel::PUBLIC | true
+ :private | Gitlab::VisibilityLevel::INTERNAL | true
+ :private | Gitlab::VisibilityLevel::PRIVATE | false
+ end
+
+ with_them do
+ it { is_expected.to eq expected }
+ end
+ end
+
+ shared_examples_for 'disallowed visibility level by child' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:child_visibility_level, :visibility_level, :expected) do
+ Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PUBLIC | false
+ Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+ Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PRIVATE | true
+ Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PUBLIC | false
+ Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::INTERNAL | false
+ Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PRIVATE | true
+ Gitlab::VisibilityLevel::PRIVATE | Gitlab::VisibilityLevel::PUBLIC | false
+ Gitlab::VisibilityLevel::PRIVATE | Gitlab::VisibilityLevel::INTERNAL | false
+ Gitlab::VisibilityLevel::PRIVATE | Gitlab::VisibilityLevel::PRIVATE | false
+ end
+
+ with_them do
+ before do
+ child.update!(visibility_level: child_visibility_level)
+ end
+
+ it { is_expected.to eq expected }
+ end
+ end
+
+ describe '#disallowed_visibility_level_by_projects?' do
+ let_it_be(:group) { create(:group, :public) }
+
+ let_it_be_with_reload(:child) { create(:project, group: group) }
+
+ subject { helper.disallowed_visibility_level_by_projects?(group, visibility_level) }
+
+ it_behaves_like 'disallowed visibility level by child'
+ end
+
+ describe '#disallowed_visibility_level_by_sub_groups?' do
+ let_it_be(:group) { create(:group, :public) }
+
+ let_it_be_with_reload(:child) { create(:group, parent: group) }
+
+ subject { helper.disallowed_visibility_level_by_sub_groups?(group, visibility_level) }
+
+ it_behaves_like 'disallowed visibility level by child'
+ end
+
describe "selected_visibility_level" do
let(:group) { create(:group, :public) }
let!(:project) { create(:project, :internal, group: group) }
@@ -323,4 +392,102 @@ RSpec.describe VisibilityLevelHelper, feature_category: :system_access do
it { is_expected.to eq(expected) }
end
end
+
+ describe '#all_visibility_levels' do
+ subject { helper.all_visibility_levels }
+
+ it 'returns all visibility levels' do
+ is_expected.to eq [
+ Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC
+ ]
+ end
+ end
+
+ describe '#disabled_visibility_level?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be_with_reload(:child) { create(:project, group: group) }
+
+ subject { helper.disabled_visibility_level?(group, visibility_level) }
+
+ where(:restricted_visibility_levels, :child_visibility_level, :visibility_level, :expected) do
+ [] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PUBLIC | false
+ [] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+ [] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PRIVATE | true
+ [] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PUBLIC | false
+ [] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::INTERNAL | false
+ [] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PRIVATE | true
+ [] | Gitlab::VisibilityLevel::PRIVATE | Gitlab::VisibilityLevel::PUBLIC | false
+ [] | Gitlab::VisibilityLevel::PRIVATE | Gitlab::VisibilityLevel::INTERNAL | false
+ [] | Gitlab::VisibilityLevel::PRIVATE | Gitlab::VisibilityLevel::PRIVATE | false
+
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PUBLIC | true
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PRIVATE | true
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PUBLIC | true
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::INTERNAL | false
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PRIVATE | true
+
+ [Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PUBLIC | false
+ [Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+ [Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PRIVATE | true
+ [Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PUBLIC | false
+ [Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::INTERNAL | true
+ [Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::INTERNAL | Gitlab::VisibilityLevel::PRIVATE | true
+
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PUBLIC | true
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+
+ Gitlab::VisibilityLevel.values | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::PUBLIC | true
+ Gitlab::VisibilityLevel.values | Gitlab::VisibilityLevel::PUBLIC | Gitlab::VisibilityLevel::INTERNAL | true
+ end
+
+ with_them do
+ before do
+ allow(helper).to receive(:current_user) { user }
+ stub_application_setting(restricted_visibility_levels: restricted_visibility_levels)
+
+ child.update!(visibility_level: child_visibility_level)
+ end
+
+ it { is_expected.to eq expected }
+ end
+ end
+
+ describe '#restricted_visibility_level?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:user) { create(:user) }
+
+ subject { helper.restricted_visibility_level?(visibility_level) }
+
+ where(:restricted_visibility_levels, :visibility_level, :expected) do
+ [] | Gitlab::VisibilityLevel::PUBLIC | false
+ [] | Gitlab::VisibilityLevel::INTERNAL | false
+ [] | Gitlab::VisibilityLevel::PRIVATE | false
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::PUBLIC | true
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::INTERNAL | false
+ [Gitlab::VisibilityLevel::PUBLIC] | Gitlab::VisibilityLevel::PRIVATE | false
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PUBLIC | true
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::INTERNAL | true
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | Gitlab::VisibilityLevel::PRIVATE | false
+ Gitlab::VisibilityLevel.values | Gitlab::VisibilityLevel::PUBLIC | true
+ Gitlab::VisibilityLevel.values | Gitlab::VisibilityLevel::INTERNAL | true
+ Gitlab::VisibilityLevel.values | Gitlab::VisibilityLevel::PRIVATE | true
+ end
+
+ with_them do
+ before do
+ allow(helper).to receive(:current_user) { user }
+ stub_application_setting(restricted_visibility_levels: restricted_visibility_levels)
+ end
+
+ it { is_expected.to eq expected }
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_boards_epic_user_preferences_group_id_spec.rb b/spec/lib/gitlab/background_migration/backfill_boards_epic_user_preferences_group_id_spec.rb
new file mode 100644
index 00000000000..2d52ad3424b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_boards_epic_user_preferences_group_id_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillBoardsEpicUserPreferencesGroupId,
+ feature_category: :portfolio_management,
+ schema: 20240521094913 do
+ include_examples 'desired sharding key backfill job' do
+ let(:batch_table) { :boards_epic_user_preferences }
+ let(:backfill_column) { :group_id }
+ let(:backfill_via_table) { :epics }
+ let(:backfill_via_column) { :group_id }
+ let(:backfill_via_foreign_key) { :epic_id }
+ end
+end
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 17d416b0f0a..21789592c29 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::MergeRequestParser do
+RSpec.describe Gitlab::ImportExport::MergeRequestParser, feature_category: :importers do
include ProjectForksHelper
let(:user) { create(:user) }
@@ -53,6 +53,36 @@ RSpec.describe Gitlab::ImportExport::MergeRequestParser do
expect(parsed_merge_request).to eq(merge_request)
end
+ describe 'target branch' do
+ before do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:branch_exists?).and_call_original
+ allow(instance).to receive(:branch_exists?).with(merge_request.target_branch).and_return(false)
+ allow(instance).to receive(:fork_merge_request?).and_return(true)
+ end
+ end
+
+ it 'parses a MR that has no target branch' do
+ expect(parsed_merge_request).to eq(merge_request)
+ end
+
+ context 'when target branch fails to be created' do
+ it 'logs the error' do
+ allow(project.repository).to receive(:create_branch).and_raise(StandardError, 'Error!')
+
+ expect(Gitlab::Import::Logger).to receive(:warn).with(
+ message: 'Import warning: Failed to create target branch',
+ target_branch: merge_request.target_branch,
+ diff_head_sha: anything,
+ merge_request_iid: merge_request.iid,
+ error: 'Error!'
+ )
+
+ expect(parsed_merge_request).to eq(merge_request)
+ end
+ end
+ end
+
it 'parses a MR that is closed' do
merge_request.update!(state: :closed, source_branch: 'new_branch')
diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb
index f697b2212d4..a550062b543 100644
--- a/spec/lib/gitlab/internal_events_spec.rb
+++ b/spec/lib/gitlab/internal_events_spec.rb
@@ -591,6 +591,16 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
end
context 'with early access program tracking' do
+ let(:namespace_participating) { false }
+ let(:namespace) do
+ settings = create(:namespace_settings, early_access_program_participant: namespace_participating)
+ create(:namespace, namespace_settings: settings)
+ end
+
+ let(:event_kwargs) do
+ { user: user, project: project, send_snowplow_event: send_snowplow_event, namespace: namespace }
+ end
+
shared_examples 'does not create early access program tracking event' do
it do
track_event
@@ -601,7 +611,7 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
before do
allow(sdk_client).to receive(:track)
- .with(event_name, { project_id: project.id, namespace_id: project.namespace.id })
+ .with(event_name, { project_id: project&.id, namespace_id: namespace&.id })
end
context 'when early_access_program FF is enabled' do
@@ -615,25 +625,31 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
it_behaves_like 'does not create early access program tracking event'
end
+ context 'without namespace' do
+ let(:project) { nil }
+ let(:namespace) { nil }
+
+ it_behaves_like 'does not create early access program tracking event'
+ end
+
context 'with user' do
- context 'when user is not early access program participant' do
+ context 'when namespace is not early access program participant' do
it_behaves_like 'does not create early access program tracking event'
end
- context 'when user is early access program participant' do
+ context 'when namespace is early access program participant' do
+ let(:namespace_participating) { true }
let(:event_name) { 'g_edit_by_snippet_ide' }
let(:additional_properties) { { label: 'label_name' } }
let(:user) { create(:user) }
before do
- preference_stub = instance_double(UserPreference, early_access_event_tracking?: true)
- allow(user).to receive(:user_preference).and_return(preference_stub)
allow(sdk_client).to receive(:track)
.with(
event_name,
{
project_id: project.id,
- namespace_id: project.namespace.id,
+ namespace_id: namespace.id,
additional_properties: additional_properties
}
)
diff --git a/spec/lib/unnested_in_filters/rewriter_spec.rb b/spec/lib/unnested_in_filters/rewriter_spec.rb
index 659f9af8bd3..cc5fe6de9d2 100644
--- a/spec/lib/unnested_in_filters/rewriter_spec.rb
+++ b/spec/lib/unnested_in_filters/rewriter_spec.rb
@@ -210,7 +210,7 @@ RSpec.describe UnnestedInFilters::Rewriter, feature_category: :shared do
let(:users_unnest) do
'FROM
- unnest\(ARRAY\(SELECT "users"."state" FROM "users"\)::character varying\[\]\) AS "states"\("state"\)\,
+ \(SELECT "users"."state" FROM "users"\) AS "states"\("state"\)\,
unnest\(\'{1\,2}\'::smallint\[\]\) AS "user_types"\("user_type"\)\,
LATERAL \('
end
diff --git a/spec/migrations/20240521094917_queue_backfill_boards_epic_user_preferences_group_id_spec.rb b/spec/migrations/20240521094917_queue_backfill_boards_epic_user_preferences_group_id_spec.rb
new file mode 100644
index 00000000000..f4895d0efda
--- /dev/null
+++ b/spec/migrations/20240521094917_queue_backfill_boards_epic_user_preferences_group_id_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillBoardsEpicUserPreferencesGroupId, feature_category: :portfolio_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :boards_epic_user_preferences,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ gitlab_schema: :gitlab_main_cell,
+ job_arguments: [
+ :group_id,
+ :epics,
+ :group_id,
+ :epic_id
+ ]
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/increase_quantity_for_gitlabcom_duo_pro_trials_spec.rb b/spec/migrations/increase_quantity_for_gitlabcom_duo_pro_trials_spec.rb
new file mode 100644
index 00000000000..96dfbf195c8
--- /dev/null
+++ b/spec/migrations/increase_quantity_for_gitlabcom_duo_pro_trials_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe IncreaseQuantityForGitlabcomDuoProTrials, feature_category: :code_suggestions do
+ let(:addon_purchases) { table(:subscription_add_on_purchases) }
+ let(:addons) { table(:subscription_add_ons) }
+ let(:namespaces) { table(:namespaces) }
+
+ let!(:today) { Date.current }
+ # GitlabSubscriptions::AddOn is an EE model. GitlabSubscriptions::AddOn.names is not available in FOSS.
+ # So we hard-code the value `1` for GitlabSubscriptions::AddOn.names[:code_suggestions].
+ let!(:duo_pro_addon) { addons.create!(name: 1, description: "code suggestions") }
+ # Similarly as above, hard-code the value `2` for GitlabSubscriptions::AddOn.names[:product_analytics]
+ let!(:product_analytics_addon) { addons.create!(name: 2, description: "product analytics") }
+
+ let!(:group1) { namespaces.create!(name: 'group1', path: 'group1', type: 'Group') }
+ let!(:group2) { namespaces.create!(name: 'group2', path: 'group2', type: 'Group') }
+ let!(:group3) { namespaces.create!(name: 'group3', path: 'group3', type: 'Group') }
+ let!(:group4) { namespaces.create!(name: 'group4', path: 'group4', type: 'Group') }
+ let!(:group5) { namespaces.create!(name: 'group5', path: 'group5', type: 'Group') }
+
+ let!(:duo_pro_trial_expired) do
+ addon_purchases.create!(
+ subscription_add_on_id: duo_pro_addon.id,
+ namespace_id: group1.id,
+ quantity: 50,
+ expires_on: today - 1.day,
+ purchase_xid: "trial-order-1",
+ last_assigned_users_refreshed_at: nil,
+ trial: true
+ )
+ end
+
+ let!(:duo_pro_trial_active_1) do
+ addon_purchases.create!(
+ subscription_add_on_id: duo_pro_addon.id,
+ namespace_id: group2.id,
+ quantity: 50,
+ expires_on: today,
+ purchase_xid: "trial-order-2",
+ last_assigned_users_refreshed_at: nil,
+ trial: true
+ )
+ end
+
+ let!(:duo_pro_trial_active_2) do
+ addon_purchases.create!(
+ subscription_add_on_id: duo_pro_addon.id,
+ namespace_id: group3.id,
+ quantity: 50,
+ expires_on: today + 1.day,
+ purchase_xid: "trial-order-3",
+ last_assigned_users_refreshed_at: nil,
+ trial: true
+ )
+ end
+
+ let!(:duo_pro_paid_active) do
+ addon_purchases.create!(
+ subscription_add_on_id: duo_pro_addon.id,
+ namespace_id: group4.id,
+ quantity: 20,
+ expires_on: today + 1.day,
+ purchase_xid: "A-S123456",
+ last_assigned_users_refreshed_at: nil,
+ trial: false
+ )
+ end
+
+ let!(:product_analytics_addon_active) do
+ addon_purchases.create!(
+ subscription_add_on_id: product_analytics_addon.id,
+ namespace_id: group5.id,
+ quantity: 20,
+ expires_on: today + 1.day,
+ purchase_xid: "A-S123457",
+ last_assigned_users_refreshed_at: nil,
+ trial: false
+ )
+ end
+
+ describe '#up' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'update only the active duo_pro trials quantity to 100' do
+ expect do
+ migrate!
+ end.to change { duo_pro_trial_active_1.reload.quantity }.from(50).to(100).and(
+ change { duo_pro_trial_active_2.reload.quantity }.from(50).to(100)
+ ).and(
+ not_change { duo_pro_trial_expired.reload.quantity }
+ ).and(
+ not_change { duo_pro_paid_active.reload.quantity }
+ ).and(
+ not_change { product_analytics_addon_active.reload.quantity }
+ )
+ end
+ end
+end
diff --git a/spec/models/members/members/member_approval_spec.rb b/spec/models/members/members/member_approval_spec.rb
index d456c75e2eb..1ac1e98c561 100644
--- a/spec/models/members/members/member_approval_spec.rb
+++ b/spec/models/members/members/member_approval_spec.rb
@@ -15,5 +15,91 @@ RSpec.describe Members::MemberApproval, feature_category: :groups_and_projects d
it { is_expected.to validate_presence_of(:new_access_level) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:member_namespace) }
+
+ context 'with metadata' do
+ subject { build(:member_approval, metadata: attribute_mapping) }
+
+ context 'with valid JSON schemas' do
+ let(:attribute_mapping) do
+ {
+ expires_at: expiry,
+ member_role_id: nil
+ }
+ end
+
+ context 'with empty metadata' do
+ let(:attribute_mapping) { {} }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with valid expiry' do
+ let(:expiry) { "1970-01-01" }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with empty expiry' do
+ let(:expiry) { "" }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with not null member_role_id' do
+ let(:attribute_mapping) do
+ {
+ member_role_id: 3
+ }
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when property has extra attributes' do
+ let(:attribute_mapping) do
+ { access_level: 20 }
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'with invalid JSON schemas' do
+ shared_examples 'is invalid record' do
+ it do
+ expect(subject).to be_invalid
+ expect(subject.errors.messages[:metadata]).to eq(['must be a valid json schema'])
+ end
+ end
+
+ context 'when property is not an object' do
+ let(:attribute_mapping) do
+ "That is not a valid schema"
+ end
+
+ it_behaves_like 'is invalid record'
+ end
+
+ context 'with invalid expiry' do
+ let(:attribute_mapping) do
+ {
+ expires_at: "1242"
+ }
+ end
+
+ it_behaves_like 'is invalid record'
+ end
+
+ context 'with member_role_id' do
+ let(:attribute_mapping) do
+ {
+ member_role_id: "some role"
+ }
+ end
+
+ it_behaves_like 'is invalid record'
+ end
+ end
+ end
end
end
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 1792965a2be..1e228f2e50e 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -24,6 +24,17 @@ RSpec.describe NamespaceSetting, feature_category: :groups_and_projects, type: :
it { expect(setting.default_branch_protection_defaults).to eq({}) }
end
+ describe '.for_namespaces' do
+ let(:setting_1) { create(:namespace_settings, namespace: namespace_1) }
+ let(:setting_2) { create(:namespace_settings, namespace: namespace_2) }
+ let_it_be(:namespace_1) { create(:namespace) }
+ let_it_be(:namespace_2) { create(:namespace) }
+
+ it 'returns namespace setting for the given projects' do
+ expect(described_class.for_namespaces(namespace_1)).to contain_exactly(setting_1)
+ end
+ end
+
describe "validations" do
describe "#default_branch_name_content" do
shared_examples "doesn't return an error" do
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index 3e92bb9e07e..6bd889b97bd 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -413,6 +413,20 @@ RSpec.describe API::GenericPackages, feature_category: :package_registry do
end
end
end
+
+ context 'when there is + sign is in filename' do
+ it 'creates a package and package file with filename' do
+ headers = workhorse_headers.merge(auth_header)
+
+ upload_file(params, headers, file_name: 'my+file.tar.gz')
+
+ aggregate_failures do
+ package = project.packages.generic.last
+ expect(response).to have_gitlab_http_status(:created)
+ expect(package.package_files.last.file_name).to eq('my+file.tar.gz')
+ end
+ end
+ end
end
context 'when valid personal access token is used' do
@@ -770,6 +784,21 @@ RSpec.describe API::GenericPackages, feature_category: :package_registry do
end
end
+ context 'when there is + sign is in filename' do
+ let(:file_name) { 'my+file.tar.gz' }
+
+ before do
+ project.add_developer(user)
+ package_file.update_column(:file_name, file_name)
+ end
+
+ it 'responds with 200 OK' do
+ download_file(personal_access_token_header, file_name: file_name)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
def download_file(request_headers, package_name: nil, file_name: nil)
package_name ||= package.name
file_name ||= package_file.file_name
diff --git a/spec/scripts/setup/generate_as_if_foss_env_spec.rb b/spec/scripts/setup/generate_as_if_foss_env_spec.rb
index ae8c5673602..fd880c9cb07 100644
--- a/spec/scripts/setup/generate_as_if_foss_env_spec.rb
+++ b/spec/scripts/setup/generate_as_if_foss_env_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe GenerateAsIfFossEnv, feature_category: :tooling do
'rspec integration pg14',
'rspec system pg14',
'rspec migration pg14',
- 'rspec background-migration pg14',
+ 'rspec background_migration pg14',
'rspec-all frontend_fixture',
'build-assets-image',
'build-qa-image',
diff --git a/yarn.lock b/yarn.lock
index 9ed0ca6eba9..0fe63a4ba54 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1331,10 +1331,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.99.0.tgz#85d2752787c3a87d40d5468a797e67c1e736a9fc"
integrity sha512-N1y2W8ti0Pw7N8UYUdcl7DQuIZfOcL0R85EzJlSgXLt+I0NdmX4s3CCqBV1g62ZDxLgfDqBfiLKLCcYz8JkJ8w==
-"@gitlab/ui@80.14.1":
- version "80.14.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-80.14.1.tgz#dcfdd5a3097b4fbf441dc5ac354399ea3736381e"
- integrity sha512-TyVz6CaYyJCwskgWJEUpjfdmJKTWwgKTEWjZa2uf8wGNCjKDO5KlVWQdOtp7//sKZPSoBRbLZrzGJJGgX/Cc+w==
+"@gitlab/ui@80.15.1":
+ version "80.15.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-80.15.1.tgz#7e56fd751dfd93503b442e23a13f6ab7b4c14e77"
+ integrity sha512-VN9DSsL4p1I65bgwZhKO9fnw3+TFyAJ8uEDpOl/XCw0wlmnZyTg3DosxrLVzaeQV0+hqbfBMNIb1gpeTX/8rsg==
dependencies:
"@floating-ui/dom" "1.4.3"
bootstrap-vue "2.23.1"