From 23835e8cac4d6ff976e2df8670ffae2a35cb92f3 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 17 Dec 2024 18:31:22 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/gitlab-gems.gitlab-ci.yml | 3 + GITALY_SERVER_VERSION | 2 +- Gemfile | 3 + Gemfile.lock | 7 + Gemfile.next.checksum | 6 +- Gemfile.next.lock | 13 +- .../graphql_shared/issuable_client.js | 23 ++ .../issues/create_merge_request_dropdown.js | 4 +- .../components/projects_list/formatter.js | 1 + .../project_list_item_description.vue | 26 +- .../projects_list/projects_list_item.vue | 295 +++++++----------- .../components/resource_lists/list_item.vue | 46 ++- .../resource_lists/list_item_description.vue | 43 +++ .../resource_lists/list_item_stat.vue | 18 +- .../components/work_item_description.vue | 99 +++++- ...work_item_description_template_listbox.vue | 116 +++++++ ...rk_item_description_template.query.graphql | 12 + ...m_description_templates_list.query.graphql | 11 + .../stylesheets/page_bundles/projects.scss | 14 - .../autocomplete_sources/expires_in.rb | 15 +- app/controllers/projects/issues_controller.rb | 2 +- app/finders/todos_finder.rb | 13 + app/graphql/resolvers/todos_resolver.rb | 5 + app/helpers/ci/runners_helper.rb | 2 +- ...ntainer_registry_authentication_service.rb | 2 +- app/services/award_emojis/copy_service.rb | 7 +- app/services/todos/snoozing_service.rb | 2 +- .../user_detail_onboarding_status.json | 13 + .../cache_autocomplete_sources_commands.yml | 8 - .../cache_autocomplete_sources_labels.yml | 8 - .../cache_autocomplete_sources_members.yml | 8 - .../wip/cache_autocomplete_sources_issues.yml | 9 - config/feature_flags/wip/todos_snoozing.yml | 9 + .../dedicated/configure_instance.md | 5 +- doc/administration/moderate_users.md | 34 +- doc/api/branches.md | 9 +- doc/api/graphql/reference/index.md | 23 ++ doc/api/group_protected_branches.md | 64 ++-- doc/api/group_ssh_certificates.md | 3 +- doc/api/project_aliases.md | 18 +- doc/api/project_pull_mirroring.md | 23 +- doc/api/project_security_settings.md | 2 +- doc/api/project_snippets.md | 3 +- doc/api/protected_branches.md | 68 ++-- doc/api/protected_tags.md | 2 +- doc/api/repositories.md | 9 +- doc/api/snippets.md | 3 +- doc/api/user_moderation.md | 144 +++++---- .../dependency_list/index.md | 7 +- .../vulnerability_management_policy.md | 1 - doc/user/gitlab_duo/index.md | 284 ++++------------- doc/user/group/saml_sso/group_sync.md | 3 +- doc/user/project/codeowners/index.md | 14 +- gems/gitlab-active-context/.gitignore | 11 + gems/gitlab-active-context/.gitlab-ci.yml | 4 + gems/gitlab-active-context/.rspec | 3 + gems/gitlab-active-context/.rubocop.yml | 5 + gems/gitlab-active-context/Gemfile | 16 + gems/gitlab-active-context/Gemfile.lock | 209 +++++++++++++ gems/gitlab-active-context/README.md | 42 +++ gems/gitlab-active-context/Rakefile | 12 + gems/gitlab-active-context/bin/console | 11 + gems/gitlab-active-context/bin/setup | 8 + .../gitlab-active-context.gemspec | 30 ++ .../lib/active_context.rb | 11 + .../lib/active_context/config.rb | 39 +++ .../lib/active_context/version.rb | 5 + .../spec/active_context_spec.rb | 31 ++ .../spec/lib/active_context/config_spec.rb | 94 ++++++ .../gitlab-active-context/spec/spec_helper.rb | 16 + locale/gitlab.pot | 39 +++ .../autocomplete_sources/expires_in_spec.rb | 13 - spec/finders/todos_finder_spec.rb | 30 +- .../groups_list/groups_list_item_spec.js | 6 +- .../projects_list/projects_list_item_spec.js | 90 +++--- .../list_item_description_spec.js | 25 ++ .../resource_lists/list_item_spec.js | 110 +++++-- .../resource_lists/list_item_stat_spec.js | 17 +- .../components/work_item_description_spec.js | 102 +++++- ..._item_description_template_listbox_spec.js | 163 ++++++++++ spec/graphql/resolvers/todos_resolver_spec.rb | 26 ++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/user_detail_spec.rb | 32 +- .../award_emojis/copy_service_spec.rb | 10 +- spec/services/todos/snoozing_service_spec.rb | 4 +- ...r_registry_auth_service_shared_examples.rb | 81 ++--- 86 files changed, 2014 insertions(+), 846 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/resource_lists/list_item_description.vue create mode 100644 app/assets/javascripts/work_items/components/work_item_description_template_listbox.vue create mode 100644 app/assets/javascripts/work_items/graphql/work_item_description_template.query.graphql create mode 100644 app/assets/javascripts/work_items/graphql/work_item_description_templates_list.query.graphql delete mode 100644 config/feature_flags/development/cache_autocomplete_sources_commands.yml delete mode 100644 config/feature_flags/development/cache_autocomplete_sources_labels.yml delete mode 100644 config/feature_flags/development/cache_autocomplete_sources_members.yml delete mode 100644 config/feature_flags/wip/cache_autocomplete_sources_issues.yml create mode 100644 config/feature_flags/wip/todos_snoozing.yml create mode 100644 gems/gitlab-active-context/.gitignore create mode 100644 gems/gitlab-active-context/.gitlab-ci.yml create mode 100644 gems/gitlab-active-context/.rspec create mode 100644 gems/gitlab-active-context/.rubocop.yml create mode 100644 gems/gitlab-active-context/Gemfile create mode 100644 gems/gitlab-active-context/Gemfile.lock create mode 100644 gems/gitlab-active-context/README.md create mode 100644 gems/gitlab-active-context/Rakefile create mode 100755 gems/gitlab-active-context/bin/console create mode 100755 gems/gitlab-active-context/bin/setup create mode 100644 gems/gitlab-active-context/gitlab-active-context.gemspec create mode 100644 gems/gitlab-active-context/lib/active_context.rb create mode 100644 gems/gitlab-active-context/lib/active_context/config.rb create mode 100644 gems/gitlab-active-context/lib/active_context/version.rb create mode 100644 gems/gitlab-active-context/spec/active_context_spec.rb create mode 100644 gems/gitlab-active-context/spec/lib/active_context/config_spec.rb create mode 100644 gems/gitlab-active-context/spec/spec_helper.rb create mode 100644 spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js create mode 100644 spec/frontend/work_items/components/work_item_description_template_listbox_spec.js diff --git a/.gitlab/ci/gitlab-gems.gitlab-ci.yml b/.gitlab/ci/gitlab-gems.gitlab-ci.yml index 6235390453b..f5b0b9fbe26 100644 --- a/.gitlab/ci/gitlab-gems.gitlab-ci.yml +++ b/.gitlab/ci/gitlab-gems.gitlab-ci.yml @@ -47,3 +47,6 @@ include: - local: .gitlab/ci/templates/gem.gitlab-ci.yml inputs: gem_name: "openbao_client" + - local: .gitlab/ci/templates/gem.gitlab-ci.yml + inputs: + gem_name: "gitlab-active-context" diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index bb871c20dcc..ee4a478a309 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -b621ac4ec3d3b032489d4ac65df1ffa5752a5565 +2035810e4e65a367b3483c75e40f1ce4eb726c48 diff --git a/Gemfile b/Gemfile index 6220b2ece5c..e775f3e6360 100644 --- a/Gemfile +++ b/Gemfile @@ -242,6 +242,9 @@ gem 'faraday_middleware-aws-sigv4', '~> 1.0.1', feature_category: :global_search # Used with Elasticsearch to support http keep-alive connections gem 'typhoeus', '~> 1.4.0', feature_category: :global_search +gem 'gitlab-active-context', path: 'gems/gitlab-active-context', require: 'active_context', + feature_category: :global_search + # Markdown and HTML processing gem 'html-pipeline', '~> 2.14.3', feature_category: :markdown gem 'deckar01-task_list', '2.3.4', feature_category: :markdown diff --git a/Gemfile.lock b/Gemfile.lock index 1f664bb6a70..ba9c793d568 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,6 +23,12 @@ PATH error_tracking_open_api (1.0.0) typhoeus (~> 1.0, >= 1.0.1) +PATH + remote: gems/gitlab-active-context + specs: + gitlab-active-context (0.0.1) + zeitwerk + PATH remote: gems/gitlab-backup-cli specs: @@ -2066,6 +2072,7 @@ DEPENDENCIES gettext (~> 3.4, >= 3.4.9) gettext_i18n_rails (~> 1.13.0) gitaly (~> 17.5.0.pre.rc1) + gitlab-active-context! gitlab-backup-cli! gitlab-chronic (~> 0.10.5) gitlab-cloud-connector (~> 0.2.5) diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index a8f7e550bd0..f74c00926e4 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -567,7 +567,7 @@ {"name":"rbs","version":"3.6.1","platform":"ruby","checksum":"ed7273d018556844583d1785ac54194e67eec594d68e317d57fa90ad035532c0"}, {"name":"rbtrace","version":"0.5.1","platform":"ruby","checksum":"e8cba64d462bfb8ba102d7be2ecaacc789247d52ac587d8003549d909cb9c5dc"}, {"name":"rchardet","version":"1.8.0","platform":"ruby","checksum":"693acd5253d5ade81a51940697955f6dd4bb2f0d245bda76a8e23deec70a52c7"}, -{"name":"rdoc","version":"6.8.1","platform":"ruby","checksum":"0128002d1bfc4892bdd780940841e4ca41275f63781fd832d11bc8ba4461462c"}, +{"name":"rdoc","version":"6.9.1","platform":"ruby","checksum":"3344bf498a46b701aba70ccdd5cdfa8be37e68493984c1bf8c579f06c3442c9f"}, {"name":"re2","version":"2.7.0","platform":"aarch64-linux","checksum":"778921298b6e8aba26a6230dd298c9b361b92e45024f81fa6aee788060fa307c"}, {"name":"re2","version":"2.7.0","platform":"arm-linux","checksum":"d328b5286d83ae265e13b855da8e348a976f80f91b748045b52073a570577954"}, {"name":"re2","version":"2.7.0","platform":"arm64-darwin","checksum":"7d993f27a1afac4001c539a829e2af211ced62604930c90df32a307cf74cb4a4"}, @@ -592,7 +592,7 @@ {"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"}, {"name":"regexp_property_values","version":"1.0.0","platform":"java","checksum":"5e26782b01241616855c4ee7bb8a62fce9387e484f2d3eaf04f2a0633708222e"}, {"name":"regexp_property_values","version":"1.0.0","platform":"ruby","checksum":"162499dc0bba1e66d334273a059f207a61981cc8cc69d2ca743594e7886d080f"}, -{"name":"reline","version":"0.5.12","platform":"ruby","checksum":"41ab36d3fd2aaa169e99f8b82a93b9585f51130529360e24388fcccc20a055a2"}, +{"name":"reline","version":"0.6.0","platform":"ruby","checksum":"57620375dcbe56ec09bac7192bfb7460c716bbf0054dc94345ecaa5438e539d2"}, {"name":"representable","version":"3.2.0","platform":"ruby","checksum":"cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace"}, {"name":"request_store","version":"1.5.1","platform":"ruby","checksum":"07a204d161590789f2b1d27f9f0eadcdecd6d868cb2f03240250e1bc747df78e"}, {"name":"responders","version":"3.0.1","platform":"ruby","checksum":"613fe28e498987f4feaa3230aa6313ca4bd5f0563a3da83511b0dd6cd8f47292"}, @@ -737,7 +737,7 @@ {"name":"thread_safe","version":"0.3.6","platform":"ruby","checksum":"9ed7072821b51c57e8d6b7011a8e282e25aeea3a4065eab326e43f66f063b05a"}, {"name":"thrift","version":"0.16.0","platform":"ruby","checksum":"d023286ea89e30444c9f1c28dd76107f87d8aaf85fe1742da1d8cd3b5417dcce"}, {"name":"tilt","version":"2.0.11","platform":"ruby","checksum":"7b180fc472cbdeb186c85d31c0f2d1e61a2c0d77e1d9fd0ca28482a9d972d6a0"}, -{"name":"timeout","version":"0.4.2","platform":"ruby","checksum":"8aca2d5ff98eb2f7a501c03f8c3622065932cc58bc58f725cd50a09e63b4cc19"}, +{"name":"timeout","version":"0.4.3","platform":"ruby","checksum":"9509f079b2b55fe4236d79633bd75e34c1c1e7e3fb4b56cb5fda61f80a0fe30e"}, {"name":"timfel-krb5-auth","version":"0.8.3","platform":"ruby","checksum":"ab388c9d747fa3cd95baf2cc1c03253e372d8c680adcc543670f4f099854bb80"}, {"name":"tins","version":"1.31.1","platform":"ruby","checksum":"51c4a347c25c630d310cbc2c040ffb84e266c8227f2ade881f1130ee4f9fbecf"}, {"name":"toml-rb","version":"2.2.0","platform":"ruby","checksum":"a1e2c54ac3cc9d49861004f75f0648b3622ac03a76abe105358c31553227d9a6"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 6df4dab08d3..4293c0f166d 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -23,6 +23,12 @@ PATH error_tracking_open_api (1.0.0) typhoeus (~> 1.0, >= 1.0.1) +PATH + remote: gems/gitlab-active-context + specs: + gitlab-active-context (0.0.1) + zeitwerk + PATH remote: gems/gitlab-backup-cli specs: @@ -1557,7 +1563,7 @@ GEM msgpack (>= 0.4.3) optimist (>= 3.0.0) rchardet (1.8.0) - rdoc (6.8.1) + rdoc (6.9.1) psych (>= 4.0.0) re2 (2.7.0) mini_portile2 (~> 2.8.5) @@ -1587,7 +1593,7 @@ GEM redis (>= 4, < 6) regexp_parser (2.6.0) regexp_property_values (1.0.0) - reline (0.5.12) + reline (0.6.0) io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) @@ -1861,7 +1867,7 @@ GEM thread_safe (0.3.6) thrift (0.16.0) tilt (2.0.11) - timeout (0.4.2) + timeout (0.4.3) timfel-krb5-auth (0.8.3) tins (1.31.1) sync @@ -2094,6 +2100,7 @@ DEPENDENCIES gettext (~> 3.4, >= 3.4.9) gettext_i18n_rails (~> 1.13.0) gitaly (~> 17.5.0.pre.rc1) + gitlab-active-context! gitlab-backup-cli! gitlab-chronic (~> 0.10.5) gitlab-cloud-connector (~> 0.2.5) diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js index 968f4995b91..057a881ad41 100644 --- a/app/assets/javascripts/graphql_shared/issuable_client.js +++ b/app/assets/javascripts/graphql_shared/issuable_client.js @@ -45,6 +45,29 @@ export const config = { toReference({ __typename: 'LocalWorkItemChildIsExpanded', id: variables.id }), }, }, + WorkItemDescriptionTemplateConnection: { + fields: { + nodes: { + read(_, { variables }) { + const templates = [ + /* eslint-disable @gitlab/require-i18n-strings */ + { name: 'template 1', content: 'A template' }, + { name: 'template 2', content: 'Another template' }, + { name: 'template 3', content: 'Secret template omg wow' }, + { name: 'template 4', content: 'Another another template' }, + /* eslint-enable @gitlab/require-i18n-strings */ + ]; + if (variables.search) { + return templates.filter(({ name }) => name.includes(variables.search)); + } + if (variables.name) { + return templates.filter(({ name }) => name === variables.name); + } + return templates; + }, + }, + }, + }, Project: { fields: { projectMembers: { diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js index 614e076ec5a..3675cd5a1bb 100644 --- a/app/assets/javascripts/issues/create_merge_request_dropdown.js +++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js @@ -497,7 +497,7 @@ export default class CreateMergeRequestDropdown { removeMessage(target) { const { input, message } = this.getTargetData(target); const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline']; - const messageClasses = ['gl-text-subtle', 'gl-text-red-500', 'gl-text-green-500']; + const messageClasses = ['gl-text-subtle', 'gl-text-red-500', 'gl-text-success']; inputClasses.forEach((cssClass) => input.classList.remove(cssClass)); messageClasses.forEach((cssClass) => message.classList.remove(cssClass)); @@ -520,7 +520,7 @@ export default class CreateMergeRequestDropdown { this.removeMessage(target); input.classList.add('gl-field-success-outline'); - message.classList.add('gl-text-green-500'); + message.classList.add('gl-text-success'); message.textContent = sprintf(__('%{text} is available'), { text }); message.style.display = 'inline-block'; } diff --git a/app/assets/javascripts/vue_shared/components/projects_list/formatter.js b/app/assets/javascripts/vue_shared/components/projects_list/formatter.js index 0dd6f4bbb13..61517d1cd56 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/formatter.js +++ b/app/assets/javascripts/vue_shared/components/projects_list/formatter.js @@ -17,6 +17,7 @@ export const formatGraphQLProjects = (projects) => ...project, id: getIdFromGraphQLId(id), name: nameWithNamespace, + avatarLabel: nameWithNamespace, mergeRequestsAccessLevel: mergeRequestsAccessLevel.stringValue, issuesAccessLevel: issuesAccessLevel.stringValue, forkingAccessLevel: forkingAccessLevel.stringValue, diff --git a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_description.vue b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_description.vue index a5706a982bc..1d8759ac78e 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_description.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/project_list_item_description.vue @@ -1,17 +1,11 @@ diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue index 4b03d5b6f14..bf634f7305d 100644 --- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue @@ -1,13 +1,5 @@ diff --git a/app/assets/javascripts/vue_shared/components/resource_lists/list_item.vue b/app/assets/javascripts/vue_shared/components/resource_lists/list_item.vue index ebe372e6696..4fb3164fa7a 100644 --- a/app/assets/javascripts/vue_shared/components/resource_lists/list_item.vue +++ b/app/assets/javascripts/vue_shared/components/resource_lists/list_item.vue @@ -1,5 +1,5 @@ + + diff --git a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_stat.vue b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_stat.vue index 4cd82dd13b6..ea619791637 100644 --- a/app/assets/javascripts/vue_shared/components/resource_lists/list_item_stat.vue +++ b/app/assets/javascripts/vue_shared/components/resource_lists/list_item_stat.vue @@ -1,5 +1,5 @@ diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index 22c74b390ad..22fc3a06a12 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -1,12 +1,14 @@ @@ -309,9 +370,42 @@ export default { + + +

