Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml1
-rw-r--r--app/assets/javascripts/boards/boards_util.js7
-rw-r--r--app/assets/javascripts/boards/constants.js5
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js4
-rw-r--r--app/assets/javascripts/lib/logger/index.js6
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb5
-rw-r--r--app/graphql/types/boards/board_issuable_input_base_type.rb2
-rw-r--r--app/graphql/types/issues/negated_issue_filter_input_type.rb3
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/work_item/type.rb16
-rw-r--r--config/feature_flags/development/keyset_pagination_for_groups_api.yml8
-rw-r--r--db/fixtures/development/001_create_base_work_item_types.rb5
-rw-r--r--db/fixtures/production/003_create_base_work_item_types.rb5
-rw-r--r--db/migrate/20210831203408_upsert_base_work_item_types.rb31
-rw-r--r--db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb17
-rw-r--r--db/schema_migrations/202108312034081
-rw-r--r--db/schema_migrations/202109010655041
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/auth/ldap/ldap-troubleshooting.md10
-rw-r--r--doc/administration/auth/smartcard.md6
-rw-r--r--doc/administration/operations/ssh_certificates.md3
-rw-r--r--doc/administration/raketasks/github_import.md2
-rw-r--r--doc/administration/raketasks/praefect.md2
-rw-r--r--doc/api/graphql/reference/index.md21
-rw-r--r--doc/api/groups.md5
-rw-r--r--doc/api/index.md40
-rw-r--r--doc/api/job_artifacts.md2
-rw-r--r--doc/api/jobs.md2
-rw-r--r--doc/api/packages/composer.md2
-rw-r--r--doc/api/packages/conan.md2
-rw-r--r--doc/api/packages/go_proxy.md2
-rw-r--r--doc/api/packages/helm.md2
-rw-r--r--doc/api/packages/maven.md2
-rw-r--r--doc/api/packages/npm.md2
-rw-r--r--doc/api/packages/nuget.md2
-rw-r--r--doc/api/packages/pypi.md2
-rw-r--r--doc/api/packages/rubygems.md2
-rw-r--r--doc/api/templates/dockerfiles.md2
-rw-r--r--doc/api/templates/gitignores.md2
-rw-r--r--doc/api/templates/gitlab_ci_ymls.md2
-rw-r--r--doc/api/templates/licenses.md2
-rw-r--r--lib/api/groups.rb16
-rw-r--r--lib/api/helpers/pagination_strategies.rb36
-rw-r--r--lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb6
-rw-r--r--lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb2
-rw-r--r--lib/gitlab/database_importers/work_items/base_type_importer.rb15
-rw-r--r--lib/gitlab/pagination/cursor_based_keyset.rb27
-rw-r--r--lib/gitlab/pagination/keyset/cursor_based_request_context.rb15
-rw-r--r--scripts/rspec_helpers.sh2
-rw-r--r--spec/db/development/create_base_work_item_types_spec.rb9
-rw-r--r--spec/db/production/create_base_work_item_types_spec.rb9
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js5
-rw-r--r--spec/frontend/lib/logger/index_spec.js23
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb22
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb59
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb7
-rw-r--r--spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb9
-rw-r--r--spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb48
-rw-r--r--spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb28
-rw-r--r--spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb6
-rw-r--r--spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb48
-rw-r--r--spec/models/ci/pipeline_variable_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb28
-rw-r--r--spec/requests/api/groups_spec.rb121
-rw-r--r--spec/support/shared_examples/work_item_base_types_importer.rb7
-rwxr-xr-xtooling/bin/find_tests5
67 files changed, 670 insertions, 127 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index a8524470aa2..00f65ab7ca8 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -21,7 +21,7 @@
.minimal-rspec-tests:
variables:
- MINIMAL_RSPEC_ENABLED: "true"
+ RSPEC_TESTS_MAPPING_ENABLED: "true"
.decomposed-database-rspec:
variables:
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index 47bdd3b538c..f2d5d872d64 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -82,6 +82,7 @@ detect-tests:
- .detect-test-base
- .rails:rules:detect-tests
variables:
+ RSPEC_TESTS_MAPPING_ENABLED: "true"
MATCHED_TESTS_FILE: tmp/matching_tests.txt
detect-tests as-if-foss:
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 3219d74f85f..cdf25b2d428 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy, cloneDeep } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ListType } from './constants';
+import { ListType, MilestoneIDs } from './constants';
export function getMilestone() {
return null;
@@ -108,7 +108,10 @@ export function formatIssueInput(issueInput, boardConfig) {
return {
...issueInput,
- milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null,
+ milestoneId:
+ milestoneId && milestoneId !== MilestoneIDs.ANY
+ ? fullMilestoneId(milestoneId)
+ : issueInput?.milestoneId,
labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])],
assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])],
};
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 16fb4596726..391e0d1fb0a 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -119,6 +119,11 @@ export const DraggableItemTypes = {
list: 'list',
};
+export const MilestoneIDs = {
+ NONE: 0,
+ ANY: -1,
+};
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index a2056165cb2..1ac6781a0e3 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -1,5 +1,6 @@
import Api from '~/api';
import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -63,8 +64,7 @@ export const deleteFreezePeriod = ({ state, commit }, { id }) => {
});
commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
- // eslint-disable-next-line no-console
- console.error('[gitlab] Unable to delete deploy freeze:', e);
+ logError(`Unable to delete deploy freeze`, e);
});
};
diff --git a/app/assets/javascripts/lib/logger/index.js b/app/assets/javascripts/lib/logger/index.js
new file mode 100644
index 00000000000..0f5353fcbed
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/index.js
@@ -0,0 +1,6 @@
+/* eslint-disable no-console */
+export const LOG_PREFIX = '[gitlab]';
+
+export const logError = (message = '', ...args) => {
+ console.error(LOG_PREFIX, `${message}\n`, ...args);
+};
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 8d77c0f3a8d..9de36b5b7d1 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -30,7 +30,7 @@ module IssueResolverArguments
description: 'Usernames of users assigned to the issue.'
argument :assignee_id, GraphQL::Types::String,
required: false,
- description: 'ID of a user assigned to the issues, "none" and "any" values are supported.'
+ description: 'ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported.'
argument :created_before, Types::TimeType,
required: false,
description: 'Issues created before this date.'
@@ -59,6 +59,9 @@ module IssueResolverArguments
argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
required: false,
description: 'Filter issues by milestone ID wildcard.'
+ argument :my_reaction_emoji, GraphQL::Types::String,
+ required: false,
+ description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
diff --git a/app/graphql/types/boards/board_issuable_input_base_type.rb b/app/graphql/types/boards/board_issuable_input_base_type.rb
index 326f73846d0..81dd21aebec 100644
--- a/app/graphql/types/boards/board_issuable_input_base_type.rb
+++ b/app/graphql/types/boards/board_issuable_input_base_type.rb
@@ -14,7 +14,7 @@ module Types
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
- description: 'Filter by reaction emoji applied by the current user.'
+ description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
end
end
end
diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb
index 5c43c5c33b5..4f620a5b3d9 100644
--- a/app/graphql/types/issues/negated_issue_filter_input_type.rb
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -26,6 +26,9 @@ module Types
argument :milestone_wildcard_id, ::Types::NegatedMilestoneWildcardIdEnum,
required: false,
description: 'Filter by negated milestone wildcard values.'
+ argument :my_reaction_emoji, GraphQL::Types::String,
+ required: false,
+ description: 'Filter by reaction emoji applied by the current user.'
end
end
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index a0e8886414b..3dca77af051 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -8,7 +8,7 @@ module Ci
alias_attribute :secret_value, :value
- validates :key, uniqueness: { scope: :pipeline_id }
+ validates :key, presence: true
def hook_attrs
{ key: key, value: value }
diff --git a/app/models/work_item/type.rb b/app/models/work_item/type.rb
index 16cb7a8be45..03badf95908 100644
--- a/app/models/work_item/type.rb
+++ b/app/models/work_item/type.rb
@@ -9,14 +9,18 @@ class WorkItem::Type < ApplicationRecord
include CacheMarkdownField
+ # Base types need to exist on the DB on app startup
+ # This constant is used by the DB seeder
+ BASE_TYPES = {
+ issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
+ incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
+ test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only
+ requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 } ## EE-only
+ }.freeze
+
cache_markdown_field :description, pipeline: :single_line
- enum base_type: {
- issue: 0,
- incident: 1,
- test_case: 2, ## EE-only
- requirement: 3 ## EE-only
- }
+ enum base_type: BASE_TYPES.transform_values { |value| value[:enum_value] }
belongs_to :namespace, optional: true
has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type
diff --git a/config/feature_flags/development/keyset_pagination_for_groups_api.yml b/config/feature_flags/development/keyset_pagination_for_groups_api.yml
new file mode 100644
index 00000000000..d4bd37565ee
--- /dev/null
+++ b/config/feature_flags/development/keyset_pagination_for_groups_api.yml
@@ -0,0 +1,8 @@
+---
+name: keyset_pagination_for_groups_api
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68346
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339831
+milestone: '14.3'
+type: development
+group: group::access
+default_enabled: false
diff --git a/db/fixtures/development/001_create_base_work_item_types.rb b/db/fixtures/development/001_create_base_work_item_types.rb
new file mode 100644
index 00000000000..7a541ca20b0
--- /dev/null
+++ b/db/fixtures/development/001_create_base_work_item_types.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Gitlab::Seeder.quiet do
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import
+end
diff --git a/db/fixtures/production/003_create_base_work_item_types.rb b/db/fixtures/production/003_create_base_work_item_types.rb
new file mode 100644
index 00000000000..7a541ca20b0
--- /dev/null
+++ b/db/fixtures/production/003_create_base_work_item_types.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Gitlab::Seeder.quiet do
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import
+end
diff --git a/db/migrate/20210831203408_upsert_base_work_item_types.rb b/db/migrate/20210831203408_upsert_base_work_item_types.rb
new file mode 100644
index 00000000000..314412d8d3d
--- /dev/null
+++ b/db/migrate/20210831203408_upsert_base_work_item_types.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class UpsertBaseWorkItemTypes < ActiveRecord::Migration[6.1]
+ module WorkItem
+ class Type < ActiveRecord::Base
+ self.table_name = 'work_item_types'
+
+ enum base_type: {
+ issue: 0,
+ incident: 1,
+ test_case: 2,
+ requirement: 3
+ }
+ end
+ end
+
+ def up
+ # upsert default types
+ WorkItem::Type.find_or_create_by(name: 'Issue', namespace_id: nil, base_type: :issue, icon_name: 'issue-type-issue')
+ WorkItem::Type.find_or_create_by(name: 'Incident', namespace_id: nil, base_type: :incident, icon_name: 'issue-type-incident')
+ WorkItem::Type.find_or_create_by(name: 'Test Case', namespace_id: nil, base_type: :test_case, icon_name: 'issue-type-test-case')
+ WorkItem::Type.find_or_create_by(name: 'Requirement', namespace_id: nil, base_type: :requirement, icon_name: 'issue-type-requirements')
+ end
+
+ def down
+ # We expect this table to be empty at the point of the up migration,
+ # however there is a remote possibility that issues could already be
+ # using one of these types, with a tight foreign constraint.
+ # Therefore we will not attempt to remove any data.
+ end
+end
diff --git a/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb b/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb
new file mode 100644
index 00000000000..77b9e5297a7
--- /dev/null
+++ b/db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnNameAndIdToPublicGroups < Gitlab::Database::Migration[1.0]
+ INDEX_NAME = 'index_namespaces_public_groups_name_id'
+ PUBLIC_VISIBILITY_LEVEL = 20
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :namespaces, [:name, :id], name: INDEX_NAME,
+ where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}"
+ end
+
+ def down
+ remove_concurrent_index_by_name :namespaces, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20210831203408 b/db/schema_migrations/20210831203408
new file mode 100644
index 00000000000..6ab3f810e57
--- /dev/null
+++ b/db/schema_migrations/20210831203408
@@ -0,0 +1 @@
+50a06a2a57ed26c25af53d3d7f6f5ef73efde8a23a36c5f825af56b4d0c4c3f6 \ No newline at end of file
diff --git a/db/schema_migrations/20210901065504 b/db/schema_migrations/20210901065504
new file mode 100644
index 00000000000..f62e6ce1272
--- /dev/null
+++ b/db/schema_migrations/20210901065504
@@ -0,0 +1 @@
+9724a5fc1703418f9b1ea1d5375fc3b01834b30e5ff16c60537db5cb00bc210a \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f149a299c45..d6b561419ed 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -25691,6 +25691,8 @@ CREATE INDEX index_namespaces_on_traversal_ids ON namespaces USING gin (traversa
CREATE INDEX index_namespaces_on_type_and_id_partial ON namespaces USING btree (type, id) WHERE (type IS NOT NULL);
+CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20));
+
CREATE INDEX index_non_requested_project_members_on_source_id_and_type ON members USING btree (source_id, source_type) WHERE ((requested_at IS NULL) AND ((type)::text = 'ProjectMember'::text));
CREATE UNIQUE INDEX index_note_diff_files_on_diff_note_id ON note_diff_files USING btree (diff_note_id);
diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md
index 78421e2d567..cb002ef5643 100644
--- a/doc/administration/auth/ldap/ldap-troubleshooting.md
+++ b/doc/administration/auth/ldap/ldap-troubleshooting.md
@@ -336,7 +336,7 @@ Gitlab::Auth::Ldap::Person.find_by_uid('<uid>', adapter)
### Group memberships **(PREMIUM SELF)**
-#### Membership(s) not granted **(PREMIUM SELF)**
+#### Membership(s) not granted
Sometimes you may think a particular user should be added to a GitLab group via
LDAP group sync, but for some reason it's not happening. There are several
@@ -395,7 +395,7 @@ group sync](#sync-all-groups) in the rails console and [look through the
output](#example-console-output-after-a-group-sync) to see what happens when
GitLab syncs the `admin_group`.
-#### Sync all groups **(PREMIUM SELF)**
+#### Sync all groups
NOTE:
To sync all groups manually when debugging is unnecessary, [use the Rake
@@ -413,7 +413,7 @@ LdapAllGroupsSyncWorker.new.perform
Next, [learn how to read the
output](#example-console-output-after-a-group-sync).
-##### Example console output after a group sync **(PREMIUM SELF)**
+##### Example console output after a group sync
Like the output from the user sync, the output from the [manual group
sync](#sync-all-groups) is also very verbose. However, it contains lots
@@ -503,7 +503,7 @@ stating as such:
No `admin_group` configured for 'ldapmain' provider. Skipping
```
-#### Sync one group **(PREMIUM SELF)**
+#### Sync one group
[Syncing all groups](#sync-all-groups) can produce a lot of noise in the output, which can be
distracting when you're only interested in troubleshooting the memberships of
@@ -525,7 +525,7 @@ EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group)
The output is similar to
[that you get from syncing all groups](#example-console-output-after-a-group-sync).
-#### Query a group in LDAP **(PREMIUM SELF)**
+#### Query a group in LDAP
When you'd like to confirm that GitLab can read a LDAP group and see all its members,
you can run the following:
diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md
index 07c29984552..7e2699d5eb3 100644
--- a/doc/administration/auth/smartcard.md
+++ b/doc/administration/auth/smartcard.md
@@ -28,7 +28,7 @@ GitLab supports two authentication methods:
### Authentication against a local database with X.509 certificates
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/726) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.6 as an experimental feature.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/726) in GitLab 11.6 as an experimental feature.
WARNING:
Smartcard authentication against local databases may change or be removed completely in future
@@ -55,7 +55,7 @@ Certificate:
### Authentication against a local database with X.509 certificates and SAN extension
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8605) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8605) in GitLab 12.3.
Smartcards with X.509 certificates using SAN extensions can be used to authenticate
with GitLab.
@@ -98,7 +98,7 @@ Certificate:
### Authentication against an LDAP server
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7693) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8 as an experimental feature. Smartcard authentication against an LDAP server may change or be removed completely in future releases.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7693) in GitLab 11.8 as an experimental feature. Smartcard authentication against an LDAP server may change or be removed completely in the future.
GitLab implements a standard way of certificate matching following
[RFC4523](https://tools.ietf.org/html/rfc4523). It uses the
diff --git a/doc/administration/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md
index 374eebeb773..814e742b026 100644
--- a/doc/administration/operations/ssh_certificates.md
+++ b/doc/administration/operations/ssh_certificates.md
@@ -6,8 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# User lookup via OpenSSH's AuthorizedPrincipalsCommand **(FREE SELF)**
-> [Available in](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19911) GitLab
-> Community Edition 11.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19911) in GitLab 11.2.
The default SSH authentication for GitLab requires users to upload their SSH
public keys before they can use the SSH transport.
diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md
index 0338732e886..f29e2a6c7f6 100644
--- a/doc/administration/raketasks/github_import.md
+++ b/doc/administration/raketasks/github_import.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# GitHub import **(FREE SELF)**
-> [Introduced]( https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10308) in GitLab 9.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10308) in GitLab 9.1.
To retrieve and import GitHub repositories, you need a [GitHub personal access token](https://github.com/settings/tokens).
A username should be passed as the second argument to the Rake task,
diff --git a/doc/administration/raketasks/praefect.md b/doc/administration/raketasks/praefect.md
index 5fe0546999b..d2fd4943c68 100644
--- a/doc/administration/raketasks/praefect.md
+++ b/doc/administration/raketasks/praefect.md
@@ -7,7 +7,7 @@ type: reference
# Praefect Rake tasks **(FREE SELF)**
-> [Introduced]( https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28369) in GitLab 12.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28369) in GitLab 12.10.
Rake tasks are available for projects that have been created on Praefect storage. See the
[Praefect documentation](../gitaly/praefect.md) for information on configuring Praefect.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 880788b361b..4622618db42 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -9999,7 +9999,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="groupissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. |
+| <a id="groupissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. |
| <a id="groupissuesassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. |
| <a id="groupissuesassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="groupissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
@@ -10016,6 +10016,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupissueslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
| <a id="groupissuesmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. |
| <a id="groupissuesmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. |
+| <a id="groupissuesmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="groupissuesnot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. |
| <a id="groupissuessearch"></a>`search` | [`String`](#string) | Search query for issue title or description. |
| <a id="groupissuessort"></a>`sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. |
@@ -12428,7 +12429,7 @@ Returns [`Issue`](#issue).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="projectissueassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. |
+| <a id="projectissueassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. |
| <a id="projectissueassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. |
| <a id="projectissueassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectissueauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
@@ -12444,6 +12445,7 @@ Returns [`Issue`](#issue).
| <a id="projectissuelabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
| <a id="projectissuemilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. |
| <a id="projectissuemilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. |
+| <a id="projectissuemyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="projectissuenot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. |
| <a id="projectissuesearch"></a>`search` | [`String`](#string) | Search query for issue title or description. |
| <a id="projectissuesort"></a>`sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. |
@@ -12463,7 +12465,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="projectissuestatuscountsassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. |
+| <a id="projectissuestatuscountsassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. |
| <a id="projectissuestatuscountsassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. |
| <a id="projectissuestatuscountsassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectissuestatuscountsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
@@ -12476,6 +12478,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <a id="projectissuestatuscountslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
| <a id="projectissuestatuscountsmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. |
| <a id="projectissuestatuscountsmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. |
+| <a id="projectissuestatuscountsmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="projectissuestatuscountsnot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. |
| <a id="projectissuestatuscountssearch"></a>`search` | [`String`](#string) | Search query for issue title or description. |
| <a id="projectissuestatuscountstypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter issues by the given issue types. |
@@ -12496,7 +12499,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
-| <a id="projectissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues, "none" and "any" values are supported. |
+| <a id="projectissuesassigneeid"></a>`assigneeId` | [`String`](#string) | ID of a user assigned to the issues. Wildcard values "NONE" and "ANY" are supported. |
| <a id="projectissuesassigneeusername"></a>`assigneeUsername` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.11. Use `assigneeUsernames`. |
| <a id="projectissuesassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. |
| <a id="projectissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
@@ -12512,6 +12515,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectissueslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
| <a id="projectissuesmilestonetitle"></a>`milestoneTitle` | [`[String]`](#string) | Milestone applied to this issue. |
| <a id="projectissuesmilestonewildcardid"></a>`milestoneWildcardId` | [`MilestoneWildcardId`](#milestonewildcardid) | Filter issues by milestone ID wildcard. |
+| <a id="projectissuesmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="projectissuesnot"></a>`not` | [`NegatedIssueFilterInput`](#negatedissuefilterinput) | Negated arguments. |
| <a id="projectissuessearch"></a>`search` | [`String`](#string) | Search query for issue title or description. |
| <a id="projectissuessort"></a>`sort` | [`IssueSort`](#issuesort) | Sort issues by this criteria. |
@@ -17359,7 +17363,7 @@ Field that are available while modifying the custom mapping attributes for an HT
| <a id="boardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`IterationWildcardId`](#iterationwildcardid) | Filter by iteration ID wildcard. |
| <a id="boardissueinputlabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. |
| <a id="boardissueinputmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Filter by milestone title. |
-| <a id="boardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. |
+| <a id="boardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="boardissueinputnot"></a>`not` | [`NegatedBoardIssueInput`](#negatedboardissueinput) | List of negated arguments. |
| <a id="boardissueinputreleasetag"></a>`releaseTag` | [`String`](#string) | Filter by release tag. |
| <a id="boardissueinputsearch"></a>`search` | [`String`](#string) | Search query for issue title or description. |
@@ -17452,7 +17456,7 @@ Input type for DastSiteProfile authentication.
| ---- | ---- | ----------- |
| <a id="epicfiltersauthorusername"></a>`authorUsername` | [`String`](#string) | Filter by author username. |
| <a id="epicfilterslabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. |
-| <a id="epicfiltersmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. |
+| <a id="epicfiltersmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="epicfiltersnot"></a>`not` | [`NegatedEpicBoardIssueInput`](#negatedepicboardissueinput) | Negated epic arguments. |
| <a id="epicfilterssearch"></a>`search` | [`String`](#string) | Search query for epic title or description. |
@@ -17515,7 +17519,7 @@ Represents an escalation rule.
| <a id="negatedboardissueinputiterationwildcardid"></a>`iterationWildcardId` | [`NegatedIterationWildcardId`](#negatediterationwildcardid) | Filter by iteration ID wildcard. |
| <a id="negatedboardissueinputlabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. |
| <a id="negatedboardissueinputmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Filter by milestone title. |
-| <a id="negatedboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. |
+| <a id="negatedboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
| <a id="negatedboardissueinputreleasetag"></a>`releaseTag` | [`String`](#string) | Filter by release tag. |
| <a id="negatedboardissueinputtypes"></a>`types` | [`[IssueType!]`](#issuetype) | Filter by the given issue types. |
| <a id="negatedboardissueinputweight"></a>`weight` | [`String`](#string) | Filter by weight. |
@@ -17528,7 +17532,7 @@ Represents an escalation rule.
| ---- | ---- | ----------- |
| <a id="negatedepicboardissueinputauthorusername"></a>`authorUsername` | [`String`](#string) | Filter by author username. |
| <a id="negatedepicboardissueinputlabelname"></a>`labelName` | [`[String]`](#string) | Filter by label name. |
-| <a id="negatedepicboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. |
+| <a id="negatedepicboardissueinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported. |
### `NegatedEpicFilterInput`
@@ -17556,6 +17560,7 @@ Represents an escalation rule.
| <a id="negatedissuefilterinputlabelname"></a>`labelName` | [`[String!]`](#string) | Labels not applied to this issue. |
| <a id="negatedissuefilterinputmilestonetitle"></a>`milestoneTitle` | [`[String!]`](#string) | Milestone not applied to this issue. |
| <a id="negatedissuefilterinputmilestonewildcardid"></a>`milestoneWildcardId` | [`NegatedMilestoneWildcardId`](#negatedmilestonewildcardid) | Filter by negated milestone wildcard values. |
+| <a id="negatedissuefilterinputmyreactionemoji"></a>`myReactionEmoji` | [`String`](#string) | Filter by reaction emoji applied by the current user. |
| <a id="negatedissuefilterinputweight"></a>`weight` | [`String`](#string) | Weight not applied to the issue. |
### `OncallRotationActivePeriodInputType`
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 3831aef10c9..b1119c9d5a4 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -13,6 +13,11 @@ authentication, only public groups are returned.
By default, this request returns 20 results at a time because the API results [are paginated](index.md#pagination).
+When accessed without authentication, this endpoint also supports [keyset pagination](index.md#keyset-based-pagination):
+
+- When requesting consecutive pages of results, we recommend you use keyset pagination.
+- Beyond a specific offset limit (specified by [max offset allowed by the REST API for offset-based pagination](../administration/instance_limits.md#max-offset-allowed-by-the-rest-api-for-offset-based-pagination)), offset pagination is unavailable.
+
Parameters:
| Attribute | Type | Required | Description |
diff --git a/doc/api/index.md b/doc/api/index.md
index f7148dcf472..ca8b7392e32 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -462,22 +462,43 @@ The response header includes a link to the next page. For example:
```http
HTTP/1.1 200 OK
...
-Links: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next"
Link: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next"
Status: 200 OK
...
```
+The link to the next page contains an additional filter `id_after=42` that
+excludes already-retrieved records.
+
+As another example, the following request lists 50 [groups](groups.md) per page ordered
+by `name` ascending using keyset pagination:
+
+```shell
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups?pagination=keyset&per_page=50&order_by=name&sort=asc"
+```
+
+The response header includes a link to the next page:
+
+```http
+HTTP/1.1 200 OK
+...
+Link: <https://gitlab.example.com/api/v4/groups?pagination=keyset&per_page=50&order_by=name&sort=asc&cursor=eyJuYW1lIjoiRmxpZ2h0anMiLCJpZCI6IjI2IiwiX2tkIjoibiJ9>; rel="next"
+Status: 200 OK
+...
+```
+
+The link to the next page contains an additional filter `cursor=eyJuYW1lIjoiRmxpZ2h0anMiLCJpZCI6IjI2IiwiX2tkIjoibiJ9` that
+excludes already-retrieved records.
+
+The type of filter depends on the
+`order_by` option used, and we can have more than one additional filter.
+
WARNING:
-The `Links` header is scheduled to be removed in GitLab 14.0 to be aligned with the
+The `Links` header was removed in GitLab 14.0 to be aligned with the
[W3C `Link` specification](https://www.w3.org/wiki/LinkHeader). The `Link`
header was [added in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33714)
and should be used instead.
-The link to the next page contains an additional filter `id_after=42` that
-excludes already-retrieved records. The type of filter depends on the
-`order_by` option used, and we may have more than one additional filter.
-
When the end of the collection is reached and there are no additional
records to retrieve, the `Link` header is absent and the resulting array is
empty.
@@ -489,9 +510,10 @@ pagination headers.
Keyset-based pagination is supported only for selected resources and ordering
options:
-| Resource | Order |
-|-------------------------|-------|
-| [Projects](projects.md) | `order_by=id` only. |
+| Resource | Options | Availability |
+|:-------------------------|:---------------------------------|:----------------------------------------|
+| [Projects](projects.md) | `order_by=id` only | Authenticated and unauthenticated users |
+| [Groups](groups.md) | `order_by=name`, `sort=asc` only | Unauthenticated users only |
## Path parameters
diff --git a/doc/api/job_artifacts.md b/doc/api/job_artifacts.md
index 0a39400dfd4..ae3b58d0764 100644
--- a/doc/api/job_artifacts.md
+++ b/doc/api/job_artifacts.md
@@ -4,7 +4,7 @@ group: Testing
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Job Artifacts API
+# Job Artifacts API **(FREE)**
## Get job artifacts
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 42774b80b27..ac8b756beac 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -466,7 +466,7 @@ Example of response
## Get Kubernetes Agents by `CI_JOB_TOKEN` **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324269) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.11.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324269) in GitLab 13.11.
Retrieve the job that generated the `CI_JOB_TOKEN`, along with a list of allowed GitLab
Kubernetes Agents.
diff --git a/doc/api/packages/composer.md b/doc/api/packages/composer.md
index 0e66654b494..b3a27519729 100644
--- a/doc/api/packages/composer.md
+++ b/doc/api/packages/composer.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Composer API
+# Composer API **(FREE)**
This is the API documentation for [Composer Packages](../../user/packages/composer_repository/index.md).
diff --git a/doc/api/packages/conan.md b/doc/api/packages/conan.md
index 88ed2524173..f5d08ed7ef8 100644
--- a/doc/api/packages/conan.md
+++ b/doc/api/packages/conan.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Conan API
+# Conan API **(FREE)**
This is the API documentation for [Conan Packages](../../user/packages/conan_repository/index.md).
diff --git a/doc/api/packages/go_proxy.md b/doc/api/packages/go_proxy.md
index 2f81435db42..ffafc387951 100644
--- a/doc/api/packages/go_proxy.md
+++ b/doc/api/packages/go_proxy.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Go Proxy API
+# Go Proxy API **(FREE SELF)**
This is the API documentation for [Go Packages](../../user/packages/go_proxy/index.md).
This API is behind a feature flag that is disabled by default. GitLab administrators with access to
diff --git a/doc/api/packages/helm.md b/doc/api/packages/helm.md
index f1d5f24cd99..fba3898a247 100644
--- a/doc/api/packages/helm.md
+++ b/doc/api/packages/helm.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Helm API
+# Helm API **(FREE)**
This is the API documentation for [Helm](../../user/packages/helm_repository/index.md).
diff --git a/doc/api/packages/maven.md b/doc/api/packages/maven.md
index d03c9be3060..b4b3d579ffb 100644
--- a/doc/api/packages/maven.md
+++ b/doc/api/packages/maven.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Maven API
+# Maven API **(FREE)**
This is the API documentation for [Maven Packages](../../user/packages/maven_repository/index.md).
diff --git a/doc/api/packages/npm.md b/doc/api/packages/npm.md
index a1d29e9691c..24ac1a640c9 100644
--- a/doc/api/packages/npm.md
+++ b/doc/api/packages/npm.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.example/handbook/engineering/ux/technical-writing/#assignments
---
-# npm API
+# npm API **(FREE)**
This is the API documentation for [npm Packages](../../user/packages/npm_registry/index.md).
diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md
index d19e2dfa65b..9d73beac452 100644
--- a/doc/api/packages/nuget.md
+++ b/doc/api/packages/nuget.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about..example/handbook/engineering/ux/technical-writing/#assignments
---
-# NuGet API
+# NuGet API **(FREE)**
This is the API documentation for [NuGet Packages](../../user/packages/nuget_repository/index.md).
diff --git a/doc/api/packages/pypi.md b/doc/api/packages/pypi.md
index 8a71bf4588a..a1c96d03297 100644
--- a/doc/api/packages/pypi.md
+++ b/doc/api/packages/pypi.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# PyPI API
+# PyPI API **(FREE)**
This is the API documentation for [PyPI Packages](../../user/packages/pypi_repository/index.md).
diff --git a/doc/api/packages/rubygems.md b/doc/api/packages/rubygems.md
index 426548d5ed2..10dcaafda42 100644
--- a/doc/api/packages/rubygems.md
+++ b/doc/api/packages/rubygems.md
@@ -4,7 +4,7 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Ruby gems API
+# Ruby gems API **(FREE SELF)**
This is the API documentation for [Ruby gems](../../user/packages/rubygems_registry/index.md).
diff --git a/doc/api/templates/dockerfiles.md b/doc/api/templates/dockerfiles.md
index 2d7e926561f..5f862201571 100644
--- a/doc/api/templates/dockerfiles.md
+++ b/doc/api/templates/dockerfiles.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# Dockerfiles API
+# Dockerfiles API **(FREE)**
GitLab provides an API endpoint for instance-level Dockerfile templates.
Default templates are defined at
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index f1bf8120574..71b791de16a 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# .gitignore API
+# .gitignore API **(FREE)**
In GitLab, the `/gitignores` endpoint returns a list of Git `.gitignore` templates. For more information,
see the [Git documentation for `.gitignore`](https://git-scm.com/docs/gitignore).
diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md
index 82abe598cf6..abed008218c 100644
--- a/doc/api/templates/gitlab_ci_ymls.md
+++ b/doc/api/templates/gitlab_ci_ymls.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# GitLab CI YMLs API
+# GitLab CI YMLs API **(FREE)**
In GitLab, there is an API endpoint available to work with GitLab CI/CD YMLs. For more
information on CI/CD pipeline configuration in GitLab, see the
diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md
index e7eaa6eb3e0..adba4f75255 100644
--- a/doc/api/templates/licenses.md
+++ b/doc/api/templates/licenses.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference
---
-# Licenses API
+# Licenses API **(FREE)**
In GitLab, there is an API endpoint available for working with various open
source license templates. For more information on the terms of various
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 0896357cc73..a1123b6291b 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -108,6 +108,20 @@ module API
present paginate(groups), options
end
+ def present_groups_with_pagination_strategies(params, groups)
+ return present_groups(params, groups) if current_user.present? || Feature.disabled?(:keyset_pagination_for_groups_api)
+
+ options = {
+ with: Entities::Group,
+ current_user: nil,
+ statistics: false
+ }
+
+ groups, options = with_custom_attributes(groups, options)
+
+ present paginate_with_strategies(groups), options
+ end
+
def delete_group(group)
destroy_conditionally!(group) do |group|
::Groups::DestroyService.new(group, current_user).async_execute
@@ -168,7 +182,7 @@ module API
end
get do
groups = find_groups(declared_params(include_missing: false), params[:id])
- present_groups params, groups
+ present_groups_with_pagination_strategies params, groups
end
desc 'Create a group. Available only for users who can create groups.' do
diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb
index 61cff37e4ab..8c2186768ea 100644
--- a/lib/api/helpers/pagination_strategies.rb
+++ b/lib/api/helpers/pagination_strategies.rb
@@ -3,10 +3,16 @@
module API
module Helpers
module PaginationStrategies
- def paginate_with_strategies(relation, request_scope)
+ def paginate_with_strategies(relation, request_scope = nil)
paginator = paginator(relation, request_scope)
- yield(paginator.paginate(relation)).tap do |records, _|
+ result = if block_given?
+ yield(paginator.paginate(relation))
+ else
+ paginator.paginate(relation)
+ end
+
+ result.tap do |records, _|
paginator.finalize(records)
end
end
@@ -20,17 +26,31 @@ module API
private
def keyset_paginator(relation)
- request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
- unless Gitlab::Pagination::Keyset.available?(request_context, relation)
+ if cursor_based_keyset_pagination_supported?(relation)
+ request_context_class = Gitlab::Pagination::Keyset::CursorBasedRequestContext
+ paginator_class = Gitlab::Pagination::Keyset::CursorPager
+ availability_checker = Gitlab::Pagination::CursorBasedKeyset
+ else
+ request_context_class = Gitlab::Pagination::Keyset::RequestContext
+ paginator_class = Gitlab::Pagination::Keyset::Pager
+ availability_checker = Gitlab::Pagination::Keyset
+ end
+
+ request_context = request_context_class.new(self)
+
+ unless availability_checker.available?(request_context, relation)
return error!('Keyset pagination is not yet available for this type of request', 405)
end
- Gitlab::Pagination::Keyset::Pager.new(request_context)
+ paginator_class.new(request_context)
end
def offset_paginator(relation, request_scope)
offset_limit = limit_for_scope(request_scope)
- if Gitlab::Pagination::Keyset.available_for_type?(relation) && offset_limit_exceeded?(offset_limit)
+ if (Gitlab::Pagination::Keyset.available_for_type?(relation) ||
+ cursor_based_keyset_pagination_supported?(relation)) &&
+ offset_limit_exceeded?(offset_limit)
+
return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
"for requests that return objects of type #{relation.klass}. " \
"Remaining records can be retrieved using keyset pagination.", 405)
@@ -39,6 +59,10 @@ module API
Gitlab::Pagination::OffsetPagination.new(self)
end
+ def cursor_based_keyset_pagination_supported?(relation)
+ Gitlab::Pagination::CursorBasedKeyset.available_for_type?(relation)
+ end
+
def keyset_pagination_enabled?
params[:pagination] == 'keyset'
end
diff --git a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
index acb4842db31..518a812b406 100644
--- a/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
+++ b/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
@@ -32,12 +32,10 @@ module Gitlab
def set_data_consistency_locations!(job)
# Once we add support for multiple databases to our load balancer, we would use something like this:
# job['wal_locations'] = Gitlab::Database::DATABASES.transform_values do |connection|
- # connection.load_balancer.primary_write_location.
+ # connection.load_balancer.primary_write_location
# end
#
- # TODO: Replace hardcoded database config name :main when we merge unification strategy
- # https://gitlab.com/gitlab-org/gitlab/-/issues/336566
- job['wal_locations'] = { main: wal_location } if wal_location
+ job['wal_locations'] = { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => wal_location } if wal_location
end
def wal_location
diff --git a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
index 2555f693a5d..f7fda14b215 100644
--- a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
+++ b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
@@ -66,7 +66,7 @@ module Gitlab
def legacy_wal_location(job)
wal_location = job['database_write_location'] || job['database_replica_location']
- { main: wal_location } if wal_location
+ { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => wal_location } if wal_location
end
def load_balancing_available?(worker_class)
diff --git a/lib/gitlab/database_importers/work_items/base_type_importer.rb b/lib/gitlab/database_importers/work_items/base_type_importer.rb
new file mode 100644
index 00000000000..c5acdb41de5
--- /dev/null
+++ b/lib/gitlab/database_importers/work_items/base_type_importer.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module WorkItems
+ module BaseTypeImporter
+ def self.import
+ WorkItem::Type::BASE_TYPES.each do |type, attributes|
+ WorkItem::Type.create!(base_type: type, **attributes.slice(:name, :icon_name))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/cursor_based_keyset.rb b/lib/gitlab/pagination/cursor_based_keyset.rb
new file mode 100644
index 00000000000..f19cdf06d9a
--- /dev/null
+++ b/lib/gitlab/pagination/cursor_based_keyset.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ module CursorBasedKeyset
+ SUPPORTED_ORDERING = {
+ Group => { name: :asc }
+ }.freeze
+
+ def self.available_for_type?(relation)
+ SUPPORTED_ORDERING.key?(relation.klass)
+ end
+
+ def self.available?(cursor_based_request_context, relation)
+ available_for_type?(relation) &&
+ order_satisfied?(relation, cursor_based_request_context)
+ end
+
+ def self.order_satisfied?(relation, cursor_based_request_context)
+ order_by_from_request = cursor_based_request_context.order_by
+
+ SUPPORTED_ORDERING[relation.klass] == order_by_from_request
+ end
+ private_class_method :order_satisfied?
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/keyset/cursor_based_request_context.rb b/lib/gitlab/pagination/keyset/cursor_based_request_context.rb
index d9c118ceef5..18390f5b59d 100644
--- a/lib/gitlab/pagination/keyset/cursor_based_request_context.rb
+++ b/lib/gitlab/pagination/keyset/cursor_based_request_context.rb
@@ -4,11 +4,12 @@ module Gitlab
module Pagination
module Keyset
class CursorBasedRequestContext
- attr_reader :request
- delegate :params, :header, to: :request
+ DEFAULT_SORT_DIRECTION = :desc
+ attr_reader :request_context
+ delegate :params, to: :request_context
- def initialize(request)
- @request = request
+ def initialize(request_context)
+ @request_context = request_context
end
def per_page
@@ -21,9 +22,13 @@ module Gitlab
def apply_headers(cursor_for_next_page)
Gitlab::Pagination::Keyset::HeaderBuilder
- .new(self)
+ .new(request_context)
.add_next_page_header({ cursor: cursor_for_next_page })
end
+
+ def order_by
+ { params[:order_by].to_sym => params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION }
+ end
end
end
end
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 4fc62038d4f..797d9188f81 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -159,7 +159,7 @@ function rspec_paralellized_job() {
local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
- if [[ -n $MINIMAL_RSPEC_ENABLED ]]; then
+ if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter "tmp/matching_tests.txt"
else
tooling/bin/parallel_rspec --rspec_args "${rspec_args}"
diff --git a/spec/db/development/create_base_work_item_types_spec.rb b/spec/db/development/create_base_work_item_types_spec.rb
new file mode 100644
index 00000000000..914b84d8668
--- /dev/null
+++ b/spec/db/development/create_base_work_item_types_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create base work item types in development' do
+ subject { load Rails.root.join('db', 'fixtures', 'development', '001_create_base_work_item_types.rb') }
+
+ it_behaves_like 'work item base types importer'
+end
diff --git a/spec/db/production/create_base_work_item_types_spec.rb b/spec/db/production/create_base_work_item_types_spec.rb
new file mode 100644
index 00000000000..81d80104bb4
--- /dev/null
+++ b/spec/db/production/create_base_work_item_types_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create base work item types in production' do
+ subject { load Rails.root.join('db', 'fixtures', 'production', '003_create_base_work_item_types.rb') }
+
+ it_behaves_like 'work item base types importer'
+end
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index ac606f1b182..ad67afdce75 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
import createFlash from '~/flash';
+import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
@@ -218,7 +219,7 @@ describe('deploy freeze store actions', () => {
});
it('should show flash error and set error in state on delete failure', () => {
- const errorSpy = jest.spyOn(console, 'error').mockImplementation();
+ jest.spyOn(logger, 'logError').mockImplementation();
const error = new Error();
Api.deleteFreezePeriod.mockRejectedValue(error);
@@ -234,7 +235,7 @@ describe('deploy freeze store actions', () => {
() => {
expect(createFlash).toHaveBeenCalled();
- expect(errorSpy).toHaveBeenCalledWith('[gitlab] Unable to delete deploy freeze:', error);
+ expect(logger.logError).toHaveBeenCalledWith('Unable to delete deploy freeze', error);
},
);
});
diff --git a/spec/frontend/lib/logger/index_spec.js b/spec/frontend/lib/logger/index_spec.js
new file mode 100644
index 00000000000..9382fafe4de
--- /dev/null
+++ b/spec/frontend/lib/logger/index_spec.js
@@ -0,0 +1,23 @@
+import { logError, LOG_PREFIX } from '~/lib/logger';
+
+describe('~/lib/logger', () => {
+ let consoleErrorSpy;
+
+ beforeEach(() => {
+ consoleErrorSpy = jest.spyOn(console, 'error');
+ consoleErrorSpy.mockImplementation();
+ });
+
+ describe('logError', () => {
+ it('sends given message to console.error', () => {
+ const message = 'Lorem ipsum dolar sit amit';
+ const error = new Error('lorem ipsum');
+
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
+
+ logError(message, error);
+
+ expect(consoleErrorSpy).toHaveBeenCalledWith(LOG_PREFIX, `${message}\n`, error);
+ });
+ });
+});
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index d752496d809..719c14ee188 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:issue4) { create(:issue) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
+ let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue1) }
specify do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
@@ -200,6 +201,27 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
+ context 'filtering by reaction emoji' do
+ let_it_be(:downvoted_issue) { create(:issue, project: project) }
+ let_it_be(:downvote_award) { create(:award_emoji, :downvote, user: current_user, awardable: downvoted_issue) }
+
+ it 'filters by reaction emoji' do
+ expect(resolve_issues(my_reaction_emoji: upvote_award.name)).to contain_exactly(issue1)
+ end
+
+ it 'filters by reaction emoji wildcard "none"' do
+ expect(resolve_issues(my_reaction_emoji: 'none')).to contain_exactly(issue2)
+ end
+
+ it 'filters by reaction emoji wildcard "any"' do
+ expect(resolve_issues(my_reaction_emoji: 'any')).to contain_exactly(issue1, downvoted_issue)
+ end
+
+ it 'filters by negated reaction emoji' do
+ expect(resolve_issues(not: { my_reaction_emoji: downvote_award.name })).to contain_exactly(issue1, issue2)
+ end
+ end
+
context 'when searching issues' do
it 'returns correct issues' do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
index 23a496d85f8..f683ade978a 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
end
it 'passes database_replica_location' do
- expected_location = { main: location }
+ expected_location = { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location }
expect(load_balancer).to receive_message_chain(:host, "database_replica_location").and_return(location)
@@ -103,7 +103,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
end
it 'passes primary write location', :aggregate_failures do
- expected_location = { main: location }
+ expected_location = { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location }
expect(load_balancer).to receive(:primary_write_location).and_return(location)
@@ -116,28 +116,43 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
end
end
- shared_examples_for 'database location was already provided' do
- shared_examples_for 'does not set database location again' do |use_primary|
- before do
- allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(use_primary)
- end
+ context 'when worker cannot be constantized' do
+ let(:worker_class) { 'ActionMailer::MailDeliveryJob' }
+ let(:expected_consistency) { :always }
- it 'does not set database locations again' do
- run_middleware
+ include_examples 'does not pass database locations'
+ end
- expect(job['wal_locations']).to eq({ main: old_location })
- end
- end
+ context 'when worker class does not include ApplicationWorker' do
+ let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
+ let(:expected_consistency) { :always }
+
+ include_examples 'does not pass database locations'
+ end
+ context 'database wal location was already provided' do
let(:old_location) { '0/D525E3A8' }
let(:new_location) { 'AB/12345' }
- let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => { main: old_location } } }
+ let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => old_location } }
+ let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations } }
before do
allow(load_balancer).to receive(:primary_write_location).and_return(new_location)
allow(load_balancer).to receive(:database_replica_location).and_return(new_location)
end
+ shared_examples_for 'does not set database location again' do |use_primary|
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(use_primary)
+ end
+
+ it 'does not set database locations again' do
+ run_middleware
+
+ expect(job['wal_locations']).to eq(wal_locations)
+ end
+ end
+
context "when write was performed" do
include_examples 'does not set database location again', true
end
@@ -147,24 +162,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
end
end
- context 'when worker cannot be constantized' do
- let(:worker_class) { 'ActionMailer::MailDeliveryJob' }
- let(:expected_consistency) { :always }
-
- include_examples 'does not pass database locations'
- end
-
- context 'when worker class does not include ApplicationWorker' do
- let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
- let(:expected_consistency) { :always }
-
- include_examples 'does not pass database locations'
- end
-
- context 'database wal location was already provided' do
- include_examples 'database location was already provided'
- end
-
context 'when worker data consistency is :always' do
include_context 'data consistency worker class', :always, :load_balancing_for_test_data_consistency_worker
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
index fcb1d52fbaf..1fe348bbf2e 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
@@ -63,10 +63,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
end
shared_examples_for 'replica is up to date' do |expected_strategy|
- let(:wal_locations) { { main: '0/D525E3A8' } }
+ let(:location) {'0/D525E3A8' }
+ let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } }
it 'does not stick to the primary', :aggregate_failures do
- expect(load_balancer).to receive(:select_up_to_date_host).with(wal_locations[:main]).and_return(true)
+ expect(load_balancer).to receive(:select_up_to_date_host).with(location).and_return(true)
run_middleware do
expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).not_to be_truthy
@@ -91,7 +92,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'wal_locations' => wal_locations } }
before do
- allow(load_balancer).to receive(:select_up_to_date_host).with(wal_locations[:main]).and_return(true)
+ allow(load_balancer).to receive(:select_up_to_date_host).with(location).and_return(true)
end
it_behaves_like 'replica is up to date', 'replica'
diff --git a/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
new file mode 100644
index 00000000000..8c3d372cc55
--- /dev/null
+++ b/spec/lib/gitlab/database_importers/work_items/base_type_importer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter do
+ subject { described_class.import }
+
+ it_behaves_like 'work item base types importer'
+end
diff --git a/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
new file mode 100644
index 00000000000..ac2695977c4
--- /dev/null
+++ b/spec/lib/gitlab/pagination/cursor_based_keyset_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pagination::CursorBasedKeyset do
+ subject { described_class }
+
+ describe '.available_for_type?' do
+ it 'returns true for Group' do
+ expect(subject.available_for_type?(Group.all)).to be_truthy
+ end
+
+ it 'return false for other types of relations' do
+ expect(subject.available_for_type?(User.all)).to be_falsey
+ end
+ end
+
+ describe '.available?' do
+ let(:request_context) { double('request_context', params: { order_by: order_by, sort: sort }) }
+ let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request_context) }
+
+ context 'with order-by name asc' do
+ let(:order_by) { :name }
+ let(:sort) { :asc }
+
+ it 'returns true for Group' do
+ expect(subject.available?(cursor_based_request_context, Group.all)).to be_truthy
+ end
+
+ it 'return false for other types of relations' do
+ expect(subject.available?(cursor_based_request_context, User.all)).to be_falsey
+ end
+ end
+
+ context 'with other order-by columns' do
+ let(:order_by) { :path }
+ let(:sort) { :asc }
+
+ it 'returns false for Group' do
+ expect(subject.available?(cursor_based_request_context, Group.all)).to be_falsey
+ end
+
+ it 'return false for other types of relations' do
+ expect(subject.available?(cursor_based_request_context, User.all)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb b/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb
index 4998abd2186..79de6f230ec 100644
--- a/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/cursor_based_request_context_spec.rb
@@ -3,32 +3,40 @@
require 'spec_helper'
RSpec.describe Gitlab::Pagination::Keyset::CursorBasedRequestContext do
- let(:params) { { per_page: 2, cursor: 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==' } }
- let(:request) { double('request', params: params) }
+ let(:params) { { per_page: 2, cursor: 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==', order_by: :name, sort: :asc } }
+ let(:request) { double('request', url: 'http://localhost') }
+ let(:request_context) { double('request_context', header: nil, params: params, request: request) }
describe '#per_page' do
- subject(:per_page) { described_class.new(request).per_page }
+ subject(:per_page) { described_class.new(request_context).per_page }
it { is_expected.to eq 2 }
end
describe '#cursor' do
- subject(:cursor) { described_class.new(request).cursor }
+ subject(:cursor) { described_class.new(request_context).cursor }
it { is_expected.to eq 'eyJuYW1lIjoiR2l0TGFiIEluc3RhbmNlIiwiaWQiOiI1MiIsIl9rZCI6Im4ifQ==' }
end
+ describe '#order_by' do
+ subject(:order_by) { described_class.new(request_context).order_by }
+
+ it { is_expected.to eq({ name: :asc }) }
+ end
+
describe '#apply_headers' do
- let(:request) { double('request', url: "http://#{Gitlab.config.gitlab.host}/api/v4/projects?per_page=3", params: params) }
+ let(:request) { double('request', url: "http://#{Gitlab.config.gitlab.host}/api/v4/projects?per_page=3") }
let(:params) { { per_page: 3 } }
+ let(:request_context) { double('request_context', header: nil, params: params, request: request) }
let(:cursor_for_next_page) { 'eyJuYW1lIjoiSDVicCIsImlkIjoiMjgiLCJfa2QiOiJuIn0=' }
- subject(:apply_headers) { described_class.new(request).apply_headers(cursor_for_next_page) }
+ subject(:apply_headers) { described_class.new(request_context).apply_headers(cursor_for_next_page) }
it 'sets Link header with same host/path as the original request' do
- orig_uri = URI.parse(request.url)
+ orig_uri = URI.parse(request_context.request.url)
- expect(request).to receive(:header).once do |name, header|
+ expect(request_context).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
uri = URI.parse(first_link)
@@ -42,9 +50,9 @@ RSpec.describe Gitlab::Pagination::Keyset::CursorBasedRequestContext do
end
it 'sets Link header with a cursor to the next page' do
- orig_uri = URI.parse(request.url)
+ orig_uri = URI.parse(request_context.request.url)
- expect(request).to receive(:header).once do |name, header|
+ expect(request_context).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
query = CGI.parse(URI.parse(first_link).query)
diff --git a/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb b/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb
index 5ba381834ea..783e728b34c 100644
--- a/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/cursor_pager_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Gitlab::Pagination::Keyset::CursorPager do
let(:relation) { Group.all.order(:name, :id) }
let(:per_page) { 3 }
let(:params) { { cursor: nil, per_page: per_page } }
- let(:request) { double('request', params: params) }
- let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request) }
+ let(:request_context) { double('request_context', params: params) }
+ let(:cursor_based_request_context) { Gitlab::Pagination::Keyset::CursorBasedRequestContext.new(request_context) }
before_all do
create_list(:group, 7)
@@ -33,7 +33,7 @@ RSpec.describe Gitlab::Pagination::Keyset::CursorPager do
it 'passes information about next page to request' do
cursor_for_next_page = relation.keyset_paginate(**params).cursor_for_next_page
- expect_next_instance_of(Gitlab::Pagination::Keyset::HeaderBuilder, cursor_based_request_context) do |builder|
+ expect_next_instance_of(Gitlab::Pagination::Keyset::HeaderBuilder, request_context) do |builder|
expect(builder).to receive(:add_next_page_header).with({ cursor: cursor_for_next_page })
end
diff --git a/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
new file mode 100644
index 00000000000..b9cc9de88cc
--- /dev/null
+++ b/spec/migrations/20210831203408_upsert_base_work_item_types_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('upsert_base_work_item_types')
+
+RSpec.describe UpsertBaseWorkItemTypes, :migration do
+ let!(:work_item_types) { table(:work_item_types) }
+
+ context 'when no default types exist' do
+ it 'creates default data' do
+ expect(work_item_types.count).to eq(0)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ # Depending on whether the migration has been run before,
+ # the size could be 4, or 0, so we don't set any expectations
+ # as we don't delete base types on migration reverse
+ }
+
+ migration.after -> {
+ expect(work_item_types.count).to eq(4)
+ expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values)
+ }
+ end
+ end
+ end
+
+ context 'when default types already exist' do
+ before do
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import
+ end
+
+ it 'does not create default types again' do
+ expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values)
+ }
+
+ migration.after -> {
+ expect(work_item_types.count).to eq(4)
+ expect(work_item_types.all.pluck(:base_type)).to match_array(WorkItem::Type.base_types.values)
+ }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb
index 04fcaab4c2d..4e8d49585d0 100644
--- a/spec/models/ci/pipeline_variable_spec.rb
+++ b/spec/models/ci/pipeline_variable_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::PipelineVariable do
it_behaves_like "CI variable"
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:pipeline_id) }
+ it { is_expected.to validate_presence_of(:key) }
describe '#hook_attrs' do
let(:variable) { create(:ci_pipeline_variable, key: 'foo', value: 'bar') }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index ff0d7ecceb5..c6b4d82bf15 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -61,6 +61,34 @@ RSpec.describe 'getting an issue list for a project' do
end
end
+ context 'filtering by my_reaction_emoji' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) }
+
+ let(:issue_a_gid) { issue_a.to_global_id.to_s }
+ let(:issue_b_gid) { issue_b.to_global_id.to_s }
+
+ where(:value, :gids) do
+ 'thumbsup' | lazy { [issue_a_gid] }
+ 'ANY' | lazy { [issue_a_gid] }
+ 'any' | lazy { [issue_a_gid] }
+ 'AnY' | lazy { [issue_a_gid] }
+ 'NONE' | lazy { [issue_b_gid] }
+ 'thumbsdown' | lazy { [] }
+ end
+
+ with_them do
+ let(:issue_filter_params) { { my_reaction_emoji: value } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_dig_at(issues_data, :node, :id)).to eq(gids)
+ end
+ end
+ end
+
context 'when limiting the number of results' do
let(:query) do
<<~GQL
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 30df47ccc41..38abedde7da 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -158,6 +158,127 @@ RSpec.describe API::Groups do
end
end
+ context 'pagination strategies' do
+ let_it_be(:group_1) { create(:group, name: '1_group') }
+ let_it_be(:group_2) { create(:group, name: '2_group') }
+
+ context 'when the user is anonymous' do
+ context 'offset pagination' do
+ context 'on making requests beyond the allowed offset pagination threshold' do
+ it 'returns error and suggests to use keyset pagination' do
+ get api('/groups'), params: { page: 3000, per_page: 25 }
+
+ expect(response).to have_gitlab_http_status(:method_not_allowed)
+ expect(json_response['error']).to eq(
+ 'Offset pagination has a maximum allowed offset of 50000 for requests that return objects of type Group. '\
+ 'Remaining records can be retrieved using keyset pagination.'
+ )
+ end
+
+ context 'when the feature flag `keyset_pagination_for_groups_api` is disabled' do
+ before do
+ stub_feature_flags(keyset_pagination_for_groups_api: false)
+ end
+
+ it 'returns successful response' do
+ get api('/groups'), params: { page: 3000, per_page: 25 }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'on making requests below the allowed offset pagination threshold' do
+ it 'paginates the records' do
+ get api('/groups'), params: { page: 1, per_page: 1 }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ records = json_response
+ expect(records.size).to eq(1)
+ expect(records.first['id']).to eq(group_1.id)
+
+ # next page
+
+ get api('/groups'), params: { page: 2, per_page: 1 }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ records = Gitlab::Json.parse(response.body)
+ expect(records.size).to eq(1)
+ expect(records.first['id']).to eq(group_2.id)
+ end
+ end
+ end
+
+ context 'keyset pagination' do
+ def pagination_links(response)
+ link = response.headers['LINK']
+ return unless link
+
+ link.split(',').map do |link|
+ match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
+ break nil unless match
+
+ { url: match[:url], rel: match[:rel] }
+ end.compact
+ end
+
+ def params_for_next_page(response)
+ next_url = pagination_links(response).find { |link| link[:rel] == 'next' }[:url]
+ Rack::Utils.parse_query(URI.parse(next_url).query)
+ end
+
+ context 'on making requests with supported ordering structure' do
+ it 'paginates the records correctly' do
+ # first page
+ get api('/groups'), params: { pagination: 'keyset', per_page: 1 }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ records = json_response
+ expect(records.size).to eq(1)
+ expect(records.first['id']).to eq(group_1.id)
+
+ params_for_next_page = params_for_next_page(response)
+ expect(params_for_next_page).to include('cursor')
+
+ get api('/groups'), params: params_for_next_page
+
+ expect(response).to have_gitlab_http_status(:ok)
+ records = Gitlab::Json.parse(response.body)
+ expect(records.size).to eq(1)
+ expect(records.first['id']).to eq(group_2.id)
+ end
+
+ context 'when the feature flag `keyset_pagination_for_groups_api` is disabled' do
+ before do
+ stub_feature_flags(keyset_pagination_for_groups_api: false)
+ end
+
+ it 'ignores the keyset pagination params and performs offset pagination' do
+ get api('/groups'), params: { pagination: 'keyset', per_page: 1 }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ records = json_response
+ expect(records.size).to eq(1)
+ expect(records.first['id']).to eq(group_1.id)
+
+ params_for_next_page = params_for_next_page(response)
+ expect(params_for_next_page).not_to include('cursor')
+ end
+ end
+ end
+
+ context 'on making requests with unsupported ordering structure' do
+ it 'returns error' do
+ get api('/groups'), params: { pagination: 'keyset', per_page: 1, order_by: 'path', sort: 'desc' }
+
+ expect(response).to have_gitlab_http_status(:method_not_allowed)
+ expect(json_response['error']).to eq('Keyset pagination is not yet available for this type of request')
+ end
+ end
+ end
+ end
+ end
+
context "when authenticated as admin" do
it "admin: returns an array of all groups" do
get api("/groups", admin)
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
new file mode 100644
index 00000000000..2c02b76d49b
--- /dev/null
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work item base types importer' do
+ it 'creates all base work item types' do
+ expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count)
+ end
+end
diff --git a/tooling/bin/find_tests b/tooling/bin/find_tests
index a82b6bb5d46..97fadf406fe 100755
--- a/tooling/bin/find_tests
+++ b/tooling/bin/find_tests
@@ -22,7 +22,10 @@ changed_files = mr_changes.changes.map { |change| change['new_path'] }
tff = TestFileFinder::FileFinder.new(paths: changed_files).tap do |file_finder|
file_finder.use TestFileFinder::MappingStrategies::PatternMatching.load('tests.yml')
- file_finder.use TestFileFinder::MappingStrategies::DirectMatching.load_json(ENV['RSPEC_TESTS_MAPPING_PATH'])
+
+ if ENV['RSPEC_TESTS_MAPPING_ENABLED']
+ file_finder.use TestFileFinder::MappingStrategies::DirectMatching.load_json(ENV['RSPEC_TESTS_MAPPING_PATH'])
+ end
end
File.write(output_file, tff.test_files.uniq.join(' '))