Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-12-17 18:31:22 +00:00
parent 2620cc543d
commit 23835e8cac
86 changed files with 2014 additions and 846 deletions

View File

@ -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"

View File

@ -1 +1 @@
b621ac4ec3d3b032489d4ac65df1ffa5752a5565
2035810e4e65a367b3483c75e40f1ce4eb726c48

View File

@ -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

View File

@ -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)

View File

@ -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"},

View File

@ -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)

View File

@ -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: {

View File

@ -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';
}

View File

@ -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,

View File

@ -1,17 +1,11 @@
<script>
import { GlTruncateText } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import ListItemDescription from '~/vue_shared/components/resource_lists/list_item_description.vue';
export default {
name: 'ProjectListItemDescription',
i18n: {
showMore: __('Show more'),
showLess: __('Show less'),
},
truncateTextToggleButtonProps: { class: '!gl-text-sm' },
components: {
GlTruncateText,
ListItemDescription,
},
directives: {
SafeHtml,
@ -31,19 +25,5 @@ export default {
</script>
<template>
<gl-truncate-text
v-if="showDescription"
:lines="2"
:mobile-lines="2"
:show-more-text="$options.i18n.showMore"
:show-less-text="$options.i18n.showLess"
:toggle-button-props="$options.truncateTextToggleButtonProps"
class="gl-mt-2 gl-max-w-88"
>
<div
v-safe-html="project.descriptionHtml"
class="md md-child-content-text-subtle gl-text-sm"
data-testid="project-description"
></div>
</gl-truncate-text>
<list-item-description v-if="showDescription" :description-html="project.descriptionHtml" />
</template>

View File

@ -1,13 +1,5 @@
<script>
import {
GlAvatarLabeled,
GlIcon,
GlLink,
GlBadge,
GlTooltipDirective,
GlPopover,
GlSprintf,
} from '@gitlab/ui';
import { GlIcon, GlBadge, GlTooltipDirective, GlPopover, GlSprintf } from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import {
@ -23,7 +15,6 @@ import { FEATURABLE_ENABLED } from '~/featurable/constants';
import { __, s__ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
import {
@ -32,6 +23,8 @@ import {
} from '~/vue_shared/components/resource_lists/constants';
import { deleteProject } from '~/rest_api';
import { createAlert } from '~/alert';
import ListItem from '~/vue_shared/components/resource_lists/list_item.vue';
import ListItemStat from '~/vue_shared/components/resource_lists/list_item_stat.vue';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
@ -45,8 +38,6 @@ export default {
topics: __('Topics'),
topicsPopoverTargetText: __('+ %{count} more'),
moreTopics: __('More topics'),
[TIMESTAMP_TYPE_CREATED_AT]: __('Created'),
[TIMESTAMP_TYPE_UPDATED_AT]: __('Updated'),
project: __('Project'),
deleteErrorMessage: s__(
'Projects|An error occurred deleting the project. Please refresh the page to try again.',
@ -54,13 +45,12 @@ export default {
ciCatalogBadge: s__('CiCatalog|CI/CD Catalog project'),
},
components: {
GlAvatarLabeled,
GlIcon,
GlLink,
GlBadge,
GlPopover,
GlSprintf,
TimeAgoTooltip,
ListItem,
ListItemStat,
DeleteModal,
ProjectListItemDescription,
ProjectListItemActions,
@ -212,12 +202,6 @@ export default {
hasActionDelete() {
return this.project.availableActions?.includes(ACTION_DELETE);
},
timestampText() {
return this.$options.i18n[this.timestampType];
},
timestamp() {
return this.project[this.timestampType];
},
},
methods: {
topicPath(topic) {
@ -255,170 +239,129 @@ export default {
</script>
<template>
<li class="projects-list-item gl-border-b gl-flex gl-py-5">
<div class="gl-grow md:gl-flex">
<div class="gl-flex gl-grow gl-items-start">
<div v-if="showProjectIcon" class="gl-mr-3 gl-flex gl-h-9 gl-shrink-0 gl-items-center">
<gl-icon name="project" variant="subtle" />
</div>
<gl-avatar-labeled
:entity-id="project.id"
:entity-name="project.name"
:label="project.name"
:label-link="project.webUrl"
:src="project.avatarUrl"
shape="rect"
:size="48"
>
<template #meta>
<div class="gl-px-2">
<div class="-gl-mx-2 gl-flex gl-flex-wrap gl-items-center">
<div class="gl-px-2">
<gl-icon
v-if="visibility"
v-gl-tooltip="visibilityTooltip"
:name="visibilityIcon"
variant="subtle"
/>
</div>
<div v-if="project.isCatalogResource" class="gl-px-2">
<gl-badge
icon="catalog-checkmark"
variant="info"
data-testid="ci-catalog-badge"
:href="project.exploreCatalogPath"
>{{ $options.i18n.ciCatalogBadge }}</gl-badge
>
</div>
<div class="gl-px-2">
<gl-badge
v-if="shouldShowAccessLevel"
class="gl-block"
data-testid="access-level-badge"
>{{ accessLevelLabel }}</gl-badge
>
</div>
</div>
</div>
</template>
<project-list-item-description :project="project" />
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
<div
class="-gl-mx-2 -gl-my-2 gl-inline-flex gl-w-full gl-flex-wrap gl-items-center gl-text-base gl-font-normal"
>
<span class="gl-p-2 gl-text-sm gl-text-subtle">{{ $options.i18n.topics }}:</span>
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
{{ topicTitle(topic) }}
</gl-badge>
</div>
<template v-if="popoverTopics.length">
<div
:id="topicsPopoverTarget"
class="gl-p-2 gl-text-sm gl-text-subtle"
role="button"
tabindex="0"
>
<gl-sprintf :message="$options.i18n.topicsPopoverTargetText">
<template #count>{{ popoverTopics.length }}</template>
</gl-sprintf>
</div>
<gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics">
<div class="-gl-mx-2 -gl-my-2 gl-text-base gl-font-normal">
<div v-for="topic in popoverTopics" :key="topic" class="gl-inline-block gl-p-2">
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
{{ topicTitle(topic) }}
</gl-badge>
</div>
</div>
</gl-popover>
</template>
</div>
</div>
</gl-avatar-labeled>
</div>
<div
class="gl-mt-3 gl-shrink-0 gl-flex-col gl-items-end md:gl-mt-0 md:gl-flex md:gl-pl-0"
:class="showProjectIcon ? 'gl-pl-12' : 'gl-pl-10'"
<list-item
:resource="project"
:show-icon="showProjectIcon"
icon-name="project"
:timestamp-type="timestampType"
>
<template #avatar-meta>
<gl-icon
v-if="visibility"
v-gl-tooltip="visibilityTooltip"
:name="visibilityIcon"
variant="subtle"
/>
<gl-badge
v-if="project.isCatalogResource"
icon="catalog-checkmark"
variant="info"
data-testid="ci-catalog-badge"
:href="project.exploreCatalogPath"
>{{ $options.i18n.ciCatalogBadge }}</gl-badge
>
<div class="gl-flex gl-items-center gl-gap-x-3 md:gl-h-9">
<project-list-item-inactive-badge :project="project" />
<gl-link
v-gl-tooltip="$options.i18n.stars"
:href="starsHref"
:aria-label="$options.i18n.stars"
class="gl-text-subtle"
data-testid="stars-btn"
>
<gl-icon name="star-o" />
<span>{{ starCount }}</span>
</gl-link>
<gl-link
v-if="isForkingEnabled"
v-gl-tooltip="$options.i18n.forks"
:href="forksHref"
:aria-label="$options.i18n.forks"
class="gl-text-subtle"
data-testid="forks-btn"
>
<gl-icon name="fork" />
<span>{{ forksCount }}</span>
</gl-link>
<gl-link
v-if="isMergeRequestsEnabled"
v-gl-tooltip="$options.i18n.mergeRequests"
:href="mergeRequestsHref"
:aria-label="$options.i18n.mergeRequests"
class="gl-text-subtle"
data-testid="mrs-btn"
>
<gl-icon name="merge-request" />
<span>{{ openMergeRequestsCount }}</span>
</gl-link>
<gl-link
v-if="isIssuesEnabled"
v-gl-tooltip="$options.i18n.issues"
:href="issuesHref"
:aria-label="$options.i18n.issues"
class="gl-text-subtle"
data-testid="issues-btn"
>
<gl-icon name="issues" />
<span>{{ openIssuesCount }}</span>
</gl-link>
</div>
<gl-badge v-if="shouldShowAccessLevel" class="gl-block" data-testid="access-level-badge">{{
accessLevelLabel
}}</gl-badge>
</template>
<template #avatar-default>
<project-list-item-description :project="project" />
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
<div
v-if="timestamp"
class="gl-mt-3 gl-whitespace-nowrap gl-text-sm gl-text-subtle md:-gl-mt-2"
class="-gl-mx-2 -gl-my-2 gl-inline-flex gl-w-full gl-flex-wrap gl-items-center gl-text-base gl-font-normal"
>
<span>{{ timestampText }}</span>
<time-ago-tooltip :time="timestamp" />
<span class="gl-p-2 gl-text-sm gl-text-subtle">{{ $options.i18n.topics }}:</span>
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
{{ topicTitle(topic) }}
</gl-badge>
</div>
<template v-if="popoverTopics.length">
<div
:id="topicsPopoverTarget"
class="gl-p-2 gl-text-sm gl-text-subtle"
role="button"
tabindex="0"
>
<gl-sprintf :message="$options.i18n.topicsPopoverTargetText">
<template #count>{{ popoverTopics.length }}</template>
</gl-sprintf>
</div>
<gl-popover :target="topicsPopoverTarget" :title="$options.i18n.moreTopics">
<div class="-gl-mx-2 -gl-my-2 gl-text-base gl-font-normal">
<div v-for="topic in popoverTopics" :key="topic" class="gl-inline-block gl-p-2">
<gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
{{ topicTitle(topic) }}
</gl-badge>
</div>
</div>
</gl-popover>
</template>
</div>
</div>
</div>
<div v-if="hasActions" class="gl-ml-3 gl-flex gl-h-9 gl-items-center">
</template>
<template #stats>
<project-list-item-inactive-badge :project="project" />
<list-item-stat
:href="starsHref"
:tooltip-text="$options.i18n.stars"
icon-name="star-o"
:stat="starCount"
data-testid="stars-btn"
/>
<list-item-stat
v-if="isForkingEnabled"
:href="forksHref"
:tooltip-text="$options.i18n.forks"
icon-name="fork"
:stat="forksCount"
data-testid="forks-btn"
/>
<list-item-stat
v-if="isMergeRequestsEnabled"
:href="mergeRequestsHref"
:tooltip-text="$options.i18n.mergeRequests"
icon-name="merge-request"
:stat="openMergeRequestsCount"
data-testid="mrs-btn"
/>
<list-item-stat
v-if="isIssuesEnabled"
:href="issuesHref"
:tooltip-text="$options.i18n.issues"
icon-name="issues"
:stat="openIssuesCount"
data-testid="issues-btn"
/>
</template>
<template v-if="hasActions" #actions>
<project-list-item-actions
:project="project"
@refetch="$emit('refetch')"
@delete="onActionDelete"
/>
</div>
</template>
<delete-modal
v-if="hasActionDelete"
v-model="isDeleteModalVisible"
:confirm-phrase="project.name"
:is-fork="project.isForked"
:confirm-loading="isDeleteLoading"
:merge-requests-count="openMergeRequestsCount"
:issues-count="openIssuesCount"
:forks-count="forksCount"
:stars-count="starCount"
@primary="onDeleteModalPrimary"
>
<template #modal-footer
><project-list-item-delayed-deletion-modal-footer :project="project"
/></template>
</delete-modal>
</li>
<template #footer>
<delete-modal
v-if="hasActionDelete"
v-model="isDeleteModalVisible"
:confirm-phrase="project.name"
:is-fork="project.isForked"
:confirm-loading="isDeleteLoading"
:merge-requests-count="openMergeRequestsCount"
:issues-count="openIssuesCount"
:forks-count="forksCount"
:stars-count="starCount"
@primary="onDeleteModalPrimary"
>
<template #modal-footer
><project-list-item-delayed-deletion-modal-footer :project="project"
/></template>
</delete-modal>
</template>
</list-item>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
import { GlAvatarLabeled, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
@ -9,21 +9,19 @@ import {
TIMESTAMP_TYPE_CREATED_AT,
TIMESTAMP_TYPE_UPDATED_AT,
} from '~/vue_shared/components/resource_lists/constants';
import ListItemDescription from './list_item_description.vue';
export default {
i18n: {
showMore: __('Show more'),
showLess: __('Show less'),
[TIMESTAMP_TYPE_CREATED_AT]: __('Created'),
[TIMESTAMP_TYPE_UPDATED_AT]: __('Updated'),
},
truncateTextToggleButtonProps: { class: '!gl-text-sm' },
components: {
GlAvatarLabeled,
GlIcon,
GlTruncateText,
ListActions,
TimeAgoTooltip,
ListItemDescription,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -34,7 +32,7 @@ export default {
type: Object,
required: true,
validator(resource) {
const requiredKeys = ['id', 'avatarUrl', 'avatarLabel', 'webUrl', 'availableActions'];
const requiredKeys = ['id', 'avatarUrl', 'avatarLabel', 'webUrl'];
return requiredKeys.every((key) => Object.prototype.hasOwnProperty.call(resource, key));
},
@ -76,7 +74,10 @@ export default {
return this.$options.i18n[this.timestampType];
},
hasActions() {
return Object.keys(this.actions).length && this.resource.availableActions?.length;
return (
this.$scopedSlots.actions ||
(Object.keys(this.actions).length && this.resource.availableActions?.length)
);
},
},
};
@ -105,21 +106,12 @@ export default {
</div>
</div>
</template>
<gl-truncate-text
v-if="resource.descriptionHtml"
:lines="2"
:mobile-lines="2"
:show-more-text="$options.i18n.showMore"
:show-less-text="$options.i18n.showLess"
:toggle-button-props="$options.truncateTextToggleButtonProps"
class="gl-mt-2 gl-max-w-88"
>
<div
v-safe-html="resource.descriptionHtml"
class="md md-child-content-text-subtle gl-text-sm"
data-testid="description"
></div>
</gl-truncate-text>
<slot name="avatar-default">
<list-item-description
v-if="resource.descriptionHtml"
:description-html="resource.descriptionHtml"
/>
</slot>
</gl-avatar-labeled>
</div>
<div
@ -138,12 +130,10 @@ export default {
</div>
</div>
</div>
<div class="-gl-mt-3 gl-ml-3 gl-flex gl-items-center">
<list-actions
v-if="hasActions"
:actions="actions"
:available-actions="resource.availableActions"
/>
<div v-if="hasActions" class="-gl-mt-3 gl-ml-3 gl-flex gl-items-center">
<slot name="actions">
<list-actions :actions="actions" :available-actions="resource.availableActions" />
</slot>
</div>
<slot name="footer"></slot>

View File

@ -0,0 +1,43 @@
<script>
import { GlTruncateText } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
i18n: {
showMore: __('Show more'),
showLess: __('Show less'),
},
truncateTextToggleButtonProps: { class: '!gl-text-sm' },
components: {
GlTruncateText,
},
directives: {
SafeHtml,
},
props: {
descriptionHtml: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-truncate-text
:lines="2"
:mobile-lines="2"
:show-more-text="$options.i18n.showMore"
:show-less-text="$options.i18n.showLess"
:toggle-button-props="$options.truncateTextToggleButtonProps"
class="gl-mt-2 gl-max-w-88"
>
<div
v-safe-html="descriptionHtml"
class="md md-child-content-text-subtle gl-text-sm"
data-testid="description"
></div>
</gl-truncate-text>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlIcon, GlTooltipDirective, GlLink } from '@gitlab/ui';
export default {
components: { GlIcon },
@ -20,17 +20,29 @@ export default {
type: [String, Number],
required: true,
},
href: {
type: String,
required: false,
default: null,
},
},
computed: {
component() {
return this.href ? GlLink : 'div';
},
},
};
</script>
<template>
<div
<component
:is="component"
v-gl-tooltip="tooltipText"
:aria-label="tooltipText"
:href="href"
class="gl-flex gl-items-center gl-gap-x-2 gl-text-subtle"
>
<gl-icon :name="iconName" />
<span class="gl-leading-1">{{ stat }}</span>
</div>
</component>
</template>

View File

@ -1,12 +1,14 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { __, s__ } from '~/locale';
import EditedAt from '~/issues/show/components/edited.vue';
import Tracking from '~/tracking';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
newWorkItemId,
newWorkItemFullPath,
@ -14,6 +16,7 @@ import {
markdownPreviewPath,
} from '~/work_items/utils';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import workItemDescriptionTemplateQuery from '../graphql/work_item_description_template.query.graphql';
import {
i18n,
NEW_WORK_ITEM_IID,
@ -21,6 +24,7 @@ import {
WIDGET_TYPE_DESCRIPTION,
} from '../constants';
import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
import WorkItemDescriptionTemplateListbox from './work_item_description_template_listbox.vue';
export default {
components: {
@ -32,8 +36,9 @@ export default {
GlFormTextarea,
MarkdownEditor,
WorkItemDescriptionRendered,
WorkItemDescriptionTemplateListbox,
},
mixins: [Tracking.mixin()],
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: ['isGroup'],
props: {
description: {
@ -103,6 +108,10 @@ export default {
id: 'work-item-description',
name: 'work-item-description',
},
selectedTemplate: '',
descriptionTemplate: null,
appliedTemplate: '',
showTemplateApplyWarning: false,
};
},
apollo: {
@ -132,6 +141,35 @@ export default {
this.$emit('error', i18n.fetchError);
},
},
descriptionTemplate: {
query: workItemDescriptionTemplateQuery,
skip() {
return !this.selectedTemplate;
},
variables() {
return {
fullPath: this.fullPath,
name: this.selectedTemplate,
};
},
update(data) {
return data.namespace.workItemDescriptionTemplates.nodes[0] || {};
},
result() {
const isDirty = this.descriptionText !== this.workItemDescription?.description;
const isUnchangedTemplate = this.descriptionText === this.appliedTemplate;
const hasContent = this.descriptionText !== '';
if (!isUnchangedTemplate && (isDirty || hasContent)) {
this.showTemplateApplyWarning = true;
} else {
this.applyTemplate();
}
},
error(e) {
Sentry.captureException(e);
this.$emit('error', s__('WorkItem|Unable to find selected template.'));
},
},
},
computed: {
createFlow() {
@ -216,6 +254,12 @@ export default {
showEditedAt() {
return (this.taskCompletionStatus || this.lastEditedAt) && !this.editMode;
},
canShowDescriptionTemplateSelector() {
return this.glFeatures.workItemsAlpha;
},
descriptionTemplateContent() {
return this.descriptionTemplate?.content || '';
},
},
watch: {
updateInProgress(newValue) {
@ -223,6 +267,9 @@ export default {
},
editMode(newValue) {
this.isEditing = newValue;
this.selectedTemplate = '';
this.appliedTemplate = '';
this.showTemplateApplyWarning = false;
if (newValue) {
this.startEditing();
}
@ -299,6 +346,20 @@ export default {
this.$emit('updateDraft', this.descriptionText);
this.updateWorkItem();
},
handleSelectTemplate(templateName) {
this.selectedTemplate = templateName;
},
applyTemplate() {
this.appliedTemplate = this.descriptionTemplateContent;
this.setDescriptionText(this.descriptionTemplateContent);
this.onInput();
this.showTemplateApplyWarning = false;
},
cancelApplyTemplate() {
this.selectedTemplate = '';
this.descriptionTemplate = null;
this.showTemplateApplyWarning = false;
},
},
};
</script>
@ -309,9 +370,42 @@ export default {
<gl-form-group
:class="formGroupClass"
:label="__('Description')"
label-sr-only
:label-sr-only="!canShowDescriptionTemplateSelector"
label-for="work-item-description"
>
<work-item-description-template-listbox
v-if="canShowDescriptionTemplateSelector"
:full-path="fullPath"
:template="selectedTemplate"
@selectTemplate="handleSelectTemplate"
/>
<gl-alert
v-if="showTemplateApplyWarning"
:dismissible="false"
variant="warning"
class="gl-mt-2"
data-testid="description-template-warning"
>
<p>
{{
s__(
'WorkItem|Applying a template will replace the existing description. Any changes you have made will be lost.',
)
}}
</p>
<template #actions>
<gl-button variant="confirm" data-testid="template-apply" @click="applyTemplate"
>{{ s__('WorkItem|Apply template') }}
</gl-button>
<gl-button
category="secondary"
class="gl-ml-3"
data-testid="template-cancel"
@click="cancelApplyTemplate"
>{{ s__('WorkItem|Cancel') }}
</gl-button>
</template>
</gl-alert>
<markdown-editor
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
@ -322,6 +416,7 @@ export default {
enable-autocomplete
supports-quick-actions
:autofocus="autofocus"
:class="{ 'gl-mt-2': canShowDescriptionTemplateSelector }"
@input="setDescriptionText"
@keydown.meta.enter="updateWorkItem"
@keydown.ctrl.enter="updateWorkItem"

View File

@ -0,0 +1,116 @@
<script>
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'),
};
</script>
<template>
<gl-skeleton-loader v-if="loading" />
<gl-collapsible-listbox
v-else-if="hasTemplates"
:items="items"
:toggle-text="toggleText"
:header-text="s__('WorkItem|Select template')"
size="small"
:selected="template"
:loading="loading"
searchable
@shown="showListbox = true"
@hidden="showListbox = false"
@select="handleSelect"
@search="handleSearch"
/>
<p v-else data-testid="template-message">
<gl-sprintf
:message="
s__(
'WorkItem|Add %{linkStart}description templates%{linkEnd} to help your contributors communicate effectively!',
)
"
>
<template #link="{ content }">
<gl-link :href="$options.templateDocsPath">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</template>

View File

@ -0,0 +1,12 @@
query workItemDescriptionTemplate($fullPath: ID!, $name: String!) {
namespace(fullPath: $fullPath) {
id
workItemDescriptionTemplates(name: $name) {
__typename
nodes @client {
name
content
}
}
}
}

View File

@ -0,0 +1,11 @@
query workItemDescriptionTemplatesList($fullPath: ID!) {
namespace(fullPath: $fullPath) {
id
workItemDescriptionTemplates {
__typename
nodes @client {
name
}
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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?

View File

@ -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],

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/).

View File

@ -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 <kbd>Enter</kbd>.
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.

View File

@ -165,7 +165,8 @@ Parameters:
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" \
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" \
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches"
```

View File

@ -18595,6 +18595,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="addonusertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="addonusertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="addonusertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="addonusertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="addonusertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="addonusertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="addonusertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -18931,6 +18932,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="alertmanagementalerttodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="alertmanagementalerttodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="alertmanagementalerttodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="alertmanagementalerttodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="alertmanagementalerttodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="alertmanagementalerttodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="alertmanagementalerttodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -19588,6 +19590,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="autocompletedusertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="autocompletedusertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="autocompletedusertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="autocompletedusertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="autocompletedusertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="autocompletedusertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="autocompletedusertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -22205,6 +22208,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="currentusertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="currentusertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="currentusertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="currentusertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="currentusertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="currentusertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="currentusertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -27624,6 +27628,7 @@ Defines which user roles, users, or groups can merge into a protected branch.
| <a id="mergerequestmilestone"></a>`milestone` | [`Milestone`](#milestone) | Milestone of the merge request. |
| <a id="mergerequestname"></a>`name` | [`String`](#string) | Name or title of this object. |
| <a id="mergerequestparticipants"></a>`participants` | [`MergeRequestParticipantConnection`](#mergerequestparticipantconnection) | Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes. (see [Connections](#connections)) |
| <a id="mergerequestpoliciesoverridingapprovalsettings"></a>`policiesOverridingApprovalSettings` | [`[PolicyApprovalSettingsOverride!]`](#policyapprovalsettingsoverride) | Approval settings that are overridden by the policies for the merge request. |
| <a id="mergerequestpolicyviolations"></a>`policyViolations` | [`PolicyViolationDetails`](#policyviolationdetails) | Policy violations reported on the merge request. |
| <a id="mergerequestpreparedat"></a>`preparedAt` | [`Time`](#time) | Timestamp of when the merge request was prepared. |
| <a id="mergerequestproject"></a>`project` | [`Project!`](#project) | Alias for target_project. |
@ -28142,6 +28147,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestassigneetodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="mergerequestassigneetodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="mergerequestassigneetodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="mergerequestassigneetodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="mergerequestassigneetodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="mergerequestassigneetodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="mergerequestassigneetodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -28551,6 +28557,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestauthortodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="mergerequestauthortodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="mergerequestauthortodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="mergerequestauthortodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="mergerequestauthortodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="mergerequestauthortodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="mergerequestauthortodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -29006,6 +29013,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestparticipanttodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="mergerequestparticipanttodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="mergerequestparticipanttodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="mergerequestparticipanttodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="mergerequestparticipanttodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="mergerequestparticipanttodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="mergerequestparticipanttodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -29434,6 +29442,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestreviewertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="mergerequestreviewertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="mergerequestreviewertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="mergerequestreviewertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="mergerequestreviewertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="mergerequestreviewertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="mergerequestreviewertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -31326,6 +31335,18 @@ Represents policy violation for `any_merge_request` report_type.
| <a id="policyapprovalgroupid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
| <a id="policyapprovalgroupweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the group. |
### `PolicyApprovalSettingsOverride`
Represents the approval settings of merge request overridden by a policy.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="policyapprovalsettingsoverrideeditpath"></a>`editPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. Path to edit the policy. |
| <a id="policyapprovalsettingsoverridename"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. Policy name. |
| <a id="policyapprovalsettingsoverridesettings"></a>`settings` | [`JSON!`](#json) | Overridden project approval settings. |
### `PolicyApproversType`
Multiple approvers action.
@ -36240,6 +36261,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="usercoretodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="usercoretodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="usercoretodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="usercoretodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="usercoretodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="usercoretodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="usercoretodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |
@ -43866,6 +43888,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="usertodosaction"></a>`action` | [`[TodoActionEnum!]`](#todoactionenum) | Action to be filtered. |
| <a id="usertodosauthorid"></a>`authorId` | [`[ID!]`](#id) | ID of an author. |
| <a id="usertodosgroupid"></a>`groupId` | [`[ID!]`](#id) | ID of a group. |
| <a id="usertodosissnoozed"></a>`isSnoozed` | [`Boolean`](#boolean) | Whether the to-do item is snoozed. |
| <a id="usertodosprojectid"></a>`projectId` | [`[ID!]`](#id) | ID of a project. |
| <a id="usertodossort"></a>`sort` | [`TodoSort`](#todosort) | Sort todos by given criteria. |
| <a id="usertodosstate"></a>`state` | [`[TodoStateEnum!]`](#todostateenum) | State of the todo. |

View File

@ -48,7 +48,7 @@ GET /groups/:id/protected_branches
```shell
curl --header "PRIVATE-TOKEN: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/5/protected_branches/*-stable"
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_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: <your_access_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: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
--data '{"allowed_to_push": [{access_level: 40}]}' \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]' \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/main"
```
Example response:

View File

@ -36,7 +36,8 @@ Read more on [pagination](rest/index.md#pagination).
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://primary.example.com/api/v4/groups/90/ssh_certificates"
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://primary.example.com/api/v4/groups/90/ssh_certificates"
```
Example response:

View File

@ -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: <your_access_token>" \
"https://gitlab.example.com/api/v4/project_aliases" \
--form "project_id=1" \
--form "name=gitlab"
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_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: <your_access_token>" \
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/project_aliases/gitlab"
```

View File

@ -32,7 +32,9 @@ Supported attributes:
Example request:
```shell
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
curl --request GET \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" \
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"enabled": true,
@ -108,7 +111,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
Example request to remove pull mirroring:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" \
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"name": "new_project",
@ -160,7 +165,8 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
Example adding pull mirroring:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" \
Example removing pull mirroring:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
```

View File

@ -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"
---

View File

@ -264,7 +264,8 @@ GET /projects/:id/snippets/:snippet_id/user_agent_detail
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail"
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail"
```
Example response:

View File

@ -41,7 +41,8 @@ GET /projects/:id/protected_branches
In the following example, the project ID is `5`.
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches"
curl --header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/main"
curl --header "PRIVATE-TOKEN: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable"
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_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: <your_access_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: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
--data '{"allowed_to_push": [{"access_level": 40}]}' \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]}' \
--header "PRIVATE-TOKEN: <your_access_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: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
```
Example response:

View File

@ -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:

View File

@ -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"
```

View File

@ -396,7 +396,8 @@ Parameters:
Example request:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/snippets/1"
```

View File

@ -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: <your_access_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: <your_access_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:

View File

@ -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.

View File

@ -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.

View File

@ -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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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.
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [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 |

View File

@ -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.

View File

@ -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

11
gems/gitlab-active-context/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
# rspec failure tracking
.rspec_status

View File

@ -0,0 +1,4 @@
include:
- local: gems/gem.gitlab-ci.yml
inputs:
gem_name: "gitlab-active-context"

View File

@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper

View File

@ -0,0 +1,5 @@
inherit_from:
- ../config/rubocop.yml
Gemfile/MissingFeatureCategory:
Enabled: false

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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__)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
module ActiveContext
VERSION = "0.0.1"
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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(),

View File

@ -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);
});
});

View File

@ -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: '<p>Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
};
const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(ListItemDescription, {
propsData: { ...defaultPropsData, ...propsData },
});
};
it('renders description', () => {
createComponent();
expect(wrapper.findComponent(GlTruncateText).element.firstChild.innerHTML).toBe(
defaultPropsData.descriptionHtml,
);
});
});

View File

@ -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': '<div data-testid="avatar-meta"></div>',
stats: '<div data-testid="stats"></div>',
footer: '<div data-testid="footer"></div>',
...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 = '<p>Foo bar</p>';
describe('when avatar-default slot is provided', () => {
beforeEach(() => {
createComponent({
propsData: {
resource: {
...resource,
descriptionHtml,
},
},
scopedSlots: { 'avatar-default': '<div data-testid="avatar-default"></div>' },
});
});
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: '<div data-testid="actions"></div>',
},
});
});
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);
});
});

View File

@ -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);
});
});
});

View File

@ -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

View File

@ -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',
);
});
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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)