+ {{ + s__( + 'WorkItem|Applying a template will replace the existing description. Any changes you have made will be lost.', + ) + }} +

+ +
+import { GlCollapsibleListbox, GlSkeletonLoader, GlSprintf, GlLink } from '@gitlab/ui'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import { s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import workItemDescriptionTemplatesListQuery from '../graphql/work_item_description_templates_list.query.graphql'; + +export default { + name: 'WorkItemDescriptionTemplateListbox', + components: { + GlCollapsibleListbox, + GlSkeletonLoader, + GlSprintf, + GlLink, + }, + props: { + fullPath: { + type: String, + required: true, + }, + template: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + showListbox: false, + descriptionTemplates: [], + searchTerm: '', + }; + }, + apollo: { + descriptionTemplates: { + query: workItemDescriptionTemplatesListQuery, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update(data) { + return data.namespace?.workItemDescriptionTemplates.nodes || []; + }, + error(e) { + Sentry.captureException(e); + }, + }, + }, + computed: { + loading() { + return this.$apollo.queries.descriptionTemplates.loading; + }, + toggleText() { + return this.template || s__('WorkItem|Choose a template'); + }, + hasTemplates() { + return this.descriptionTemplates.length > 0; + }, + items() { + const listboxItems = this.descriptionTemplates.map(({ name }) => ({ + value: name, + text: name, + })); + if (this.searchTerm) { + return listboxItems.filter(({ text }) => text.includes(this.searchTerm)); + } + return listboxItems; + }, + }, + methods: { + handleSelect(item) { + this.$emit('selectTemplate', item); + }, + handleSearch(searchTerm) { + this.searchTerm = searchTerm; + }, + }, + templateDocsPath: helpPagePath('user/project/description_templates'), +}; + + + diff --git a/app/assets/javascripts/work_items/graphql/work_item_description_template.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_description_template.query.graphql new file mode 100644 index 00000000000..1abc534286f --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item_description_template.query.graphql @@ -0,0 +1,12 @@ +query workItemDescriptionTemplate($fullPath: ID!, $name: String!) { + namespace(fullPath: $fullPath) { + id + workItemDescriptionTemplates(name: $name) { + __typename + nodes @client { + name + content + } + } + } +} diff --git a/app/assets/javascripts/work_items/graphql/work_item_description_templates_list.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_description_templates_list.query.graphql new file mode 100644 index 00000000000..cc5e279ddcb --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item_description_templates_list.query.graphql @@ -0,0 +1,11 @@ +query workItemDescriptionTemplatesList($fullPath: ID!) { + namespace(fullPath: $fullPath) { + id + workItemDescriptionTemplates { + __typename + nodes @client { + name + } + } + } +} diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss index 9f210376402..131b3004285 100644 --- a/app/assets/stylesheets/page_bundles/projects.scss +++ b/app/assets/stylesheets/page_bundles/projects.scss @@ -553,20 +553,6 @@ } } -.projects-list-item { - .description { - max-height: $gl-spacing-scale-8; - - p { - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - text-overflow: ellipsis; - /* stylelint-disable-next-line value-no-vendor-prefix */ - display: -webkit-box; - } - } -} - .projects-list .description p { @apply gl-line-clamp-2 gl-whitespace-normal; margin-bottom: 0; diff --git a/app/controllers/concerns/autocomplete_sources/expires_in.rb b/app/controllers/concerns/autocomplete_sources/expires_in.rb index dedc9ec7ac2..bba3bc77cb6 100644 --- a/app/controllers/concerns/autocomplete_sources/expires_in.rb +++ b/app/controllers/concerns/autocomplete_sources/expires_in.rb @@ -5,7 +5,7 @@ module AutocompleteSources extend ActiveSupport::Concern AUTOCOMPLETE_EXPIRES_IN = 3.minutes - AUTOCOMPLETE_CACHED_ACTIONS = [:members, :commands, :labels, :issues].freeze + AUTOCOMPLETE_CACHED_ACTIONS = [:members, :labels].freeze included do before_action :set_expires_in, only: AUTOCOMPLETE_CACHED_ACTIONS @@ -14,18 +14,7 @@ module AutocompleteSources private def set_expires_in - case action_name.to_sym - when :members - expires_in AUTOCOMPLETE_EXPIRES_IN if Feature.enabled?(:cache_autocomplete_sources_members, current_user) - when :commands - expires_in AUTOCOMPLETE_EXPIRES_IN if Feature.enabled?(:cache_autocomplete_sources_commands, current_user) - when :labels - expires_in AUTOCOMPLETE_EXPIRES_IN if Feature.enabled?(:cache_autocomplete_sources_labels, current_user) - when :issues - if Feature.enabled?(:cache_autocomplete_sources_issues, current_user, type: :wip) - expires_in AUTOCOMPLETE_EXPIRES_IN - end - end + expires_in AUTOCOMPLETE_EXPIRES_IN end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 1cf5b45f824..0705e0f2063 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -50,6 +50,7 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:issues_list_drawer, project) push_frontend_feature_flag(:notifications_todos_buttons, current_user) push_force_frontend_feature_flag(:glql_integration, project&.glql_integration_feature_flag_enabled?) + push_force_frontend_feature_flag(:work_items_alpha, project&.work_items_alpha_feature_flag_enabled?) end before_action only: [:index, :show] do @@ -63,7 +64,6 @@ class Projects::IssuesController < Projects::ApplicationController before_action only: :show do push_frontend_feature_flag(:work_items_beta, project&.group) push_force_frontend_feature_flag(:work_items_beta, project&.work_items_beta_feature_flag_enabled?) - push_force_frontend_feature_flag(:work_items_alpha, project&.work_items_alpha_feature_flag_enabled?) push_frontend_feature_flag(:epic_widget_edit_confirmation, project) push_frontend_feature_flag(:namespace_level_work_items, project&.group) push_frontend_feature_flag(:work_items_view_preference, current_user) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 1e75290a801..b4f53354b82 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -12,6 +12,7 @@ # project_id; integer # target_id; integer # state: 'pending' (default) or 'done' +# is_snoozed: boolean # type: 'Issue' or 'MergeRequest' or ['Issue', 'MergeRequest'] # @@ -51,6 +52,7 @@ class TodosFinder items = by_action(items) items = by_author(items) items = by_state(items) + items = by_snoozed_status(items) if Feature.enabled?(:todos_snoozing, current_user) items = by_target_id(items) items = by_types(items) items = by_group(items) @@ -103,6 +105,10 @@ class TodosFinder params[:action] end + def snoozed? + params[:is_snoozed] + end + def author? params[:author_id].present? end @@ -214,6 +220,13 @@ class TodosFinder items end + def by_snoozed_status(items) + return items.snoozed if snoozed? + return items.not_snoozed if filter_pending_only? + + items + end + def by_target_id(items) return items if params[:target_id].blank? diff --git a/app/graphql/resolvers/todos_resolver.rb b/app/graphql/resolvers/todos_resolver.rb index 6daa65bb0d9..071f4812f9e 100644 --- a/app/graphql/resolvers/todos_resolver.rb +++ b/app/graphql/resolvers/todos_resolver.rb @@ -26,6 +26,10 @@ module Resolvers required: false, description: 'State of the todo.' + argument :is_snoozed, GraphQL::Types::Boolean, + required: false, + description: 'Whether the to-do item is snoozed.' + argument :type, [Types::TodoTargetEnum], required: false, description: 'Type of the todo.' @@ -53,6 +57,7 @@ module Resolvers def todo_finder_params(args) { state: args[:state], + is_snoozed: args[:is_snoozed], type: args[:type], group_id: args[:group_id], author_id: args[:author_id], diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb index 5dca6eb1e65..f651d254411 100644 --- a/app/helpers/ci/runners_helper.rb +++ b/app/helpers/ci/runners_helper.rb @@ -16,7 +16,7 @@ module Ci when :online title = s_("Runners|Runner is online; last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(contacted_at) } icon = 'status-active' - span_class = 'gl-text-green-500' + span_class = 'gl-text-success' when :never_contacted title = s_("Runners|Runner has never contacted this instance") icon = 'warning-solid' diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index debc0dce0c0..55249d5be9c 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -145,7 +145,7 @@ module Auth patterns = actions_to_check.index_with { [] } # Admins get unrestricted access, but the registry expects to always see an array for each granted actions, so we # can return early here, but not any earlier. - return patterns if user.admin? + return patterns if user.can_admin_all_resources? user_access_level = user.max_member_access_for_project(project.id) applicable_rules = project.container_registry_protection_tag_rules.for_actions_and_access(actions_to_check, user_access_level) diff --git a/app/services/award_emojis/copy_service.rb b/app/services/award_emojis/copy_service.rb index 2e500d4c697..1790da60844 100644 --- a/app/services/award_emojis/copy_service.rb +++ b/app/services/award_emojis/copy_service.rb @@ -17,7 +17,12 @@ module AwardEmojis from_awardable.award_emoji.find_each do |award| new_award = award.dup new_award.awardable = to_awardable - new_award.save! + # In some instances when an awardable has a custom emoji and is being moved to a namespace where this + # emoji does not exist the save! will raise a validation exception. + # see `AwardEmoji`: validates :name, presence: true, 'gitlab/emoji_name': true + # + # We can skip copying custom emoji for now: https://gitlab.com/gitlab-org/gitlab/-/issues/501193#note_2186334353 + new_award.save! if new_award.valid? end ServiceResponse.success diff --git a/app/services/todos/snoozing_service.rb b/app/services/todos/snoozing_service.rb index 2e56718edc7..106df80159c 100644 --- a/app/services/todos/snoozing_service.rb +++ b/app/services/todos/snoozing_service.rb @@ -10,7 +10,7 @@ module Todos class SnoozingService def snooze_todo(todo, snooze_until) - if !todo.snoozed_until.nil? || todo.update(snoozed_until: snooze_until) + if todo.update(snoozed_until: snooze_until) ServiceResponse.success(payload: { todo: todo }) else ServiceResponse.error(message: todo.errors.full_messages) diff --git a/app/validators/json_schemas/user_detail_onboarding_status.json b/app/validators/json_schemas/user_detail_onboarding_status.json index a9dc6b35960..84ef6b36083 100644 --- a/app/validators/json_schemas/user_detail_onboarding_status.json +++ b/app/validators/json_schemas/user_detail_onboarding_status.json @@ -32,6 +32,19 @@ "description": "Setting to understand if a user is joining a project or not during onboarding", "type": "boolean" }, + "registration_objective": { + "description": "Goal of registration collected during onboarding", + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ] + }, "role": { "description": "User persona collected during onboarding", "type": "integer", diff --git a/config/feature_flags/development/cache_autocomplete_sources_commands.yml b/config/feature_flags/development/cache_autocomplete_sources_commands.yml deleted file mode 100644 index c4cf6d19d48..00000000000 --- a/config/feature_flags/development/cache_autocomplete_sources_commands.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: cache_autocomplete_sources_commands -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138226 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433168 -milestone: '16.7' -type: development -group: group::global search -default_enabled: false diff --git a/config/feature_flags/development/cache_autocomplete_sources_labels.yml b/config/feature_flags/development/cache_autocomplete_sources_labels.yml deleted file mode 100644 index fe1e3ffc86c..00000000000 --- a/config/feature_flags/development/cache_autocomplete_sources_labels.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: cache_autocomplete_sources_labels -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138226 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433170 -milestone: '16.7' -type: development -group: group::global search -default_enabled: true diff --git a/config/feature_flags/development/cache_autocomplete_sources_members.yml b/config/feature_flags/development/cache_autocomplete_sources_members.yml deleted file mode 100644 index f55c604153a..00000000000 --- a/config/feature_flags/development/cache_autocomplete_sources_members.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: cache_autocomplete_sources_members -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133454 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427452 -milestone: '16.5' -type: development -group: group::global search -default_enabled: true diff --git a/config/feature_flags/wip/cache_autocomplete_sources_issues.yml b/config/feature_flags/wip/cache_autocomplete_sources_issues.yml deleted file mode 100644 index d6d8f05a8a7..00000000000 --- a/config/feature_flags/wip/cache_autocomplete_sources_issues.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cache_autocomplete_sources_issues -feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11777 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156032 -rollout_issue_url: -milestone: '17.2' -group: group::product planning -type: wip -default_enabled: false diff --git a/config/feature_flags/wip/todos_snoozing.yml b/config/feature_flags/wip/todos_snoozing.yml new file mode 100644 index 00000000000..340a356cc7c --- /dev/null +++ b/config/feature_flags/wip/todos_snoozing.yml @@ -0,0 +1,9 @@ +--- +name: todos_snoozing +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/17712 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175163 +rollout_issue_url: +milestone: '17.7' +group: group::personal productivity +type: wip +default_enabled: false diff --git a/doc/administration/dedicated/configure_instance.md b/doc/administration/dedicated/configure_instance.md index d60323634b7..9e8123f870b 100644 --- a/doc/administration/dedicated/configure_instance.md +++ b/doc/administration/dedicated/configure_instance.md @@ -543,7 +543,10 @@ Logs stored in the S3 bucket are retained indefinitely, until the one year reten To gain read only access to the S3 bucket with your application logs: 1. Open a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650) with the title `Customer Log Access`. -1. In the body of the ticket, include a list of IAM Principal Amazon Resource Names (users or roles) that require access to the logs from the S3 bucket. +1. In the body of the ticket, include a list of IAM Principal Amazon Resource Names (ARNs) that require access to the logs from the S3 bucket. The ARNs can be for users or roles. + + NOTE: + Specify the full ARN path without wildcards (`*`). Wildcard characters are not supported. GitLab team members can read more about the proposed feature to add wildcard support in this confidential issue: [7010](https://gitlab.com/gitlab-com/gl-infra/gitlab-dedicated/team/-/issues/7010). GitLab provides the name of the S3 bucket. Your authorized users or roles can then access all objects in the bucket. To verify access, you can use the [AWS CLI](https://aws.amazon.com/cli/). diff --git a/doc/administration/moderate_users.md b/doc/administration/moderate_users.md index 0d3cad8dd0c..699f9f0e56f 100644 --- a/doc/administration/moderate_users.md +++ b/doc/administration/moderate_users.md @@ -43,7 +43,7 @@ sign in. ### View user sign ups pending approval -> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. +> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. To view user sign ups pending approval: @@ -53,7 +53,7 @@ To view user sign ups pending approval: ### Approve or reject a user sign up -> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. +> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. A user sign up pending approval can be approved or rejected from the **Admin** area. @@ -120,7 +120,7 @@ To report abuse from other users, see [report abuse](../user/report_abuse.md). F ### Unblock a user -> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. +> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. A blocked user can be unblocked from the **Admin** area. To do this: @@ -133,7 +133,7 @@ The user's state is set to active and they consume a [seat](../subscriptions/self_managed/index.md#billable-users). NOTE: -Users can also be unblocked using the [GitLab API](../api/user_moderation.md#unblock-a-user). +Users can also be unblocked using the [GitLab API](../api/user_moderation.md#unblock-access-to-a-user). The unblock option may be unavailable for LDAP users. To enable the unblock option, the LDAP identity first needs to be deleted: @@ -145,15 +145,15 @@ the LDAP identity first needs to be deleted: 1. Select the **Identities** tab. 1. Find the LDAP provider and select **Delete**. -## Activate and deactivate users +## Deactivate and reactivate users -GitLab administrators can deactivate and activate users. -You should deactivate a user if they have no recent activity, and you don't want them to occupy a seat on the instance. +GitLab administrators can deactivate and reactivate users. +You should deactivate a user if they have no recent activity, and you do not want them to occupy a seat on the instance. A deactivated user: - Can sign in to GitLab. - - If a deactivated user signs in, they are automatically activated. + - If a deactivated user signs in, they are automatically reactivated. - Cannot access repositories or the API. - Cannot use slash commands. For more information, see [slash commands](../user/project/integrations/gitlab_slack_application.md#slash-commands). - Does not occupy a seat. For more information, see [billable users](../subscriptions/self_managed/index.md#billable-users). @@ -200,7 +200,7 @@ To do this: 1. Under **Days of inactivity before deactivation**, enter the number of days before deactivation. Minimum value is 90 days. 1. Select **Save changes**. -When this feature is enabled, GitLab runs a job once a day to deactivate the dormant users. +When this feature is enabled, GitLab runs a daily job to deactivate the dormant users. A maximum of 100,000 users can be deactivated per day. @@ -239,25 +239,25 @@ This job only runs when the `email_confirmation_setting` is set to `soft` or `ha A maximum of 240,000 users can be deleted per day. -### Activate a user +### Reactivate a user -> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. +> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. -A deactivated user can be activated from the **Admin** area. +You can reactivate a deactivated user from the **Admin** area. To do this: 1. On the left sidebar, at the bottom, select **Admin**. 1. Select **Overview > Users**. 1. In the search box, filter by **State=Deactivated** and press Enter. -1. For the user you want to activate, select the vertical ellipsis (**{ellipsis_v}**), then **Activate**. +1. For the user you want to reactivate, select the vertical ellipsis (**{ellipsis_v}**), then **Activate**. The user's state is set to active and they consume a [seat](../subscriptions/self_managed/index.md#billable-users). NOTE: -A deactivated user can also activate their account themselves by logging back in through the UI. -Users can also be activated using the [GitLab API](../api/user_moderation.md#activate-a-user). +A deactivated user can also reactivate their account themselves by logging back in through the UI. +Users can also be reactivated using the [GitLab API](../api/user_moderation.md#reactivate-a-user). ## Ban and unban users @@ -288,7 +288,7 @@ To ban a user: ### Unban a user -> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. +> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. To unban a user: @@ -328,7 +328,7 @@ Before 15.1, additionally groups of which deleted user were the only owner among ## Trust and untrust users > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132402) in GitLab 16.5. -> - Ability to filter a user by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. +> - Filter users by state [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/238183) in GitLab 17.0. You can trust and untrust users from the **Admin** area. diff --git a/doc/api/branches.md b/doc/api/branches.md index b36e73b4548..17e4aa09f74 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -165,7 +165,8 @@ Parameters: Example request: ```shell -curl --request POST --header "PRIVATE-TOKEN: " \ +curl --request POST \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=main" ``` @@ -223,7 +224,8 @@ Parameters: Example request: ```shell -curl --request DELETE --header "PRIVATE-TOKEN: " \ +curl --request DELETE \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch" ``` @@ -252,7 +254,8 @@ Parameters: Example request: ```shell -curl --request DELETE --header "PRIVATE-TOKEN: " \ +curl --request DELETE \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches" ``` diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 11e3f7bef32..2e3c3c0e516 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -18595,6 +18595,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -18931,6 +18932,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -19588,6 +19590,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -22205,6 +22208,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -27624,6 +27628,7 @@ Defines which user roles, users, or groups can merge into a protected branch. | `milestone` | [`Milestone`](#milestone) | Milestone of the merge request. | | `name` | [`String`](#string) | Name or title of this object. | | `participants` | [`MergeRequestParticipantConnection`](#mergerequestparticipantconnection) | Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes. (see [Connections](#connections)) | +| `policiesOverridingApprovalSettings` | [`[PolicyApprovalSettingsOverride!]`](#policyapprovalsettingsoverride) | Approval settings that are overridden by the policies for the merge request. | | `policyViolations` | [`PolicyViolationDetails`](#policyviolationdetails) | Policy violations reported on the merge request. | | `preparedAt` | [`Time`](#time) | Timestamp of when the merge request was prepared. | | `project` | [`Project!`](#project) | Alias for target_project. | @@ -28142,6 +28147,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -28551,6 +28557,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -29006,6 +29013,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -29434,6 +29442,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -31326,6 +31335,18 @@ Represents policy violation for `any_merge_request` report_type. | `id` | [`ID!`](#id) | ID of the namespace. | | `webUrl` | [`String!`](#string) | Web URL of the group. | +### `PolicyApprovalSettingsOverride` + +Represents the approval settings of merge request overridden by a policy. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `editPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. Path to edit the policy. | +| `name` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. Policy name. | +| `settings` | [`JSON!`](#json) | Overridden project approval settings. | + ### `PolicyApproversType` Multiple approvers action. @@ -36240,6 +36261,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | @@ -43866,6 +43888,7 @@ four standard [pagination arguments](#pagination-arguments): | `action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. | | `authorId` | [`[ID!]`](#id) | ID of an author. | | `groupId` | [`[ID!]`](#id) | ID of a group. | +| `isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. | | `projectId` | [`[ID!]`](#id) | ID of a project. | | `sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. | | `state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. | diff --git a/doc/api/group_protected_branches.md b/doc/api/group_protected_branches.md index 0824db5ca4c..683a4abdd26 100644 --- a/doc/api/group_protected_branches.md +++ b/doc/api/group_protected_branches.md @@ -48,7 +48,7 @@ GET /groups/:id/protected_branches ```shell curl --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/5/protected_branches" + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches" ``` Example response: @@ -122,7 +122,7 @@ GET /groups/:id/protected_branches/:name ```shell curl --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/5/protected_branches/main" + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches/main" ``` Example response: @@ -163,8 +163,9 @@ POST /groups/:id/protected_branches ``` ```shell -curl --request POST --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40" ``` | Attribute | Type | Required | Description | @@ -227,8 +228,9 @@ access to the project and each group must allow [more granular control over protected branch access](../user/project/repository/branches/protected.md). ```shell -curl --request POST --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1" ``` Example response: @@ -275,18 +277,18 @@ Example request: ```shell curl --request POST \ - --header "PRIVATE-TOKEN: " \ - --header "Content-Type: application/json" \ - --data '{ - "name": "main", - "allowed_to_push": [{"access_level": 30}], - "allowed_to_merge": [{ - "access_level": 30 - },{ - "access_level": 40 - } - ]}' - "https://gitlab.example.com/api/v4/groups/5/protected_branches" + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --data '{ + "name": "main", + "allowed_to_push": [{"access_level": 30}], + "allowed_to_merge": [{ + "access_level": 30 + },{ + "access_level": 40 + } + ]}' + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches" ``` Example response: @@ -343,8 +345,9 @@ DELETE /groups/:id/protected_branches/:name ``` ```shell -curl --request DELETE --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/5/protected_branches/*-stable" +curl --request DELETE \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches/*-stable" ``` | Attribute | Type | Required | Description | @@ -378,8 +381,9 @@ PATCH /groups/:id/protected_branches/:name ``` ```shell -curl --request PATCH --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true" +curl --request PATCH \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true" ``` | Attribute | Type | Required | Description | @@ -412,9 +416,9 @@ To delete: ```shell curl --header 'Content-Type: application/json' --request PATCH \ - --data '{"allowed_to_push": [{access_level: 40}]}' \ - --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main" + --data '{"allowed_to_push": [{access_level: 40}]}' \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main" ``` Example response: @@ -438,8 +442,9 @@ Example response: ```shell curl --header 'Content-Type: application/json' --request PATCH \ - --data '{"allowed_to_push": [{"id": 12, "access_level": 0}]' \ - --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main" + --data '{"allowed_to_push": [{"id": 12, "access_level": 0}]' \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main" ``` Example response: @@ -463,8 +468,9 @@ Example response: ```shell curl --header 'Content-Type: application/json' --request PATCH \ - --data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \ - --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main" + --data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main" ``` Example response: diff --git a/doc/api/group_ssh_certificates.md b/doc/api/group_ssh_certificates.md index 69602ea5ce2..f65b0f8d803 100644 --- a/doc/api/group_ssh_certificates.md +++ b/doc/api/group_ssh_certificates.md @@ -36,7 +36,8 @@ Read more on [pagination](rest/index.md#pagination). Example request: ```shell -curl --header "PRIVATE-TOKEN: " "https://primary.example.com/api/v4/groups/90/ssh_certificates" +curl --header "PRIVATE-TOKEN: " \ + --url "https://primary.example.com/api/v4/groups/90/ssh_certificates" ``` Example response: diff --git a/doc/api/project_aliases.md b/doc/api/project_aliases.md index e28d7f71db9..efb46ab4c49 100644 --- a/doc/api/project_aliases.md +++ b/doc/api/project_aliases.md @@ -84,19 +84,20 @@ POST /project_aliases | `project_id` | integer or string | Yes | The ID or path of the project. | ```shell -curl --request POST --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/project_aliases" \ - --form "project_id=1" \ - --form "name=gitlab" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/project_aliases" \ + --form "project_id=1" \ + --form "name=gitlab" ``` or ```shell curl --request POST --header "PRIVATE-TOKEN: " \ - --url "https://gitlab.example.com/api/v4/project_aliases" \ - --form "project_id=gitlab-org/gitlab" \ - --form "name=gitlab" + --url "https://gitlab.example.com/api/v4/project_aliases" \ + --form "project_id=gitlab-org/gitlab" \ + --form "name=gitlab" ``` Example response: @@ -123,6 +124,7 @@ DELETE /project_aliases/:name | `name` | string | Yes | The name of the alias. | ```shell -curl --request DELETE --header "PRIVATE-TOKEN: " \ +curl --request DELETE \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/project_aliases/gitlab" ``` diff --git a/doc/api/project_pull_mirroring.md b/doc/api/project_pull_mirroring.md index 98f9d235956..6682d71dcbc 100644 --- a/doc/api/project_pull_mirroring.md +++ b/doc/api/project_pull_mirroring.md @@ -32,7 +32,9 @@ Supported attributes: Example request: ```shell -curl --request GET --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" +curl --request GET \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" ``` If successful, returns [`200 OK`](rest/troubleshooting.md#status-codes) and the @@ -94,7 +96,8 @@ Supported attributes: Example request to add pull mirroring: ```shell -curl --request PUT --header "PRIVATE-TOKEN: " \ +curl --request PUT \ + --header "PRIVATE-TOKEN: " \ --header "Content-Type: application/json" \ --data '{ "enabled": true, @@ -108,7 +111,8 @@ curl --request PUT --header "PRIVATE-TOKEN: " \ Example request to remove pull mirroring: ```shell -curl --request PUT --header "PRIVATE-TOKEN: " \ +curl --request PUT \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" \ --data "enabled=false" ``` @@ -146,7 +150,8 @@ Supported attributes: Example creating a project with pull mirroring: ```shell -curl --request POST --header "PRIVATE-TOKEN: " \ +curl --request POST \ + --header "PRIVATE-TOKEN: " \ --header "Content-Type: application/json" \ --data '{ "name": "new_project", @@ -160,7 +165,8 @@ curl --request POST --header "PRIVATE-TOKEN: " \ Example adding pull mirroring: ```shell -curl --request PUT --header "PRIVATE-TOKEN: " \ +curl --request PUT \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/:id" \ --data "mirror=true&import_url=https://username:token@gitlab.example.com/group/project.git" ``` @@ -168,7 +174,8 @@ curl --request PUT --header "PRIVATE-TOKEN: " \ Example removing pull mirroring: ```shell -curl --request PUT --header "PRIVATE-TOKEN: " \ +curl --request PUT \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/:id" \ --data "mirror=false" ``` @@ -190,5 +197,7 @@ Supported attributes: Example request: ```shell -curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" ``` diff --git a/doc/api/project_security_settings.md b/doc/api/project_security_settings.md index e307903af18..09de440969a 100644 --- a/doc/api/project_security_settings.md +++ b/doc/api/project_security_settings.md @@ -1,5 +1,5 @@ --- -stage: Secure +stage: Application Security Testing group: Secret Detection info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments" --- diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index e423d6237ed..3af3b04a67b 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -264,7 +264,8 @@ GET /projects/:id/snippets/:snippet_id/user_agent_detail Example request: ```shell -curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail" +curl --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail" ``` Example response: diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index a78aa5c5851..011cb530e9c 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -41,7 +41,8 @@ GET /projects/:id/protected_branches In the following example, the project ID is `5`. ```shell -curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches" +curl --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches" ``` The following example response includes: @@ -167,7 +168,8 @@ GET /projects/:id/protected_branches/:name In the following example, the project ID is `5` and branch name is `main`: ```shell -curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches/main" +curl --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches/main" ``` Example response: @@ -254,7 +256,9 @@ POST /projects/:id/protected_branches In the following example, the project ID is `5` and branch name is `*-stable`. ```shell -curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40" ``` The example response includes: @@ -356,7 +360,9 @@ The following example request creates a protected branch with user push access a The `user_id` is `2` and the `group_id` is `3`. ```shell -curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=2&allowed_to_merge%5B%5D%5Bgroup_id%5D=3" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=2&allowed_to_merge%5B%5D%5Bgroup_id%5D=3" ``` The following example response includes: @@ -416,7 +422,9 @@ The deploy key must be enabled for your project and it must have write access to For other requirements, see [Allow deploy keys to push to a protected branch](../user/project/repository/branches/protected.md#allow-deploy-keys-to-push-to-a-protected-branch). ```shell -curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push[][deploy_key_id]=1" +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push[][deploy_key_id]=1" ``` The following example response includes: @@ -475,19 +483,19 @@ Example request: ```shell curl --request POST \ - --header "PRIVATE-TOKEN: " \ - --header "Content-Type: application/json" \ - --data '{ - "name": "main", - "allowed_to_push": [ - {"access_level": 30} - ], - "allowed_to_merge": [ - {"access_level": 30}, - {"access_level": 40} - ] - }' - "https://gitlab.example.com/api/v4/projects/5/protected_branches" + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --data '{ + "name": "main", + "allowed_to_push": [ + {"access_level": 30} + ], + "allowed_to_merge": [ + {"access_level": 30}, + {"access_level": 40} + ] + }' + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches" ``` The following example response includes: @@ -556,7 +564,9 @@ DELETE /projects/:id/protected_branches/:name In the following example, the project ID is `5` and branch name is `*-stable`. ```shell -curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable" +curl --request DELETE \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable" ``` ## Update a protected branch @@ -583,7 +593,9 @@ PATCH /projects/:id/protected_branches/:name In the following example, the project ID is `5` and branch name is `feature-branch`. ```shell -curl --request PATCH --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true" +curl --request PATCH \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true" ``` Elements in the `allowed_to_push`, `allowed_to_merge` and `allowed_to_unprotect` arrays should be one of `user_id`, `group_id` or @@ -608,9 +620,9 @@ To delete: ```shell curl --header 'Content-Type: application/json' --request PATCH \ - --data '{"allowed_to_push": [{"access_level": 40}]}' \ - --header "PRIVATE-TOKEN: " \ - "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main" + --data '{"allowed_to_push": [{"access_level": 40}]}' \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main" ``` Example response: @@ -634,8 +646,9 @@ Example response: ```shell curl --header 'Content-Type: application/json' --request PATCH \ - --data '{"allowed_to_push": [{"id": 12, "access_level": 0}]}' \ - --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main" + --data '{"allowed_to_push": [{"id": 12, "access_level": 0}]}' \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main" ``` Example response: @@ -659,8 +672,9 @@ Example response: ```shell curl --header 'Content-Type: application/json' --request PATCH \ - --data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \ - --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main" + --data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main" ``` Example response: diff --git a/doc/api/protected_tags.md b/doc/api/protected_tags.md index 98341793787..e196252c2e0 100644 --- a/doc/api/protected_tags.md +++ b/doc/api/protected_tags.md @@ -10,7 +10,7 @@ DETAILS: **Tier:** Free, Premium, Ultimate **Offering:** GitLab.com, Self-managed, GitLab Dedicated -**Valid access levels** +## Valid access levels These access levels are recognized: diff --git a/doc/api/repositories.md b/doc/api/repositories.md index dcf28db696c..ab40d699f95 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -418,7 +418,8 @@ If the last tag is `v0.9.0` and the default branch is `main`, the range of commi included in this example is `v0.9.0..main`: ```shell -curl --request POST --header "PRIVATE-TOKEN: token" \ +curl --request POST \ + --header "PRIVATE-TOKEN: token" \ --data "version=1.0.0" \ --url "https://gitlab.com/api/v4/projects/42/repository/changelog" ``` @@ -427,7 +428,8 @@ To generate the data on a different branch, specify the `branch` parameter. This command generates data from the `foo` branch: ```shell -curl --request POST --header "PRIVATE-TOKEN: token" \ +curl --request POST \ + --header "PRIVATE-TOKEN: token" \ --data "version=1.0.0&branch=foo" \ --url "https://gitlab.com/api/v4/projects/42/repository/changelog" ``` @@ -443,7 +445,8 @@ curl --request POST --header "PRIVATE-TOKEN: token" \ To store the results in a different file, use the `file` parameter: ```shell -curl --request POST --header "PRIVATE-TOKEN: token" \ +curl --request POST \ + --header "PRIVATE-TOKEN: token" \ --data "version=1.0.0&file=NEWS" \ --url "https://gitlab.com/api/v4/projects/42/repository/changelog" ``` diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 2fe1c66fc3b..3697f00f5be 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -396,7 +396,8 @@ Parameters: Example request: ```shell -curl --request DELETE --header "PRIVATE-TOKEN: " \ +curl --request DELETE \ + --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/snippets/1" ``` diff --git a/doc/api/user_moderation.md b/doc/api/user_moderation.md index 2674ba4de78..a90e21bd152 100644 --- a/doc/api/user_moderation.md +++ b/doc/api/user_moderation.md @@ -12,23 +12,23 @@ DETAILS: Use this API to moderate user accounts. For more information, see [Moderate users](../administration/moderate_users.md). -## Approve a user +## Approve access to a user -Approves the specified user. +Approves access to a given user account that is pending approval. Prerequisites: -- You must be an administrator. +- You must have administrator access to the instance. ```plaintext POST /users/:id/approve ``` -Parameters: +Supported attributes: -| Attribute | Type | Required | Description | -|------------|---------|----------|----------------------| -| `id` | integer | yes | ID of specified user | +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | ```shell curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/users/42/approve" @@ -55,22 +55,23 @@ Example Responses: { "message": "The user you are trying to approve is not pending approval" } ``` -## Reject a user +## Reject access to a user -Reject the specified user that is -[pending approval](../administration/moderate_users.md#users-pending-approval). +Rejects access to a given user account that is pending approval. Prerequisites: -- You must be an administrator. +- You must have administrator access to the instance. ```plaintext POST /users/:id/reject ``` -Parameters: +Supported attributes: -- `id` (required) - ID of specified user +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | ```shell curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/users/42/reject" @@ -97,47 +98,23 @@ Example Responses: { "message": "User does not have a pending request" } ``` -## Activate a user - -Activate the specified user. - -Prerequisites: - -- You must be an administrator. - -```plaintext -POST /users/:id/activate -``` - -Parameters: - -| Attribute | Type | Required | Description | -|------------|---------|----------|----------------------| -| `id` | integer | yes | ID of specified user | - -Returns: - -- `201 OK` on success. -- `404 User Not Found` if the user cannot be found. -- `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization. - ## Deactivate a user -Deactivate the specified user. +Deactivates a given user account. For more information on banned users, see [Activate and deactivate users](../administration/moderate_users.md#deactivate-and-reactivate-users). Prerequisites: -- You must be an administrator. +- You must have administrator access to the instance. ```plaintext POST /users/:id/deactivate ``` -Parameters: +Supported attributes: -| Attribute | Type | Required | Description | -|------------|---------|----------|----------------------| -| `id` | integer | yes | ID of specified user | +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | Returns: @@ -148,23 +125,47 @@ Returns: - Not [dormant](../administration/moderate_users.md#automatically-deactivate-dormant-users). - Internal. -## Block a user +## Reactivate a user -Block the specified user. +Reactivates a given user account that was previously deactivated. Prerequisites: -- You must be an administrator. +- You must have administrator access to the instance. + +```plaintext +POST /users/:id/activate +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | + +Returns: + +- `201 OK` on success. +- `404 User Not Found` if the user cannot be found. +- `403 Forbidden` if the user cannot be activated because they are blocked by an administrator or by LDAP synchronization. + +## Block access to a user + +Blocks a given user account. For more information on banned users, see [Block and unblock users](../administration/moderate_users.md#block-and-unblock-users). + +Prerequisites: + +- You must have administrator access to the instance. ```plaintext POST /users/:id/block ``` -Parameters: +Supported attributes: -| Attribute | Type | Required | Description | -|------------|---------|----------|----------------------| -| `id` | integer | yes | ID of specified user | +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | Returns: @@ -174,42 +175,47 @@ Returns: - A user that is blocked through LDAP. - An internal user. -## Unblock a user +## Unblock access to a user -Unblock the specified user. +Unblocks a given user account that was previously blocked. Prerequisites: -- You must be an administrator. +- You must have administrator access to the instance. ```plaintext POST /users/:id/unblock ``` -Parameters: +Supported attributes: -| Attribute | Type | Required | Description | -|------------|---------|----------|----------------------| -| `id` | integer | yes | ID of specified user | +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | -Returns `201 OK` on success, `404 User Not Found` is user cannot be found or -`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. +Returns: + +- `201 OK` on success. +- `404 User Not Found` if user cannot be found. +- `403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. ## Ban a user -Ban the specified user. +Bans a given user account. For more information on banned users, see [Ban and unban users](../administration/moderate_users.md#ban-and-unban-users). Prerequisites: -- You must be an administrator. +- You must have administrator access to the instance. ```plaintext POST /users/:id/ban ``` -Parameters: +Supported attributes: -- `id` (required) - ID of specified user +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | Returns: @@ -219,15 +225,21 @@ Returns: ## Unban a user -Unban the specified user. Available only for administrator. +Unbans a given user account that was previously banned. + +Prerequisites: + +- You must have administrator access to the instance. ```plaintext POST /users/:id/unban ``` -Parameters: +Supported attributes: -- `id` (required) - ID of specified user +| Attribute | Type | Required | Description | +|------------|---------|----------|--------------------| +| `id` | integer | yes | ID of user account | Returns: diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md index 416898ea7f5..2fe141c74a0 100644 --- a/doc/user/application_security/dependency_list/index.md +++ b/doc/user/application_security/dependency_list/index.md @@ -72,14 +72,17 @@ Details of each dependency are listed, sorted by decreasing severity of vulnerab > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422356) in GitLab 16.7 [with a flag](../../../administration/feature_flags.md) named `group_level_dependencies_filtering`. Disabled by default. > - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/422356) in GitLab 16.10. Feature flag `group_level_dependencies_filtering` removed. -In the group-level dependency list you can filter by: +You can filter the dependency list to focus on only a subset of dependencies. The dependency +list is only available for groups. + +You can filter by: - Project - License To filter the dependency list: -1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project or group. +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. 1. Select **Secure > Dependency list**. 1. Select the filter bar. 1. Select a filter, then from the dropdown list select one or more criteria. diff --git a/doc/user/application_security/policies/vulnerability_management_policy.md b/doc/user/application_security/policies/vulnerability_management_policy.md index cdc355d2be2..ba17a5c2d08 100644 --- a/doc/user/application_security/policies/vulnerability_management_policy.md +++ b/doc/user/application_security/policies/vulnerability_management_policy.md @@ -11,7 +11,6 @@ DETAILS: **Offering:** GitLab.com, Self-managed, GitLab Dedicated > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5708) in GitLab 17.7 [with a flag](../../../administration/feature_flags.md) named `vulnerability_management_policy_type`. Enabled by default. -> - [Enabled on GitLab.com, self-managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/467259) in GitLab 17.7. FLAG: The availability of this feature is controlled by a feature flag. diff --git a/doc/user/gitlab_duo/index.md b/doc/user/gitlab_duo/index.md index 8458d359c3d..d6979767164 100644 --- a/doc/user/gitlab_duo/index.md +++ b/doc/user/gitlab_duo/index.md @@ -24,244 +24,92 @@ GitLab is [transparent](https://handbook.gitlab.com/handbook/values/#transparenc As GitLab Duo features mature, the documentation will be updated to clearly state how and where you can access these features. -## Generally available features +## Working across the entire software development lifecycle -### GitLab Duo Chat +To improve your workflow across the entire software development lifecycle, try these features: -DETAILS: -**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated +- [GitLab Duo Chat](../gitlab_duo_chat/index.md): Write and understand code, get up to speed on the status of projects, + and learn about GitLab by asking your questions in a chat window. + [Watch overview](https://www.youtube.com/watch?v=ZQBAuf-CTAY&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) +- [Self-Hosted Models](../../administration/self_hosted_models/index.md): Host the language models that power AI features in GitLab. + Code Suggestions and Duo Chat are supported. Use GitLab model vendors or self-host a supported language model. +- [GitLab Duo Workflow](../duo_workflow/index.md): Automate tasks and help increase productivity in your development workflow. +- [AI Impact Dashboard](../analytics/ai_impact_analytics.md): Measure the AI effectiveness and impact on SDLC metrics. -- Help you write and understand code faster, get up to speed on the status of projects, - and quickly learn about GitLab by answering your questions in a chat window. -- [Watch overview](https://www.youtube.com/watch?v=ZQBAuf-CTAY&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) -- [View documentation](../gitlab_duo_chat/index.md). +## Planning work -### Discussion Summary +To improve your workflow while planning work, try these features: -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated +- [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation): Generate a more in-depth issue description based on a short summary. +- [Discussion Summary](../discussions/index.md#summarize-issue-discussions-with-duo-chat): Summarize lengthy conversations in an issue. + [Watch overview](https://www.youtube.com/watch?v=IcdxLfTIUgc) -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. +## Authoring code -- Helps everyone get up to speed by summarizing the lengthy conversations in an issue. -- [Watch overview](https://www.youtube.com/watch?v=IcdxLfTIUgc) -- [View documentation](../discussions/index.md#summarize-issue-discussions-with-duo-chat). +To improve your workflow while authoring code, try these features: -### Code Suggestions +- [Code Suggestions](../project/repository/code_suggestions/index.md): Generate code and show suggestions as you type. + [Watch overview](https://youtu.be/ds7SG1wgcVM) +- Code Explanation: Have code explained. View docs for explaining code in: -DETAILS: -**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated - -- Helps you write code more efficiently by generating code and showing suggestions as you type. -- [Watch overview](https://youtu.be/ds7SG1wgcVM) -- [View documentation](../project/repository/code_suggestions/index.md). - -### Code Explanation - -DETAILS: -**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated - -- Helps you understand the selected code by explaining it more clearly. -- View documentation for explaining code in: - [The IDE](../gitlab_duo_chat/examples.md#explain-selected-code). - [A file](../../user/project/repository/code_explain.md). - [A merge request](../../user/project/merge_requests/changes.md#explain-code-in-a-merge-request). +- [Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide): Test your code by generating tests. + [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) +- [Refactor Code](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide): Improve or refactor the selected code. + [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) +- [Fix Code](../gitlab_duo_chat/examples.md#fix-code-in-the-ide): Fix quality problems, like bugs or typos, in the selected code. + [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) +- [GitLab Duo for the CLI](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-for-the-cli): Discover or recall `git` commands. -### Test Generation +## Reviewing code -DETAILS: -**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated +To improve your workflow while reviewing code in merge requests, try these features: -- Helps catch bugs early by generating tests for the selected code. -- [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) -- [View documentation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide). +- [Merge Request Summary](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes): Generate a description based on the code changes. + [Watch overview](https://www.youtube.com/watch?v=CKjkVsfyFd8&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) +- [Code Review](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code): Review proposed code changes. +- [Code Review Summary](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review): Summarize all the comments in a review. + [Watch overview](https://www.youtube.com/watch?v=Bx6Zajyuy9k&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) +- [Merge Commit Message Generation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message): Generate commit messages. -### Refactor Code +## Testing and deploying code -DETAILS: -**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated +To improve your testing and deployment workflow, try these features: -- Improve or refactor the selected code. -- [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) -- [View documentation](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide). +- [Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis): Research the root cause for a CI/CD job failure by analyzing the logs. + [Watch overview](https://www.youtube.com/watch?v=MLjhVbMjFAY&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) -### Fix Code +## Securing code -DETAILS: -**Tier:** Premium with GitLab Duo Pro, Ultimate with GitLab Duo Pro or Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated +To improve your security, try these features: -- Fix quality problems such as bugs or typos in the selected code. -- [Watch overview](https://www.youtube.com/watch?v=zWhwuixUkYU&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) -- [View documentation](../gitlab_duo_chat/examples.md#fix-code-in-the-ide). +- [Vulnerability Explanation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability): Learn more about vulnerabilities, how they can be exploited, and how to fix them. + [Watch overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) +- [Vulnerability Resolution](../application_security/vulnerabilities/index.md#vulnerability-resolution): Generate a merge request that addresses a vulnerability. + [Watch overview](https://www.youtube.com/watch?v=VJmsw_C125E&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) -### GitLab Duo for the CLI +## Summary of all GitLab Duo features -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- `glab duo ask` helps you discover or recall `git` commands when and where you need them. -- [View documentation](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-for-the-cli). - -### Merge Commit Message Generation - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Helps you merge more quickly by generating meaningful commit messages. -- [View documentation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message). - -### Root Cause Analysis - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123692) in GitLab 16.2 as an [experiment](../../policy/development_stages_support.md#experiment) on GitLab.com. -> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/441681) and moved to GitLab Duo Chat in GitLab 17.3. -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Helps you determine the root cause for a CI/CD job failure by analyzing the logs. -- [Watch overview](https://www.youtube.com/watch?v=MLjhVbMjFAY&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) -- [View documentation](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis). - -### Vulnerability Explanation - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Helps you understand vulnerabilities, how they can be exploited, and how to fix them. -- [Watch overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) -- [View documentation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability). - -### AI Impact Dashboard - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Measure the AI effectiveness and impact on SDLC metrics. -- Visualize which metrics improved as a result of investments in AI. -- Compare the performance of teams that are using AI against teams that are not using AI. -- Track the progress of AI adoption. -- [View documentation](../analytics/ai_impact_analytics.md). - -## Beta features - -### Self-Hosted Models - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** Self-managed -**Status:** Beta - -> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/12972) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `ai_custom_model`. Disabled by default. -> - [Enabled on self-managed](https://gitlab.com/groups/gitlab-org/-/epics/15176) in GitLab 17.6. -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. -> - Feature flag `ai_custom_model` removed in GitLab 17.8 - -Host the language models that power AI features in GitLab. Code Suggestions and Duo Chat are supported. - -You can use language model vendors provided by GitLab or fully manage specific language models in your self-hosted environment. - -- Use GitLab model vendors: Connect with default external model providers, like Google Vertex AI or Anthropic, by - using the GitLab-managed AI gateway. -- Host your own models: Deploy and manage your own AI gateway and language models in your infrastructure, - without depending on GitLab-provided external language providers. -- [View documentation](../../administration/self_hosted_models/index.md). - -### Merge Request Summary - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com -**Status:** Beta - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Helps populate a merge request more quickly by generating a description based on the code changes. -- [Watch overview](https://www.youtube.com/watch?v=CKjkVsfyFd8&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) -- [View documentation](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes). - -### Vulnerability Resolution - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com, Self-managed, GitLab Dedicated -**Status:** Beta - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Help resolve a vulnerability by generating a merge request that addresses it. -- [Watch overview](https://www.youtube.com/watch?v=VJmsw_C125E&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW) -- [View documentation](../application_security/vulnerabilities/index.md#vulnerability-resolution). - -## Experimental features - -### Issue Description Generation - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com -**Status:** Experiment - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Helps populate an issue more quickly by generating a more in-depth description, based on a short summary you provide. -- [View documentation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation). - -### Code Review - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com -**Status:** Experiment - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Automated code review of the proposed changes in your merge request. -- [View documentation](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code). - -### Code Review Summary - -DETAILS: -**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial) -**Offering:** GitLab.com -**Status:** Experiment - -> - Changed to require GitLab Duo add-on in GitLab 17.6 and later. - -- Helps make merge request handover to reviewers easier by summarizing all the comments in a merge request review. -- [Watch overview](https://www.youtube.com/watch?v=Bx6Zajyuy9k&list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED) -- [View documentation](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review). - -### GitLab Duo Workflow - -DETAILS: -**Tier:** Ultimate -**Offering:** GitLab.com -**Status:** Experiment - -- Automate tasks and help increase productivity in your development workflow. -- [View documentation](../duo_workflow/index.md). - -## Disable GitLab Duo features for specific groups or projects or an entire instance - -Disable GitLab Duo features by [following these instructions](turn_on_off.md). +| Feature | Tier | Add-on | Offering | Status | +| ------- | ---- | ------ | -------- | ------ | +| [GitLab Duo Chat](../gitlab_duo_chat/index.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Self-Hosted Models](../../administration/self_hosted_models/index.md) | Ultimate | GitLab Duo Enterprise | Self-managed | Beta | +| [GitLab Duo Workflow](../duo_workflow/index.md) | Ultimate | - | GitLab.com | Experiment | +| [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment | +| [Discussion Summary](../discussions/index.md#summarize-issue-discussions-with-duo-chat) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Code Suggestions](../project/repository/code_suggestions/index.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Code Explanation](../../user/project/repository/code_explain.md) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Refactor Code](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Fix Code](../gitlab_duo_chat/examples.md#fix-code-in-the-ide) | Premium, Ultimate | GitLab Duo Pro or Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [GitLab Duo for the CLI](../../editor_extensions/gitlab_cli/index.md#gitlab-duo-for-the-cli) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Merge Request Summary](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) | Ultimate | GitLab Duo Enterprise | GitLab.com | Beta | +| [Code Review](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment | +| [Code Review Summary](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review) | Ultimate | GitLab Duo Enterprise | GitLab.com | Experiment | +| [Merge Commit Message Generation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | General availability | +| [Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | Generally available | +| [Vulnerability Explanation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | Generally available | +| [Vulnerability Resolution](../application_security/vulnerabilities/index.md#vulnerability-resolution) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed, GitLab Dedicated | Beta | +| [AI Impact Dashboard](../analytics/ai_impact_analytics.md) | Ultimate | GitLab Duo Enterprise | GitLab.com, Self-managed | Generally available | diff --git a/doc/user/group/saml_sso/group_sync.md b/doc/user/group/saml_sso/group_sync.md index 425f7fd7fc1..156a6a98cb9 100644 --- a/doc/user/group/saml_sso/group_sync.md +++ b/doc/user/group/saml_sso/group_sync.md @@ -264,7 +264,8 @@ When global group memberships lock is enabled: - Share a project with other groups. NOTE: - This limits the use of groups in other product features such as [adding a group as a Code Owner](../../project/codeowners/index.md#add-a-group-as-a-code-owner). + You cannot set groups or subgroups as [Code Owners](../../project/codeowners/index.md). + The Code Owners feature requires direct group memberships, which are not possible when this lock is enabled. - Invite members to a project created in a group. diff --git a/doc/user/project/codeowners/index.md b/doc/user/project/codeowners/index.md index f7c28ab4bc5..778d5dfd0c9 100644 --- a/doc/user/project/codeowners/index.md +++ b/doc/user/project/codeowners/index.md @@ -143,10 +143,22 @@ file.md @group-x @group-x/subgroup-y ``` NOTE: -You cannot set a member of a group or subgroup as a Code Owner if [Global SAML group memberships lock](../../group/saml_sso/group_sync.md#global-saml-group-memberships-lock) is enabled. +When [Global SAML group memberships lock](../../group/saml_sso/group_sync.md#global-saml-group-memberships-lock) is enabled, you cannot set a group or subgroup as a Code Owner. For more information, see [Incompatibility with Global SAML group memberships lock](#incompatibility-with-global-saml-group-memberships-lock). If you encounter issues, refer to [User not shown as possible approver](troubleshooting.md#user-not-shown-as-possible-approver). +#### Incompatibility with Global SAML group memberships lock + +The Code Owners feature requires direct group memberships to projects. +When the [Global SAML group memberships lock](../../group/saml_sso/group_sync.md#global-saml-group-memberships-lock) is enabled, +it prevents groups from being invited as direct members to projects. This creates an incompatibility between the two features. + +If you enabled Global SAML group memberships lock, you can't use groups or subgroups as Code Owners. +In this case, you have the following options: + +- Use individual users as Code Owners instead of groups. +- If using group-based Code Owners is a higher priority, disable the Global SAML group memberships lock. + #### Group inheritance and eligibility ```mermaid diff --git a/gems/gitlab-active-context/.gitignore b/gems/gitlab-active-context/.gitignore new file mode 100644 index 00000000000..b04a8c840df --- /dev/null +++ b/gems/gitlab-active-context/.gitignore @@ -0,0 +1,11 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ + +# rspec failure tracking +.rspec_status diff --git a/gems/gitlab-active-context/.gitlab-ci.yml b/gems/gitlab-active-context/.gitlab-ci.yml new file mode 100644 index 00000000000..07804f168b9 --- /dev/null +++ b/gems/gitlab-active-context/.gitlab-ci.yml @@ -0,0 +1,4 @@ +include: + - local: gems/gem.gitlab-ci.yml + inputs: + gem_name: "gitlab-active-context" diff --git a/gems/gitlab-active-context/.rspec b/gems/gitlab-active-context/.rspec new file mode 100644 index 00000000000..34c5164d9b5 --- /dev/null +++ b/gems/gitlab-active-context/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/gems/gitlab-active-context/.rubocop.yml b/gems/gitlab-active-context/.rubocop.yml new file mode 100644 index 00000000000..a02c038a3e9 --- /dev/null +++ b/gems/gitlab-active-context/.rubocop.yml @@ -0,0 +1,5 @@ +inherit_from: + - ../config/rubocop.yml + +Gemfile/MissingFeatureCategory: + Enabled: false \ No newline at end of file diff --git a/gems/gitlab-active-context/Gemfile b/gems/gitlab-active-context/Gemfile new file mode 100644 index 00000000000..ea2cd03ed92 --- /dev/null +++ b/gems/gitlab-active-context/Gemfile @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in active_context.gemspec +gemspec + +gem "rake", "~> 13.0" +gem "activesupport" + +group :development, :test do + gem "rspec", "~> 3.0" + gem "byebug" + gem "rubocop" + gem "rubocop-rspec" +end diff --git a/gems/gitlab-active-context/Gemfile.lock b/gems/gitlab-active-context/Gemfile.lock new file mode 100644 index 00000000000..f20e0bf5e27 --- /dev/null +++ b/gems/gitlab-active-context/Gemfile.lock @@ -0,0 +1,209 @@ +PATH + remote: . + specs: + gitlab-active-context (0.0.1) + zeitwerk + +GEM + remote: https://rubygems.org/ + specs: + actionpack (8.0.0.1) + actionview (= 8.0.0.1) + activesupport (= 8.0.0.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (8.0.0.1) + activesupport (= 8.0.0.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (8.0.0.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.2) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.8) + builder (3.3.0) + byebug (11.1.3) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + crack (1.0.0) + bigdecimal + rexml + crass (1.0.6) + date (3.4.1) + diff-lcs (1.5.1) + drb (2.2.1) + erubi (1.13.0) + gitlab-styles (13.0.2) + rubocop (~> 1.68.0) + rubocop-capybara (~> 2.21.0) + rubocop-factory_bot (~> 2.26.1) + rubocop-graphql (~> 1.5.4) + rubocop-performance (~> 1.21.1) + rubocop-rails (~> 2.26.0) + rubocop-rspec (~> 3.0.4) + rubocop-rspec_rails (~> 2.30.0) + hashdiff (1.1.2) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + io-console (0.8.0) + irb (1.14.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.9.0) + language_server-protocol (3.17.0.3) + logger (1.6.2) + loofah (2.23.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mini_portile2 (2.8.8) + minitest (5.25.4) + nokogiri (1.17.1) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.17.1-arm64-darwin) + racc (~> 1.4) + parallel (1.26.3) + parser (3.3.6.0) + ast (~> 2.4.1) + racc + psych (5.2.1) + date + stringio + public_suffix (6.0.1) + racc (1.8.1) + rack (3.1.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.1) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.0.1) + actionpack (= 8.0.0.1) + activesupport (= 8.0.0.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.8.1) + psych (>= 4.0.0) + regexp_parser (2.9.3) + reline (0.5.12) + io-console (~> 0.5) + rexml (3.3.9) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (7.1.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.2) + rubocop (1.68.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.36.2) + parser (>= 3.3.1.0) + rubocop-capybara (2.21.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.26.1) + rubocop (~> 1.61) + rubocop-graphql (1.5.4) + rubocop (>= 1.50, < 2) + rubocop-performance (1.21.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.26.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rspec (3.0.5) + rubocop (~> 1.61) + rubocop-rspec_rails (2.30.0) + rubocop (~> 1.61) + rubocop-rspec (~> 3, >= 3.0.1) + ruby-progressbar (1.13.0) + securerandom (0.4.0) + stringio (3.1.2) + thor (1.3.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.6.0) + uri (1.0.2) + useragent (0.16.11) + webmock (3.24.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.7.1) + +PLATFORMS + arm64-darwin + ruby + +DEPENDENCIES + activesupport + byebug + gitlab-active-context! + gitlab-styles + rake (~> 13.0) + rspec (~> 3.0) + rspec-rails + rubocop + rubocop-rspec + webmock + +BUNDLED WITH + 2.5.23 diff --git a/gems/gitlab-active-context/README.md b/gems/gitlab-active-context/README.md new file mode 100644 index 00000000000..9a08af1c81b --- /dev/null +++ b/gems/gitlab-active-context/README.md @@ -0,0 +1,42 @@ +# GitLab Active Context + +`ActiveContext` is a gem used for interfacing with vector stores like Elasticsearch, OpenSearch and Postgres with PGVector for storing and querying vectors. + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +## Installation + +TODO + +## Usage + +### Configuration + +Add an initializer with the following options: + +1. `enabled`: `true|false`. Defaults to `false` +1. `databases`: Hash containing database configuration options +1. `logger`: Logger. Defaults to `Logger.new($stdout)` + +For example: + +```ruby +ActiveContext.configure do |config| + config.enabled = true + config.logger = ::Gitlab::Elasticsearch::Logger.build + + config.databases = { + es1: { + adapter: 'elasticsearch', + prefix: 'gitlab', + options: ::Gitlab::CurrentSettings.elasticsearch_config + } + } +end +``` + +## Contributing + +TODO diff --git a/gems/gitlab-active-context/Rakefile b/gems/gitlab-active-context/Rakefile new file mode 100644 index 00000000000..cca71754493 --- /dev/null +++ b/gems/gitlab-active-context/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +require "rubocop/rake_task" + +RuboCop::RakeTask.new + +task default: %i[spec rubocop] diff --git a/gems/gitlab-active-context/bin/console b/gems/gitlab-active-context/bin/console new file mode 100755 index 00000000000..03b917ae71f --- /dev/null +++ b/gems/gitlab-active-context/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "active_context" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/gems/gitlab-active-context/bin/setup b/gems/gitlab-active-context/bin/setup new file mode 100755 index 00000000000..dce67d860af --- /dev/null +++ b/gems/gitlab-active-context/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/gems/gitlab-active-context/gitlab-active-context.gemspec b/gems/gitlab-active-context/gitlab-active-context.gemspec new file mode 100644 index 00000000000..fe9c5bb7f7c --- /dev/null +++ b/gems/gitlab-active-context/gitlab-active-context.gemspec @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "lib/active_context/version" + +Gem::Specification.new do |spec| + spec.name = "gitlab-active-context" + spec.version = ActiveContext::VERSION + spec.authors = ["GitLab"] + spec.email = ["gitlab_rubygems@gitlab.com"] + + spec.summary = "Abstraction for indexing and searching vectors" + spec.description = "Abstraction for indexing and searching vectors" + spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-active-context" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.1.0" + + spec.metadata["homepage_uri"] = spec.homepage + + spec.files = Dir['lib/**/*.rb'] + spec.require_paths = ["lib"] + + spec.add_dependency 'zeitwerk' + + spec.add_development_dependency 'gitlab-styles' + spec.add_development_dependency 'rspec-rails' + spec.add_development_dependency 'rubocop-rspec' + spec.add_development_dependency 'webmock' + + spec.metadata["rubygems_mfa_required"] = "true" +end diff --git a/gems/gitlab-active-context/lib/active_context.rb b/gems/gitlab-active-context/lib/active_context.rb new file mode 100644 index 00000000000..f27e283de19 --- /dev/null +++ b/gems/gitlab-active-context/lib/active_context.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "zeitwerk" +loader = Zeitwerk::Loader.for_gem +loader.setup + +module ActiveContext + def self.configure(...) + ActiveContext::Config.configure(...) + end +end diff --git a/gems/gitlab-active-context/lib/active_context/config.rb b/gems/gitlab-active-context/lib/active_context/config.rb new file mode 100644 index 00000000000..60f8e1399e8 --- /dev/null +++ b/gems/gitlab-active-context/lib/active_context/config.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module ActiveContext + class Config + CONFIG = Struct.new(:enabled, :databases, :logger) + + class << self + def configure(&block) + @instance = new(block) + end + + def config + @instance&.config || {} + end + + def enabled? + config.enabled || false + end + + def databases + config.databases || {} + end + + def logger + config.logger || Logger.new($stdout) + end + end + + def initialize(config_block) + @config_block = config_block + end + + def config + struct = CONFIG.new + @config_block.call(struct) + struct + end + end +end diff --git a/gems/gitlab-active-context/lib/active_context/version.rb b/gems/gitlab-active-context/lib/active_context/version.rb new file mode 100644 index 00000000000..a0d650521e4 --- /dev/null +++ b/gems/gitlab-active-context/lib/active_context/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module ActiveContext + VERSION = "0.0.1" +end diff --git a/gems/gitlab-active-context/spec/active_context_spec.rb b/gems/gitlab-active-context/spec/active_context_spec.rb new file mode 100644 index 00000000000..6b9bc738671 --- /dev/null +++ b/gems/gitlab-active-context/spec/active_context_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.describe ActiveContext do + it "has a version number" do + expect(ActiveContext::VERSION).not_to be_nil + end + + describe '.configure' do + let(:elastic) do + { + es1: { + adapter: 'elasticsearch', + prefix: 'gitlab', + options: { elastisearch_url: 'http://localhost:9200' } + } + } + end + + it 'creates a new instance with the provided configuration block' do + ActiveContext.configure do |config| + config.enabled = true + config.databases = elastic + config.logger = ::Logger.new(nil) + end + + expect(ActiveContext::Config.enabled?).to be true + expect(ActiveContext::Config.databases).to eq(elastic) + expect(ActiveContext::Config.logger).to be_a(::Logger) + end + end +end diff --git a/gems/gitlab-active-context/spec/lib/active_context/config_spec.rb b/gems/gitlab-active-context/spec/lib/active_context/config_spec.rb new file mode 100644 index 00000000000..d689a0d60fd --- /dev/null +++ b/gems/gitlab-active-context/spec/lib/active_context/config_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.describe ActiveContext::Config do + let(:logger) { ::Logger.new(nil) } + let(:elastic) do + { + es1: { + adapter: 'elasticsearch', + prefix: 'gitlab', + options: { elastisearch_url: 'http://localhost:9200' } + } + } + end + + before do + described_class.configure do |config| + config.enabled = nil + end + end + + describe '.configure' do + it 'creates a new instance with the provided configuration block' do + described_class.configure do |config| + config.enabled = true + config.databases = elastic + config.logger = logger + end + + expect(described_class.enabled?).to be true + expect(described_class.databases).to eq(elastic) + expect(described_class.logger).to eq(logger) + end + end + + describe '.enabled?' do + context 'when enabled is not set' do + it 'returns false' do + expect(described_class.enabled?).to be false + end + end + + context 'when enabled is set to true' do + before do + described_class.configure do |config| + config.enabled = true + end + end + + it 'returns true' do + expect(described_class.enabled?).to be true + end + end + end + + describe '.databases' do + context 'when databases are not set' do + it 'returns an empty hash' do + expect(described_class.databases).to eq({}) + end + end + + context 'when databases are set' do + before do + described_class.configure do |config| + config.databases = elastic + end + end + + it 'returns the configured databases' do + expect(described_class.databases).to eq(elastic) + end + end + end + + describe '.logger' do + context 'when logger is not set' do + it 'returns a default stdout logger' do + expect(described_class.logger).to be_a(Logger) + end + end + + context 'when logger is set' do + before do + described_class.configure do |config| + config.logger = logger + end + end + + it 'returns the configured logger' do + expect(described_class.logger).to eq(logger) + end + end + end +end diff --git a/gems/gitlab-active-context/spec/spec_helper.rb b/gems/gitlab-active-context/spec/spec_helper.rb new file mode 100644 index 00000000000..d32874b9f34 --- /dev/null +++ b/gems/gitlab-active-context/spec/spec_helper.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "active_context" +require 'logger' + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cef772a66a8..ae9637cae96 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6088,6 +6088,12 @@ msgstr "" msgid "An error occured fetching failed jobs count" msgstr "" +msgid "An error occured when subscribing to the comment temperature updates. Please try again." +msgstr "" + +msgid "An error occured while parsing comment temperature. Please try again." +msgstr "" + msgid "An error occurred creating the new branch." msgstr "" @@ -13783,6 +13789,9 @@ msgstr "" msgid "Comment added to the timeline." msgstr "" +msgid "Comment anyway" +msgstr "" + msgid "Comment could not be submitted. Please check your network connection and try again." msgstr "" @@ -23377,6 +23386,9 @@ msgstr "" msgid "Failed to mark this issue as a duplicate because referenced issue was not found." msgstr "" +msgid "Failed to measure the comment temperature. Please try again." +msgstr "" + msgid "Failed to move this issue because label was not found." msgstr "" @@ -42327,6 +42339,9 @@ msgstr "" msgid "Proceed" msgstr "" +msgid "Proceed with caution." +msgstr "" + msgid "Product Analytics" msgstr "" @@ -59857,6 +59872,9 @@ msgstr "" msgid "Updated date" msgstr "" +msgid "Updated. Check again" +msgstr "" + msgid "Updating" msgstr "" @@ -62293,6 +62311,9 @@ msgstr "" msgid "We found your token in a public project and have automatically revoked it to protect your account." msgstr "" +msgid "We have detected that your message might be composed against %{linkStart}our guidelines%{linkEnd}. Please review our findings below:" +msgstr "" + msgid "We heard back from your device. You have been authenticated." msgstr "" @@ -63274,6 +63295,9 @@ msgstr "" msgid "WorkItem|Add" msgstr "" +msgid "WorkItem|Add %{linkStart}description templates%{linkEnd} to help your contributors communicate effectively!" +msgstr "" + msgid "WorkItem|Add %{workItemType}" msgstr "" @@ -63301,6 +63325,12 @@ msgstr "" msgid "WorkItem|Ancestor not available" msgstr "" +msgid "WorkItem|Apply template" +msgstr "" + +msgid "WorkItem|Applying a template will replace the existing description. Any changes you have made will be lost." +msgstr "" + msgid "WorkItem|Apricot" msgstr "" @@ -63352,6 +63382,9 @@ msgstr "" msgid "WorkItem|Child removed" msgstr "" +msgid "WorkItem|Choose a template" +msgstr "" + msgid "WorkItem|Clear" msgstr "" @@ -63679,6 +63712,9 @@ msgstr "" msgid "WorkItem|Select parent" msgstr "" +msgid "WorkItem|Select template" +msgstr "" + msgid "WorkItem|Select type" msgstr "" @@ -63856,6 +63892,9 @@ msgstr "" msgid "WorkItem|Type changed." msgstr "" +msgid "WorkItem|Unable to find selected template." +msgstr "" + msgid "WorkItem|Undo" msgstr "" diff --git a/spec/controllers/concerns/autocomplete_sources/expires_in_spec.rb b/spec/controllers/concerns/autocomplete_sources/expires_in_spec.rb index 6eb669a4a83..097dddba135 100644 --- a/spec/controllers/concerns/autocomplete_sources/expires_in_spec.rb +++ b/spec/controllers/concerns/autocomplete_sources/expires_in_spec.rb @@ -48,19 +48,6 @@ RSpec.describe AutocompleteSources::ExpiresIn, feature_category: :global_search expect(response.headers['Cache-Control']).to eq(expected_cache_control) end end - - context "when action is #{action} with feature flag disabled" do - before do - stub_feature_flags("cache_autocomplete_sources_#{action}" => false) - end - - it 'does not set cache-control' do - get action - - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Cache-Control']).to be_nil - end - end end context 'when action is not in AUTOCOMPLETE_CACHED_ACTIONS' do diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index 0cc6bd05089..ff907de0bb1 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -198,11 +198,13 @@ RSpec.describe TodosFinder, feature_category: :notifications do let!(:todo2) { create(:todo, user: user, group: group, target: issue, state: :done, author: banned_user) } let!(:todo3) { create(:todo, user: user, group: group, target: issue, state: :pending) } let!(:todo4) { create(:todo, user: user, group: group, target: issue, state: :pending, author: banned_user) } + let!(:todo5) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.from_now) } + let!(:todo6) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.ago) } it 'returns the expected items when no state is provided' do todos = finder.new(user, {}).execute - expect(todos).to match_array([todo3]) + expect(todos).to match_array([todo3, todo6]) end it 'returns the expected items when a state is provided' do @@ -214,7 +216,31 @@ RSpec.describe TodosFinder, feature_category: :notifications do it 'returns the expected items when multiple states are provided' do todos = finder.new(user, { state: [:pending, :done] }).execute - expect(todos).to match_array([todo1, todo2, todo3]) + expect(todos).to match_array([todo1, todo2, todo3, todo5, todo6]) + end + end + + context 'by snoozed state' do + let_it_be(:todo1) { create(:todo, user: user, group: group, target: issue, state: :pending) } + let_it_be(:todo2) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.from_now) } + let_it_be(:todo3) { create(:todo, user: user, group: group, target: issue, state: :pending, snoozed_until: 1.hour.ago) } + + it 'returns the snoozed todos only' do + todos = finder.new(user, { is_snoozed: true }).execute + + expect(todos).to match_array([todo2]) + end + + context 'when todos_snoozing feature flag is disabled' do + before do + stub_feature_flags(todos_snoozing: false) + end + + it 'returns all pending todos' do + todos = finder.new(user, { is_snoozed: true }).execute + + expect(todos).to match_array([todo1, todo2, todo3]) + end end end diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js index dad6916b742..1c19fbb5705 100644 --- a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js +++ b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js @@ -76,7 +76,7 @@ describe('GroupsListItem', () => { it('renders subgroup count', () => { createComponent(); - expect(wrapper.findByTestId('subgroups-count').props()).toEqual({ + expect(wrapper.findByTestId('subgroups-count').props()).toMatchObject({ tooltipText: 'Subgroups', iconName: 'subgroup', stat: group.descendantGroupsCount.toString(), @@ -86,7 +86,7 @@ describe('GroupsListItem', () => { it('renders projects count', () => { createComponent(); - expect(wrapper.findByTestId('projects-count').props()).toEqual({ + expect(wrapper.findByTestId('projects-count').props()).toMatchObject({ tooltipText: 'Projects', iconName: 'project', stat: group.projectsCount.toString(), @@ -96,7 +96,7 @@ describe('GroupsListItem', () => { it('renders members count', () => { createComponent(); - expect(wrapper.findByTestId('members-count').props()).toEqual({ + expect(wrapper.findByTestId('members-count').props()).toMatchObject({ tooltipText: 'Direct members', iconName: 'users', stat: group.groupMembersCount.toString(), diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js index 1960e29a5e2..faf7fcf8658 100644 --- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js @@ -47,16 +47,20 @@ jest.mock('~/api/projects_api'); describe('ProjectsListItem', () => { let wrapper; - const [{ permissions, ...project }] = convertObjectPropsToCamelCase(projects, { deep: true }); + const [{ permissions, ...mockProject }] = convertObjectPropsToCamelCase(projects, { deep: true }); + + const project = { + ...mockProject, + accessLevel: { + integerValue: permissions.projectAccess.accessLevel, + }, + avatarUrl: 'avatar.jpg', + avatarLabel: mockProject.nameWithNamespace, + isForked: false, + }; const defaultPropsData = { - project: { - ...project, - accessLevel: { - integerValue: permissions.projectAccess.accessLevel, - }, - avatarUrl: 'avatar.jpg', - }, + project, }; const createComponent = ({ propsData = {} } = {}) => { @@ -69,9 +73,9 @@ describe('ProjectsListItem', () => { }; const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); - const findMergeRequestsLink = () => wrapper.findByTestId('mrs-btn'); - const findIssuesLink = () => wrapper.findByTestId('issues-btn'); - const findForksLink = () => wrapper.findByTestId('forks-btn'); + const findMergeRequestsStat = () => wrapper.findByTestId('mrs-btn'); + const findIssuesStat = () => wrapper.findByTestId('issues-btn'); + const findForksStat = () => wrapper.findByTestId('forks-btn'); const findProjectTopics = () => wrapper.findByTestId('project-topics'); const findPopover = () => findProjectTopics().findComponent(GlPopover); const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon); @@ -97,13 +101,13 @@ describe('ProjectsListItem', () => { const avatarLabeled = findAvatarLabeled(); expect(avatarLabeled.props()).toMatchObject({ - label: project.name, + label: project.nameWithNamespace, labelLink: project.webUrl, }); expect(avatarLabeled.attributes()).toMatchObject({ 'entity-id': project.id.toString(), - 'entity-name': project.name, + 'entity-name': project.nameWithNamespace, src: defaultPropsData.project.avatarUrl, shape: 'rect', }); @@ -143,7 +147,7 @@ describe('ProjectsListItem', () => { describe('when access level is not available', () => { beforeEach(() => { createComponent({ - propsData: { project }, + propsData: { project: { ...project, accessLevel: null } }, }); }); @@ -175,13 +179,12 @@ describe('ProjectsListItem', () => { it('renders stars count', () => { createComponent(); - const starsLink = wrapper.findByTestId('stars-btn'); - const tooltip = getBinding(starsLink.element, 'gl-tooltip'); - - expect(tooltip.value).toBe(ProjectsListItem.i18n.stars); - expect(starsLink.attributes('href')).toBe(`${project.webUrl}/-/starrers`); - expect(starsLink.text()).toBe(project.starCount.toString()); - expect(starsLink.findComponent(GlIcon).props('name')).toBe('star-o'); + expect(wrapper.findByTestId('stars-btn').props()).toEqual({ + href: `${project.webUrl}/-/starrers`, + tooltipText: 'Stars', + iconName: 'star-o', + stat: project.starCount.toString(), + }); }); describe.each` @@ -233,13 +236,12 @@ describe('ProjectsListItem', () => { }, }); - const mergeRequestsLink = findMergeRequestsLink(); - const tooltip = getBinding(mergeRequestsLink.element, 'gl-tooltip'); - - expect(tooltip.value).toBe(ProjectsListItem.i18n.mergeRequests); - expect(mergeRequestsLink.attributes('href')).toBe(`${project.webUrl}/-/merge_requests`); - expect(mergeRequestsLink.text()).toBe('5'); - expect(mergeRequestsLink.findComponent(GlIcon).props('name')).toBe('merge-request'); + expect(findMergeRequestsStat().props()).toEqual({ + href: `${project.webUrl}/-/merge_requests`, + tooltipText: 'Merge requests', + iconName: 'merge-request', + stat: '5', + }); }); }); @@ -254,7 +256,7 @@ describe('ProjectsListItem', () => { }, }); - expect(findMergeRequestsLink().exists()).toBe(false); + expect(findMergeRequestsStat().exists()).toBe(false); }); }); @@ -262,13 +264,12 @@ describe('ProjectsListItem', () => { it('renders issues count', () => { createComponent(); - const issuesLink = findIssuesLink(); - const tooltip = getBinding(issuesLink.element, 'gl-tooltip'); - - expect(tooltip.value).toBe(ProjectsListItem.i18n.issues); - expect(issuesLink.attributes('href')).toBe(`${project.webUrl}/-/issues`); - expect(issuesLink.text()).toBe(project.openIssuesCount.toString()); - expect(issuesLink.findComponent(GlIcon).props('name')).toBe('issues'); + expect(findIssuesStat().props()).toEqual({ + href: `${project.webUrl}/-/issues`, + tooltipText: 'Issues', + iconName: 'issues', + stat: project.openIssuesCount.toString(), + }); }); }); @@ -283,7 +284,7 @@ describe('ProjectsListItem', () => { }, }); - expect(findIssuesLink().exists()).toBe(false); + expect(findIssuesStat().exists()).toBe(false); }); }); @@ -291,13 +292,12 @@ describe('ProjectsListItem', () => { it('renders forks count', () => { createComponent(); - const forksLink = findForksLink(); - const tooltip = getBinding(forksLink.element, 'gl-tooltip'); - - expect(tooltip.value).toBe(ProjectsListItem.i18n.forks); - expect(forksLink.attributes('href')).toBe(`${project.webUrl}/-/forks`); - expect(forksLink.text()).toBe(project.openIssuesCount.toString()); - expect(forksLink.findComponent(GlIcon).props('name')).toBe('fork'); + expect(findForksStat().props()).toEqual({ + href: `${project.webUrl}/-/forks`, + tooltipText: 'Forks', + iconName: 'fork', + stat: project.forksCount.toString(), + }); }); }); @@ -320,7 +320,7 @@ describe('ProjectsListItem', () => { }, }); - expect(findForksLink().exists()).toBe(false); + expect(findForksStat().exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js b/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js new file mode 100644 index 00000000000..96b2ea35aa7 --- /dev/null +++ b/spec/frontend/vue_shared/components/resource_lists/list_item_description_spec.js @@ -0,0 +1,25 @@ +import { GlTruncateText } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ListItemDescription from '~/vue_shared/components/resource_lists/list_item_description.vue'; + +describe('ListItemDescription', () => { + let wrapper; + + const defaultPropsData = { + descriptionHtml: '

Dolorem dolorem omnis impedit cupiditate pariatur officia velit.

', + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = shallowMountExtended(ListItemDescription, { + propsData: { ...defaultPropsData, ...propsData }, + }); + }; + + it('renders description', () => { + createComponent(); + + expect(wrapper.findComponent(GlTruncateText).element.firstChild.innerHTML).toBe( + defaultPropsData.descriptionHtml, + ); + }); +}); diff --git a/spec/frontend/vue_shared/components/resource_lists/list_item_spec.js b/spec/frontend/vue_shared/components/resource_lists/list_item_spec.js index 559e777360c..257de50d535 100644 --- a/spec/frontend/vue_shared/components/resource_lists/list_item_spec.js +++ b/spec/frontend/vue_shared/components/resource_lists/list_item_spec.js @@ -1,6 +1,7 @@ import { GlAvatarLabeled, GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ListItem from '~/vue_shared/components/resource_lists/list_item.vue'; +import ListItemDescription from '~/vue_shared/components/resource_lists/list_item_description.vue'; import ListActions from '~/vue_shared/components/list_actions/list_actions.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants'; @@ -27,20 +28,21 @@ describe('ListItem', () => { resource, }; - const createComponent = ({ propsData = {}, stubs = {} } = {}) => { + const createComponent = ({ propsData = {}, stubs = {}, scopedSlots = {} } = {}) => { wrapper = shallowMountExtended(ListItem, { propsData: { ...defaultPropsData, ...propsData }, scopedSlots: { 'avatar-meta': '
', stats: '
', footer: '
', + ...scopedSlots, }, stubs, }); }; const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); - const findGroupDescription = () => wrapper.findByTestId('description'); + const findDescription = () => wrapper.findComponent(ListItemDescription); const findListActions = () => wrapper.findComponent(ListActions); const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); @@ -80,35 +82,47 @@ describe('ListItem', () => { expect(wrapper.findByTestId('footer').exists()).toBe(true); }); - describe('when resource has a description', () => { - it('renders description', () => { - const descriptionHtml = '

Foo bar

'; - + describe('when avatar-default slot is provided', () => { + beforeEach(() => { createComponent({ - propsData: { - resource: { - ...resource, - descriptionHtml, - }, - }, + scopedSlots: { 'avatar-default': '
' }, }); + }); - expect(findGroupDescription().element.innerHTML).toBe(descriptionHtml); + it('renders slot instead of description', () => { + expect(wrapper.findByTestId('avatar-default').exists()).toBe(true); + expect(findDescription().exists()).toBe(false); }); }); - describe('when resource does not have a description', () => { - it('does not render description', () => { - createComponent({ - propsData: { - resource: { - ...resource, - descriptionHtml: null, - }, - }, + describe('when avatar-default slot is not provided', () => { + describe('when resource has a description', () => { + beforeEach(() => { + createComponent(); }); - expect(findGroupDescription().exists()).toBe(false); + it('renders description', () => { + expect(findDescription().props('descriptionHtml')).toBe( + defaultPropsData.resource.descriptionHtml, + ); + }); + }); + + describe('when resource does not have a description', () => { + beforeEach(() => { + createComponent({ + propsData: { + resource: { + ...resource, + descriptionHtml: null, + }, + }, + }); + }); + + it('does not render description', () => { + expect(findDescription().exists()).toBe(false); + }); }); }); @@ -130,17 +144,37 @@ describe('ListItem', () => { }); }); - describe('when resource has available actions', () => { - it('displays actions dropdown', () => { - createComponent({ - propsData: { + describe('when actions prop is passed', () => { + describe('when resource has available actions', () => { + it('displays actions dropdown', () => { + createComponent({ + propsData: { + actions, + }, + }); + + expect(findListActions().props()).toMatchObject({ actions, - }, + availableActions: resource.availableActions, + }); + }); + }); + + describe('when resource does not have available actions', () => { + beforeEach(() => { + createComponent({ + propsData: { + actions, + resource: { + ...resource, + availableActions: [], + }, + }, + }); }); - expect(findListActions().props()).toMatchObject({ - actions, - availableActions: resource.availableActions, + it('does not display actions dropdown', () => { + expect(findListActions().exists()).toBe(false); }); }); }); @@ -155,12 +189,20 @@ describe('ListItem', () => { }); }); - describe('when resource does not have available actions', () => { + describe('when actions slot is provided', () => { beforeEach(() => { - createComponent(); + createComponent({ + propsData: { + actions, + }, + scopedSlots: { + actions: '
', + }, + }); }); - it('does not display actions dropdown', () => { + it('renders slot instead of list actions component', () => { + expect(wrapper.findByTestId('actions').exists()).toBe(true); expect(findListActions().exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/components/resource_lists/list_item_stat_spec.js b/spec/frontend/vue_shared/components/resource_lists/list_item_stat_spec.js index 4d3f0d713cd..6b833e2339c 100644 --- a/spec/frontend/vue_shared/components/resource_lists/list_item_stat_spec.js +++ b/spec/frontend/vue_shared/components/resource_lists/list_item_stat_spec.js @@ -1,4 +1,4 @@ -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlLink } from '@gitlab/ui'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ListItemStat from '~/vue_shared/components/resource_lists/list_item_stat.vue'; @@ -21,13 +21,26 @@ describe('ListItemStat', () => { }); }; - it('renders stat with icon and tooltip', () => { + it('renders stat in div with icon and tooltip', () => { createComponent(); const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(wrapper.element.tagName).toBe('DIV'); expect(wrapper.text()).toBe(defaultPropsData.stat); expect(tooltip.value).toBe(defaultPropsData.tooltipText); expect(wrapper.findComponent(GlIcon).props('name')).toBe(defaultPropsData.iconName); }); + + describe('when href prop is passed', () => { + const href = 'http://gdk.test:3000/foo/bar/-/forks`'; + + beforeEach(() => { + createComponent({ propsData: { href } }); + }); + + it('renders `GlLink` component', () => { + expect(wrapper.findComponent(GlLink).attributes('href')).toBe(href); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 1f8d49da9fc..b52d0219543 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -1,9 +1,9 @@ import { GlAlert, GlForm } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import EditedAt from '~/issues/show/components/edited.vue'; import { updateDraft } from '~/lib/utils/autosave'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; @@ -11,8 +11,10 @@ import { ENTER_KEY } from '~/lib/utils/keys'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import WorkItemDescription from '~/work_items/components/work_item_description.vue'; import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue'; +import WorkItemDescriptionTemplatesListbox from '~/work_items/components/work_item_description_template_listbox.vue'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; +import workItemDescriptionTemplateQuery from '~/work_items/graphql/work_item_description_template.query.graphql'; import { autocompleteDataSources, markdownPreviewPath, newWorkItemId } from '~/work_items/utils'; import { updateWorkItemMutationResponse, @@ -34,14 +36,32 @@ describe('WorkItemDescription', () => { const findRenderedDescription = () => wrapper.findComponent(WorkItemDescriptionRendered); const findEditedAt = () => wrapper.findComponent(EditedAt); const findConflictsAlert = () => wrapper.findComponent(GlAlert); - const findConflictedDescription = () => wrapper.find('[data-testid="conflicted-description"]'); + const findConflictedDescription = () => wrapper.findByTestId('conflicted-description'); + const findDescriptionTemplateListbox = () => + wrapper.findComponent(WorkItemDescriptionTemplatesListbox); + const findDescriptionTemplateWarning = () => wrapper.findByTestId('description-template-warning'); + const findDescriptionTemplateWarningButton = (type) => + findDescriptionTemplateWarning().find(`[data-testid="template-${type}"]`); const editDescription = (newText) => findMarkdownEditor().vm.$emit('input', newText); - const findCancelButton = () => wrapper.find('[data-testid="cancel"]'); - const findSubmitButton = () => wrapper.find('[data-testid="save-description"]'); + const findCancelButton = () => wrapper.findByTestId('cancel'); + const findSubmitButton = () => wrapper.findByTestId('save-description'); const clickCancel = () => findForm().vm.$emit('reset', new Event('reset')); + const successfulTemplateHandler = jest.fn().mockResolvedValue({ + data: { + namespace: { + id: 'gid://gitlab/Namespaces::ProjectNamespace/34', + workItemDescriptionTemplates: { + __typename: 'WorkItemDescriptionTemplateConnection', + nodes: [{ name: 'example', content: 'A template' }], + }, + __typename: 'Namespace', + }, + }, + }); + const createComponent = async ({ mutationHandler = mutationSuccessHandler, canUpdate = true, @@ -55,11 +75,13 @@ describe('WorkItemDescription', () => { workItemTypeName = workItemQueryResponse.data.workItem.workItemType.name, editMode = false, showButtonsBelowField, + descriptionTemplateHandler = successfulTemplateHandler, } = {}) => { - wrapper = shallowMount(WorkItemDescription, { + wrapper = shallowMountExtended(WorkItemDescription, { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemResponseHandler], [updateWorkItemMutation, mutationHandler], + [workItemDescriptionTemplateQuery, descriptionTemplateHandler], ]), propsData: { fullPath: 'test-project-path', @@ -72,6 +94,9 @@ describe('WorkItemDescription', () => { }, provide: { isGroup, + glFeatures: { + workItemsAlpha: true, + }, }, stubs: { GlAlert, @@ -269,6 +294,73 @@ describe('WorkItemDescription', () => { expect(wrapper.emitted('updateWorkItem')).toEqual([[{ clearDraft: expect.any(Function) }]]); }); + describe('description templates', () => { + it('displays the description template selection listbox', async () => { + await createComponent({ isEditing: true }); + expect(findDescriptionTemplateListbox().exists()).toBe(true); + }); + + describe('selecting a template successfully', () => { + beforeEach(async () => { + await createComponent({ + isEditing: true, + workItemId: newWorkItemId(workItemQueryResponse.data.workItem.workItemType.name), + }); + findDescriptionTemplateListbox().vm.$emit('selectTemplate', 'example'); + await nextTick(); + await waitForPromises(); + }); + + it('queries for the template content when a template is selected', () => { + expect(successfulTemplateHandler).toHaveBeenCalledWith({ + name: 'example', + fullPath: 'test-project-path', + }); + }); + + it('displays a warning when a description template is selected', () => { + expect(findDescriptionTemplateWarning().exists()).toBe(true); + expect(findDescriptionTemplateWarningButton('cancel').exists()).toBe(true); + expect(findDescriptionTemplateWarningButton('apply').exists()).toBe(true); + }); + + it('hides the warning when the cancel button is clicked', async () => { + expect(findDescriptionTemplateWarning().exists()).toBe(true); + findDescriptionTemplateWarningButton('cancel').vm.$emit('click'); + await nextTick(); + expect(findDescriptionTemplateWarning().exists()).toBe(false); + }); + + it('applies the template when the apply button is clicked', async () => { + findDescriptionTemplateWarningButton('apply').vm.$emit('click'); + await nextTick(); + expect(findMarkdownEditor().props('value')).toBe('A template'); + }); + + it('hides the warning when the template is applied', async () => { + findDescriptionTemplateWarningButton('apply').vm.$emit('click'); + await nextTick(); + expect(findDescriptionTemplateWarning().exists()).toBe(false); + }); + }); + + describe('selecting a template unsuccessfully', () => { + beforeEach(async () => { + await createComponent({ + isEditing: true, + descriptionTemplateHandler: jest.fn().mockRejectedValue(new Error()), + }); + findDescriptionTemplateListbox().vm.$emit('selectTemplate', 'example'); + await nextTick(); + await waitForPromises(); + }); + + it('emits an error event', () => { + expect(wrapper.emitted('error')).toEqual([['Unable to find selected template.']]); + }); + }); + }); + describe('when description has conflicts', () => { beforeEach(async () => { const workItemResponseHandler = jest diff --git a/spec/frontend/work_items/components/work_item_description_template_listbox_spec.js b/spec/frontend/work_items/components/work_item_description_template_listbox_spec.js new file mode 100644 index 00000000000..50f96f04132 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_description_template_listbox_spec.js @@ -0,0 +1,163 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlCollapsibleListbox, GlSkeletonLoader, GlLink } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import WorkItemDescriptionTemplateListbox from '~/work_items/components/work_item_description_template_listbox.vue'; +import descriptionTemplatesListQuery from '~/work_items/graphql/work_item_description_templates_list.query.graphql'; + +Vue.use(VueApollo); + +const mockTemplatesList = [ + { name: 'template 1', __typename: 'WorkItemDescriptionTemplate' }, + { name: 'template 2', __typename: 'WorkItemDescriptionTemplate' }, + { name: 'template 3', __typename: 'WorkItemDescriptionTemplate' }, + { name: 'template 4', __typename: 'WorkItemDescriptionTemplate' }, +]; + +const mockDescriptionTemplatesResult = { + data: { + namespace: { + __typename: 'Namespace', + id: 'gid://gitlab/Project/1', + workItemDescriptionTemplates: { + __typename: 'WorkItemDescriptionTemplateConnection', + nodes: mockTemplatesList, + }, + }, + }, +}; + +const mockEmptyDescriptionTemplatesResult = { + data: { + namespace: { + __typename: 'Namespace', + id: 'gid://gitlab/Project/1', + workItemDescriptionTemplates: { + __typename: 'WorkItemDescriptionTemplateConnection', + nodes: [], + }, + }, + }, +}; + +describe('WorkItemDescriptionTemplateListbox', () => { + let wrapper; + let handler; + + const createComponent = ({ template, templatesResult = mockDescriptionTemplatesResult } = {}) => { + handler = jest.fn().mockResolvedValue(templatesResult); + wrapper = mountExtended(WorkItemDescriptionTemplateListbox, { + apolloProvider: createMockApollo([[descriptionTemplatesListQuery, handler]]), + propsData: { + fullPath: 'gitlab-org/gitlab', + template, + }, + }); + }; + + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findTemplateMessage = () => wrapper.findByTestId('template-message'); + const findTemplateMessageLink = () => wrapper.findComponent(GlLink); + + it('displays a skeleton loader', () => { + createComponent(); + expect(findSkeletonLoader().exists()).toBe(true); + }); + + describe('when the templates have been fetched', () => { + it('does not display a skeleton loader', async () => { + createComponent(); + await waitForPromises(); + expect(findSkeletonLoader().exists()).toBe(false); + }); + describe('and there are templates to display', () => { + describe('and there is no template already selected', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + }); + + it('renders a collapsible-listbox component', () => { + expect(findListbox().exists()).toBe(true); + }); + + it('displays "Choose a template" by default', () => { + expect(findListbox().text()).toContain('Choose a template'); + }); + + it('displays a header in the listbox that says "Select template"', () => { + expect(findListbox().text()).toContain('Select template'); + }); + }); + + describe('when there is already a template selected', () => { + beforeEach(async () => { + createComponent({ + template: mockTemplatesList[0].name, + }); + await waitForPromises(); + }); + + it('displays the template name in the listbox', () => { + expect(findListbox().text()).toContain(mockTemplatesList[0].name); + }); + }); + + describe('when the listbox is opened', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + findListbox().vm.$emit('shown'); + await nextTick(); + }); + + it('displays a list of templates', () => { + const text = findListbox().text(); + for (const template of mockTemplatesList) { + expect(text).toContain(template.name); + } + }); + + it('allows searching to narrow down results', async () => { + // only matches 'template 4' + findListbox().vm.$emit('search', '4'); + await nextTick(); + expect(findListbox().props('items')).toHaveLength(1); + }); + }); + + describe('when a template is selected from the list', () => { + beforeEach(async () => { + createComponent(); + await waitForPromises(); + findListbox().vm.$emit('shown'); + findListbox().vm.$emit('select', mockTemplatesList[0]); + }); + + it('emits the selected template', () => { + expect(wrapper.emitted('selectTemplate')).toEqual([[mockTemplatesList[0]]]); + }); + }); + }); + + describe('but there are no templates to display', () => { + beforeEach(async () => { + createComponent({ templatesResult: mockEmptyDescriptionTemplatesResult }); + await waitForPromises(); + }); + it('displays a message about adding description templates', () => { + expect(findTemplateMessage().text()).toMatchInterpolatedText( + 'Add description templates to help your contributors communicate effectively!', + ); + }); + it('displays a link to the docs', () => { + expect(findTemplateMessageLink().attributes('href')).toBe( + '/help/user/project/description_templates', + ); + }); + }); + }); +}); diff --git a/spec/graphql/resolvers/todos_resolver_spec.rb b/spec/graphql/resolvers/todos_resolver_spec.rb index e9651c48c9d..78788e2cccc 100644 --- a/spec/graphql/resolvers/todos_resolver_spec.rb +++ b/spec/graphql/resolvers/todos_resolver_spec.rb @@ -120,6 +120,32 @@ RSpec.describe Resolvers::TodosResolver, feature_category: :notifications do expect(todos).to contain_exactly(todo4, todo5) end + + context 'when filtering by is_snoozed' do + let_it_be(:new_user) { create(:user) } + + let_it_be(:todo1) { create(:todo, user: new_user, project: project) } + let_it_be(:todo2) { create(:todo, user: new_user, snoozed_until: 1.month.from_now, project: project) } + let_it_be(:todo3) { create(:todo, user: new_user, snoozed_until: 1.hour.from_now, project: project) } + + it 'only returns snoozed todos' do + todos = resolve_todos(args: { is_snoozed: true, sort: 'CREATED_ASC' }, context: { current_user: new_user }) + + expect(todos.items).to eq([todo2, todo3]) + end + + context 'when todos_snoozing feature flag is disabled' do + before do + stub_feature_flags(todos_snoozing: false) + end + + it 'ignores the is_snoozed filter' do + todos = resolve_todos(args: { is_snoozed: true, sort: 'CREATED_ASC' }, context: { current_user: new_user }) + + expect(todos.items).to eq([todo1, todo2, todo3]) + end + end + end end context 'when sort is provided' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 3939f3a9b57..8e1bccf92a1 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -276,6 +276,7 @@ merge_requests: - applicable_post_merge_approval_rules - requested_changes - scan_result_policy_reads_through_violations +- security_policies_through_violations - scan_result_policy_reads_through_approval_rules - running_scan_result_policy_violations - failed_scan_result_policy_violations diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb index 16332fbe5a3..489a8183e6d 100644 --- a/spec/models/user_detail_spec.rb +++ b/spec/models/user_detail_spec.rb @@ -16,6 +16,7 @@ RSpec.describe UserDetail, feature_category: :system_access do let(:step_url) { '_some_string_' } let(:email_opt_in) { true } let(:registration_type) { 'free' } + let(:registration_objective) { 0 } let(:glm_source) { 'glm_source' } let(:glm_content) { 'glm_content' } let(:joining_project) { true } @@ -29,7 +30,8 @@ RSpec.describe UserDetail, feature_category: :system_access do glm_source: glm_source, glm_content: glm_content, joining_project: joining_project, - role: role + role: role, + registration_objective: registration_objective } end @@ -99,6 +101,34 @@ RSpec.describe UserDetail, feature_category: :system_access do end end + context 'for registration_objective' do + let(:onboarding_status) do + { + registration_objective: registration_objective + } + end + + it { is_expected.to allow_value(onboarding_status).for(:onboarding_status) } + + context "when 'registration_objective' is invalid" do + let(:registration_objective) { [] } + + it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) } + end + + context "when 'registration_objective' is invalid integer" do + let(:registration_objective) { 10 } + + it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) } + end + + context "when 'registration_objective' is invalid string" do + let(:registration_objective) { 'long-string-not-listed' } + + it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) } + end + end + context 'for glm_content' do let(:onboarding_status) do { diff --git a/spec/services/award_emojis/copy_service_spec.rb b/spec/services/award_emojis/copy_service_spec.rb index c7b9ea7e1c5..98c98b8b861 100644 --- a/spec/services/award_emojis/copy_service_spec.rb +++ b/spec/services/award_emojis/copy_service_spec.rb @@ -3,12 +3,14 @@ require 'spec_helper' RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do + let_it_be(:project) { create(:project, :in_group) } + let_it_be(:custom_emoji_in_origin_namespace) { create(:custom_emoji, name: 'partyparrot', namespace: project.group) } let_it_be(:from_awardable) do - create( - :issue, + create(:issue, project: project, award_emoji: [ build(:award_emoji, name: AwardEmoji::THUMBS_UP), - build(:award_emoji, name: AwardEmoji::THUMBS_DOWN) + build(:award_emoji, name: AwardEmoji::THUMBS_DOWN), + build(:award_emoji, name: custom_emoji_in_origin_namespace.name) ]) end @@ -23,7 +25,7 @@ RSpec.describe AwardEmojis::CopyService, feature_category: :team_planning do subject(:execute_service) { described_class.new(from_awardable, to_awardable).execute } - it 'copies AwardEmojis', :aggregate_failures do + it 'copies AwardEmojis that exist in the destination namespace', :aggregate_failures do expect { execute_service }.to change { AwardEmoji.count }.by(2) expect(to_awardable.award_emoji.map(&:name)).to match_array([AwardEmoji::THUMBS_UP, AwardEmoji::THUMBS_DOWN]) end diff --git a/spec/services/todos/snoozing_service_spec.rb b/spec/services/todos/snoozing_service_spec.rb index 2c41804d325..26a3e364edd 100644 --- a/spec/services/todos/snoozing_service_spec.rb +++ b/spec/services/todos/snoozing_service_spec.rb @@ -25,11 +25,11 @@ RSpec.describe Todos::SnoozingService, feature_category: :team_planning do context 'when the todo is already snoozed' do let!(:todo) { create(:todo, :pending, snoozed_until: time1, user: user) } - it 'does not change the snoozed_until timestamp' do + it 'changes the snoozed_until timestamp' do service.snooze_todo(todo, time2) todo.reload - expect(todo.snoozed_until).to eq(time1) + expect(todo.snoozed_until).to eq(time2) end end diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index fb849de6489..e0d5c096847 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -1554,52 +1554,49 @@ RSpec.shared_examples 'a container registry auth service' do ] end - before do - enable_admin_mode!(current_user) if current_user == instance_admin - end - using RSpec::Parameterized::TableSyntax # rubocop:disable Layout/LineLength -- Avoid formatting to keep one-line table layout - where(:user, :requested_scopes, :expected_access, :expected_deny_patterns) do - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {} - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => %w[v1.* latest admin-only] } - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:delete"] } | false | nil # developers can't obtain delete access - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => %w[v1.* latest admin-only] } - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | {} - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => %w[v1.* latest admin-only] } - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => %w[v1.* latest admin-only] } - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:*"] } | false | nil # developers can't obtain full access - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,push"] } | true | { 'push' => %w[v1.* latest admin-only] } # single test for edge case where access may be repeated - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,foo"] } | true | { 'push' => %w[v1.* latest admin-only] } # test for (today impossible) case where an access is unknown - ref(:project_developer) | lazy { ["repository:#{container_repository_path}:foo"] } | false | {} # test for (today impossible) case where the access is unknown + where(:user, :requested_scopes, :enable_admin_mode, :expected_access, :expected_deny_patterns) do + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull"] } | false | true | {} + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push"] } | false | true | { 'push' => %w[v1.* latest admin-only] } + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:delete"] } | false | false | nil # developers can't obtain delete access + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | false | true | { 'push' => %w[v1.* latest admin-only] } + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | false | true | {} + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | false | true | { 'push' => %w[v1.* latest admin-only] } + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | false | true | { 'push' => %w[v1.* latest admin-only] } + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:*"] } | false | false | nil # developers can't obtain full access + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,push"] } | false | true | { 'push' => %w[v1.* latest admin-only] } # single test for edge case where access may be repeated + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:push,foo"] } | false | true | { 'push' => %w[v1.* latest admin-only] } # test for (today impossible) case where an access is unknown + ref(:project_developer) | lazy { ["repository:#{container_repository_path}:foo"] } | false | false | {} # test for (today impossible) case where the access is unknown - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {} - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => %w[latest admin-only] } - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:delete"] } | true | { 'delete' => %w[admin-only] } - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => %w[latest admin-only] } - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | { 'delete' => %w[admin-only] } - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] } - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] } - ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:*"] } | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull"] } | false | true | {} + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push"] } | false | true | { 'push' => %w[latest admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:delete"] } | false | true | { 'delete' => %w[admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push"] } | false | true | { 'push' => %w[latest admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | false | true | { 'delete' => %w[admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:push,delete"] } | false | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | false | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] } + ref(:project_maintainer) | lazy { ["repository:#{container_repository_path}:*"] } | false | true | { 'push' => %w[latest admin-only], 'delete' => %w[admin-only] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {} - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => %w[admin-only] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:delete"] } | true | { 'delete' => [] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => %w[admin-only] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | { 'delete' => [] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => %w[admin-only], 'delete' => [] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => %w[admin-only], 'delete' => [] } - ref(:project_owner) | lazy { ["repository:#{container_repository_path}:*"] } | true | { 'push' => %w[admin-only], 'delete' => [] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull"] } | false | true | {} + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push"] } | false | true | { 'push' => %w[admin-only] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:delete"] } | false | true | { 'delete' => [] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push"] } | false | true | { 'push' => %w[admin-only] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | false | true | { 'delete' => [] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:push,delete"] } | false | true | { 'push' => %w[admin-only], 'delete' => [] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | false | true | { 'push' => %w[admin-only], 'delete' => [] } + ref(:project_owner) | lazy { ["repository:#{container_repository_path}:*"] } | false | true | { 'push' => %w[admin-only], 'delete' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull"] } | true | {} - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push"] } | true | { 'push' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:delete"] } | true | { 'delete' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | { 'push' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | { 'delete' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | { 'push' => [], 'delete' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | { 'push' => [], 'delete' => [] } - ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:*"] } | true | { 'push' => [], 'delete' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull"] } | true | true | {} + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push"] } | true | true | { 'push' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:delete"] } | true | true | { 'delete' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push"] } | true | true | { 'push' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,delete"] } | true | true | { 'delete' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:push,delete"] } | true | true | { 'push' => [], 'delete' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:pull,push,delete"] } | true | true | { 'push' => [], 'delete' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:*"] } | true | true | { 'push' => [], 'delete' => [] } + ref(:instance_admin) | lazy { ["repository:#{container_repository_path}:*"] } | false | false | {} # ensure that admin mode is properly enforced end # rubocop:enable Layout/LineLength @@ -1607,6 +1604,10 @@ RSpec.shared_examples 'a container registry auth service' do let(:current_user) { user } let(:current_params) { { scopes: requested_scopes } } + before do + enable_admin_mode!(current_user) if enable_admin_mode + end + it 'returns the expected tag deny access patterns' do is_expected.to include(:token)