Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-02 18:11:29 +00:00
parent 8a2fe0af21
commit 995fb1cd0f
111 changed files with 1818 additions and 1214 deletions

View File

@ -100,22 +100,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/mutations/work_items/delete.rb'
- 'app/graphql/mutations/work_items/update.rb'
- 'app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb'
- 'app/graphql/resolvers/alert_management/alert_resolver.rb'
- 'app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb'
- 'app/graphql/resolvers/alert_management/http_integrations_resolver.rb'
- 'app/graphql/resolvers/alert_management/integrations_resolver.rb'
- 'app/graphql/resolvers/blobs_resolver.rb'
- 'app/graphql/resolvers/board_list_issues_resolver.rb'
- 'app/graphql/resolvers/board_list_resolver.rb'
- 'app/graphql/resolvers/board_lists_resolver.rb'
- 'app/graphql/resolvers/board_resolver.rb'
- 'app/graphql/resolvers/boards_resolver.rb'
- 'app/graphql/resolvers/ci/all_jobs_resolver.rb'
- 'app/graphql/resolvers/ci/config_resolver.rb'
- 'app/graphql/resolvers/ci/group_runners_resolver.rb'
- 'app/graphql/resolvers/ci/jobs_resolver.rb'
- 'app/graphql/resolvers/ci/project_pipeline_counts_resolver.rb'
- 'app/graphql/resolvers/ci/runner_jobs_resolver.rb'
- 'app/graphql/resolvers/ci/runner_projects_resolver.rb'
- 'app/graphql/resolvers/ci/runner_resolver.rb'
- 'app/graphql/resolvers/ci/runner_setup_resolver.rb'
@ -247,23 +231,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/types/issue_status_counts_type.rb'
- 'app/graphql/types/issue_type.rb'
- 'app/graphql/types/issue_type_enum.rb'
- 'app/graphql/types/issues/negated_issue_filter_input_type.rb'
- 'app/graphql/types/issues/unioned_issue_filter_input_type.rb'
- 'app/graphql/types/jira_import_type.rb'
- 'app/graphql/types/jira_user_type.rb'
- 'app/graphql/types/jira_users_mapping_input_type.rb'
- 'app/graphql/types/kas/agent_configuration_type.rb'
- 'app/graphql/types/kas/agent_connection_type.rb'
- 'app/graphql/types/kas/agent_metadata_type.rb'
- 'app/graphql/types/key_type.rb'
- 'app/graphql/types/label_type.rb'
- 'app/graphql/types/member_interface.rb'
- 'app/graphql/types/merge_request_connection_type.rb'
- 'app/graphql/types/merge_request_review_state_enum.rb'
- 'app/graphql/types/merge_request_type.rb'
- 'app/graphql/types/merge_requests/detailed_merge_status_enum.rb'
- 'app/graphql/types/merge_requests/interacts_with_merge_request.rb'
- 'app/graphql/types/merge_requests/merge_status_enum.rb'
- 'app/graphql/types/metadata/kas_type.rb'
- 'app/graphql/types/metadata_type.rb'
- 'app/graphql/types/metrics/dashboards/annotation_type.rb'

View File

@ -113,14 +113,10 @@ All documentation can be found on <https://docs.gitlab.com>.
Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on our website for the many options to get help.
## Why?
## Why? Is it any good?
[Read here](https://about.gitlab.com/why/)
## Is it any good?
[Yes](https://about.gitlab.com/is-it-any-good/)
Read [why our customers choose GitLab](https://about.gitlab.com/why-gitlab/).
## Is it awesome?
[These people](https://twitter.com/gitlab/followers) seem to like it.
[These people](https://about.gitlab.com/customers/) seem to like it.

View File

@ -1,113 +0,0 @@
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
import { n__, s__, sprintf } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
export default {
components: {
GlIcon,
GlLink,
},
props: {
isLoadingDetails: {
type: Boolean,
required: true,
},
isLoadingSharedData: {
type: Boolean,
required: true,
},
openIssuesCount: {
required: false,
type: Number,
default: 0,
},
openMergeRequestsCount: {
required: false,
type: Number,
default: 0,
},
latestVersion: {
required: false,
type: Object,
default: () => ({}),
},
webPath: {
required: false,
type: String,
default: '',
},
},
computed: {
lastReleaseText() {
if (this.latestVersion?.createdAt) {
const timeAgo = getTimeago().format(this.latestVersion.createdAt);
return sprintf(this.$options.i18n.lastRelease, { timeAgo });
}
return this.$options.i18n.lastReleaseMissing;
},
openIssuesText() {
return n__('%d issue', '%d issues', this.openIssuesCount);
},
openMergeRequestText() {
return n__('%d merge request', '%d merge requests', this.openMergeRequestsCount);
},
projectInfoItems() {
return [
{
icon: 'project',
link: `${this.webPath}`,
text: this.$options.i18n.projectLink,
isLoading: this.isLoadingSharedData,
},
{
icon: 'issues',
link: `${this.webPath}/issues`,
text: this.openIssuesText,
isLoading: this.isLoadingDetails,
},
{
icon: 'merge-request',
link: `${this.webPath}/merge_requests`,
text: this.openMergeRequestText,
isLoading: this.isLoadingDetails,
},
{
icon: 'clock',
text: this.lastReleaseText,
isLoading: this.isLoadingSharedData,
},
];
},
},
i18n: {
projectLink: s__('CiCatalog|Go to the project'),
lastRelease: s__('CiCatalog|Released %{timeAgo}'),
lastReleaseMissing: s__('CiCatalog|No release available'),
},
};
</script>
<template>
<div class="gl-py-2 gl-sm-display-flex gl-gap-5">
<span
v-for="item in projectInfoItems"
:key="`${item.icon}`"
class="gl-display-flex gl-align-items-center gl-mb-3 gl-sm-mb-0"
>
<gl-icon class="gl-text-primary gl-mr-2" :name="item.icon" />
<div
v-if="item.isLoading"
class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-15"
data-testid="skeleton-loading-line"
></div>
<template v-else>
<gl-link v-if="item.link" :href="item.link"> {{ item.text }} </gl-link>
<span v-else class="gl-text-secondary">
{{ item.text }}
</span>
</template>
</span>
</div>
</template>

View File

@ -16,7 +16,6 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
import { VERIFICATION_LEVEL_UNVERIFIED } from '../../constants';
import CiVerificationBadge from '../shared/ci_verification_badge.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
export default {
@ -28,7 +27,6 @@ export default {
},
components: {
AbuseCategorySelector,
CiResourceAbout,
CiResourceHeaderSkeletonLoader,
CiVerificationBadge,
GlAvatar,
@ -44,24 +42,10 @@ export default {
},
inject: ['reportAbusePath'],
props: {
isLoadingDetails: {
isLoadingData: {
type: Boolean,
required: true,
},
isLoadingSharedData: {
type: Boolean,
required: true,
},
openIssuesCount: {
type: Number,
required: false,
default: 0,
},
openMergeRequestsCount: {
type: Number,
required: false,
default: 0,
},
resource: {
type: Object,
required: true,
@ -120,7 +104,7 @@ export default {
</script>
<template>
<div>
<ci-resource-header-skeleton-loader v-if="isLoadingSharedData" class="gl-py-5" />
<ci-resource-header-skeleton-loader v-if="isLoadingData" class="gl-py-5" />
<div v-else class="gl-display-flex gl-justify-content-space-between gl-py-5">
<div class="gl-display-flex">
<gl-avatar-link :href="resource.webPath">
@ -189,17 +173,8 @@ export default {
</gl-disclosure-dropdown>
</div>
</div>
<ci-resource-about
v-if="false"
:is-loading-details="isLoadingDetails"
:is-loading-shared-data="isLoadingSharedData"
:open-issues-count="openIssuesCount"
:open-merge-requests-count="openMergeRequestsCount"
:latest-version="latestVersion"
:web-path="resource.webPath"
/>
<div
v-if="isLoadingSharedData"
v-if="isLoadingData"
class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-my-3 gl-max-w-20!"
></div>
<markdown v-else class="gl-mt-2" :markdown="resource.description" />

View File

@ -3,7 +3,6 @@ import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import getCatalogCiResourceDetails from '../../graphql/queries/get_ci_catalog_resource_details.query.graphql';
import getCatalogCiResourceSharedData from '../../graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
import CiResourceDetails from '../details/ci_resource_details.vue';
import CiResourceHeader from '../details/ci_resource_header.vue';
@ -19,7 +18,6 @@ export default {
return {
isEmpty: false,
resourceSharedData: {},
resourceAdditionalDetails: {},
};
},
apollo: {
@ -38,30 +36,12 @@ export default {
createAlert({ message: e.message });
},
},
resourceAdditionalDetails: {
query: getCatalogCiResourceDetails,
variables() {
return {
fullPath: this.cleanFullPath,
};
},
update(data) {
return data.ciCatalogResource;
},
error(e) {
this.isEmpty = true;
createAlert({ message: e.message });
},
},
},
computed: {
cleanFullPath() {
return cleanLeadingSeparator(this.$route.params.id);
},
isLoadingDetails() {
return this.$apollo.queries.resourceAdditionalDetails.loading;
},
isLoadingSharedData() {
isLoadingData() {
return this.$apollo.queries.resourceSharedData.loading;
},
version() {
@ -88,13 +68,7 @@ export default {
/>
</div>
<div v-else>
<ci-resource-header
:open-issues-count="resourceAdditionalDetails.openIssuesCount"
:open-merge-requests-count="resourceAdditionalDetails.openMergeRequestsCount"
:is-loading-details="isLoadingDetails"
:is-loading-shared-data="isLoadingSharedData"
:resource="resourceSharedData"
/>
<ci-resource-header :is-loading-data="isLoadingData" :resource="resourceSharedData" />
<ci-resource-details :resource-path="cleanFullPath" :version="version" />
</div>
</div>

View File

@ -1,8 +0,0 @@
query getCiCatalogResourceDetails($fullPath: ID!) {
ciCatalogResource(fullPath: $fullPath) {
id
webPath
openIssuesCount
openMergeRequestsCount
}
}

View File

@ -1,6 +0,0 @@
export const RESOURCE_TYPE_GROUPS = 'groups';
export const RESOURCE_TYPE_PROJECTS = 'projects';
export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
export const ORGANIZATION_USERS_PER_PAGE = 20;

View File

@ -2,13 +2,14 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { __ } from '~/locale';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import { onPageChange } from '~/organizations/shared/utils';
import {
RESOURCE_TYPE_GROUPS,
RESOURCE_TYPE_PROJECTS,
QUERY_PARAM_END_CURSOR,
QUERY_PARAM_START_CURSOR,
SORT_DIRECTION_ASC,

View File

@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
import { ORGANIZATION_ROOT_ROUTE_NAME } from '~/organizations/shared/constants';
import App from './components/app.vue';
export const createRouter = () => {

View File

@ -1,6 +1,11 @@
import { formValidators } from '@gitlab/ui/dist/utils';
import { s__, __ } from '~/locale';
export const RESOURCE_TYPE_GROUPS = 'groups';
export const RESOURCE_TYPE_PROJECTS = 'projects';
export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
export const FORM_FIELD_NAME = 'name';
export const FORM_FIELD_ID = 'id';
export const FORM_FIELD_PATH = 'path';
@ -30,17 +35,21 @@ export const QUERY_PARAM_END_CURSOR = 'end_cursor';
export const SORT_DIRECTION_ASC = 'asc';
export const SORT_DIRECTION_DESC = 'desc';
export const SORT_NAME = 'name';
export const SORT_CREATED_AT = 'created_at';
export const SORT_UPDATED_AT = 'updated_at';
export const SORT_ITEM_NAME = {
value: 'name',
value: SORT_NAME,
text: __('Name'),
};
export const SORT_ITEM_CREATED_AT = {
value: 'created_at',
value: SORT_CREATED_AT,
text: __('Created'),
};
export const SORT_ITEM_UPDATED_AT = {
value: 'updated_at',
value: SORT_UPDATED_AT,
text: __('Updated'),
};

View File

@ -1,6 +1,6 @@
<script>
import { __, s__ } from '~/locale';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/shared/constants';
import AssociationCountCard from './association_count_card.vue';
export default {

View File

@ -2,12 +2,19 @@
import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { s__, __ } from '~/locale';
import GroupsView from '../../shared/components/groups_view.vue';
import ProjectsView from '../../shared/components/projects_view.vue';
import { onPageChange } from '../../shared/utils';
import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from '../../shared/constants';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
import { FILTER_FREQUENTLY_VISITED, GROUPS_AND_PROJECTS_PER_PAGE } from '../constants';
import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import { onPageChange } from '~/organizations/shared/utils';
import {
RESOURCE_TYPE_GROUPS,
RESOURCE_TYPE_PROJECTS,
QUERY_PARAM_END_CURSOR,
QUERY_PARAM_START_CURSOR,
SORT_CREATED_AT,
SORT_UPDATED_AT,
SORT_DIRECTION_DESC,
} from '~/organizations/shared/constants';
import { GROUPS_AND_PROJECTS_PER_PAGE } from '../constants';
import { buildDisplayListboxItem } from '../utils';
export default {
@ -20,17 +27,28 @@ export default {
components: { GlCollapsibleListbox, GlLink },
displayListboxItems: [
buildDisplayListboxItem({
filter: FILTER_FREQUENTLY_VISITED,
resourceType: RESOURCE_TYPE_PROJECTS,
text: s__('Organization|Frequently visited projects'),
sortName: SORT_UPDATED_AT,
resourceType: RESOURCE_TYPE_GROUPS,
text: s__('Organization|Recently updated groups'),
}),
buildDisplayListboxItem({
filter: FILTER_FREQUENTLY_VISITED,
sortName: SORT_CREATED_AT,
resourceType: RESOURCE_TYPE_GROUPS,
text: s__('Organization|Frequently visited groups'),
text: s__('Organization|Recently created groups'),
}),
buildDisplayListboxItem({
sortName: SORT_UPDATED_AT,
resourceType: RESOURCE_TYPE_PROJECTS,
text: s__('Organization|Recently updated projects'),
}),
buildDisplayListboxItem({
sortName: SORT_CREATED_AT,
resourceType: RESOURCE_TYPE_PROJECTS,
text: s__('Organization|Recently created projects'),
}),
],
PER_PAGE: GROUPS_AND_PROJECTS_PER_PAGE,
SORT_DIRECTION_DESC,
props: {
groupsAndProjectsOrganizationPath: {
type: String,
@ -40,11 +58,10 @@ export default {
computed: {
displayListboxSelected() {
const { display } = this.$route.query;
const [{ value: fallbackSelected }] = this.$options.displayListboxItems;
const [fallbackSelected] = this.$options.displayListboxItems;
return (
this.$options.displayListboxItems.find(({ value }) => value === display)?.value ||
fallbackSelected
this.$options.displayListboxItems.find(({ value }) => value === display) || fallbackSelected
);
},
startCursor() {
@ -55,9 +72,12 @@ export default {
},
resourceTypeSelected() {
return [RESOURCE_TYPE_PROJECTS, RESOURCE_TYPE_GROUPS].find((resourceType) =>
this.displayListboxSelected.endsWith(resourceType),
this.displayListboxSelected.value.endsWith(resourceType),
);
},
sortName() {
return this.displayListboxSelected.sortName;
},
routerView() {
switch (this.resourceTypeSelected) {
case RESOURCE_TYPE_GROUPS:
@ -107,7 +127,7 @@ export default {
<gl-collapsible-listbox
block
toggle-class="gl-w-30"
:selected="displayListboxSelected"
:selected="displayListboxSelected.value"
:items="$options.displayListboxItems"
:toggle-aria-labelled-by="$options.displayListboxLabelId"
@select="onDisplayListboxSelect"
@ -124,6 +144,8 @@ export default {
:start-cursor="startCursor"
:end-cursor="endCursor"
:per-page="$options.PER_PAGE"
:sort-name="sortName"
:sort-direction="$options.SORT_DIRECTION_DESC"
@page-change="onPageChange"
/>
</div>

View File

@ -1,3 +1 @@
export const FILTER_FREQUENTLY_VISITED = 'frequently_visited';
export const GROUPS_AND_PROJECTS_PER_PAGE = 5;

View File

@ -3,7 +3,7 @@ import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
import { ORGANIZATION_ROOT_ROUTE_NAME } from '~/organizations/shared/constants';
import App from './components/app.vue';
export const createRouter = () => {

View File

@ -1,4 +1,5 @@
export const buildDisplayListboxItem = ({ filter, resourceType, text }) => ({
export const buildDisplayListboxItem = ({ sortName, resourceType, text }) => ({
text,
value: `${filter}_${resourceType}`,
value: `${sortName}_${resourceType}`,
sortName,
});

View File

@ -1,9 +1,9 @@
<script>
import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import organizationUsersQuery from '../graphql/organization_users.query.graphql';
import { ORGANIZATION_USERS_PER_PAGE } from '../constants';
import UsersView from './users_view.vue';
const defaultPagination = {

View File

@ -0,0 +1 @@
export const ORGANIZATION_USERS_PER_PAGE = 20;

View File

@ -10,10 +10,12 @@ import {
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
TAB_VULNERABILITY_MANAGEMENT_INDEX,
i18n,
PRE_RECEIVE_SECRET_DETECTION,
} from '../constants';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import FeatureCard from './feature_card.vue';
import PreReceiveSecretDetectionFeatureCard from './pre_receive_secret_detection_feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
export default {
@ -22,6 +24,7 @@ export default {
AutoDevOpsAlert,
AutoDevOpsEnabledAlert,
FeatureCard,
PreReceiveSecretDetectionFeatureCard,
GlAlert,
GlLink,
GlSprintf,
@ -95,6 +98,12 @@ export default {
},
},
methods: {
getComponentName(feature) {
if (feature.type === PRE_RECEIVE_SECRET_DETECTION) {
return 'pre-receive-secret-detection-feature-card';
}
return 'feature-card';
},
dismissAutoDevopsEnabledAlert() {
const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
dismissedProjects.add(this.projectFullPath);
@ -192,7 +201,8 @@ export default {
</template>
<template #features>
<feature-card
<component
:is="getComponentName(feature)"
v-for="feature in augmentedSecurityFeatures"
:id="feature.anchor"
:key="feature.type"

View File

@ -0,0 +1,181 @@
<script>
import { GlCard, GlIcon, GlLink, GlPopover, GlToggle, GlAlert } from '@gitlab/ui';
import ProjectSetPreReceiveSecretDetection from '~/security_configuration/graphql/set_pre_receive_secret_detection.graphql';
import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import { __, s__ } from '~/locale';
export default {
name: 'PreReceiveSecretDetectionFeatureCard',
components: {
GlCard,
GlIcon,
GlLink,
GlPopover,
GlToggle,
GlAlert,
BetaBadge,
},
inject: [
'preReceiveSecretDetectionAvailable',
'preReceiveSecretDetectionEnabled',
'projectFullPath',
],
props: {
feature: {
type: Object,
required: true,
},
},
data() {
return {
toggleValue: this.preReceiveSecretDetectionEnabled,
errorMessage: '',
isAlertDismissed: false,
};
},
computed: {
shouldShowAlert() {
return this.errorMessage && !this.isAlertDismissed;
},
available() {
return this.feature.available;
},
enabled() {
return this.available && this.toggleValue;
},
cardClasses() {
return { 'gl-bg-gray-10': !this.available };
},
statusClasses() {
const { enabled } = this;
return {
'gl-ml-auto': true,
'gl-flex-shrink-0': true,
'gl-text-gray-500': !enabled,
'gl-text-green-500': enabled,
'gl-w-full': true,
'gl-justify-content-space-between': true,
'gl-display-flex': true,
'gl-mb-4': true,
};
},
showLock() {
return !this.preReceiveSecretDetectionAvailable;
},
},
methods: {
onError(message) {
this.$emit('error', message);
},
reportError(error) {
this.errorMessage = error;
this.isAlertDismissed = false;
},
async togglePreReceiveSecretDetection(checked) {
try {
const { data } = await this.$apollo.mutate({
mutation: ProjectSetPreReceiveSecretDetection,
variables: {
input: {
namespacePath: this.projectFullPath,
enable: checked,
},
},
});
const { errors, preReceiveSecretDetectionEnabled } = data.setPreReceiveSecretDetection;
if (errors.length > 0) {
this.reportError(errors[0].message);
}
if (preReceiveSecretDetectionEnabled !== null) {
this.toggleValue = preReceiveSecretDetectionEnabled;
this.$toast.show(
preReceiveSecretDetectionEnabled
? this.$options.i18n.toastMessageEnabled
: this.$options.i18n.toastMessageDisabled,
);
}
} catch (error) {
this.reportError(error);
}
},
},
i18n: {
enabled: s__('SecurityConfiguration|Enabled'),
notEnabled: s__('SecurityConfiguration|Not enabled'),
availableWith: s__('SecurityConfiguration|Available with Ultimate'),
learnMore: __('Learn more'),
featureLockTitle: s__('SecretDetection||Feature not available'),
featureLockDescription: s__(
'SecretDetection|This feature has been disabled at the instance level. Please reach out to your instance administrator to request activation.',
),
toastMessageEnabled: s__('SecretDetection|Pre-receive Secret Detection is enabled'),
toastMessageDisabled: s__('SecretDetection|Pre-receive Secret Detection is disabled'),
},
};
</script>
<template>
<gl-card :class="cardClasses">
<div class="gl-display-flex gl-align-items-baseline gl-flex-direction-column-reverse">
<h3 class="gl-font-lg gl-m-0 gl-mr-3">
{{ feature.name }}
<gl-icon v-if="showLock" id="lockIcon" name="lock" class="gl-mb-1" />
</h3>
<gl-popover target="lockIcon" placement="right">
<template #title> {{ $options.i18n.featureLockTitle }} </template>
<slot>
{{ $options.i18n.featureLockDescription }}
</slot>
</gl-popover>
<div
:class="statusClasses"
data-testid="feature-status"
:data-qa-feature="`${feature.type}_${enabled}_status`"
>
<beta-badge size="sm" />
<template v-if="enabled">
<span>
<gl-icon name="check-circle-filled" />
<span class="gl-text-green-700">{{ $options.i18n.enabled }}</span>
</span>
</template>
<template v-else-if="available">
<span>{{ $options.i18n.notEnabled }}</span>
</template>
<template v-else>
{{ $options.i18n.availableWith }}
</template>
</div>
</div>
<p class="gl-mb-0 gl-mt-5">
{{ feature.description }}
<gl-link :href="feature.helpPath" target="_blank">{{ $options.i18n.learnMore }}</gl-link>
</p>
<template v-if="available">
<gl-alert
v-if="shouldShowAlert"
class="gl-mb-5 gl-mt-2"
variant="danger"
@dismiss="isAlertDismissed = true"
>{{ errorMessage }}</gl-alert
>
<gl-toggle
class="gl-mt-5"
:disabled="!preReceiveSecretDetectionAvailable"
:value="toggleValue"
:label="s__('SecurityConfiguration|Toggle Pre-receive secret detection')"
label-position="hidden"
@change="togglePreReceiveSecretDetection"
/>
</template>
</gl-card>
</template>

View File

@ -50,6 +50,8 @@ export const API_FUZZING_NAME = __('API Fuzzing');
export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning');
export const PRE_RECEIVE_SECRET_DETECTION = 'pre_receive_secret_detection';
export const SCANNER_NAMES_MAP = {
SAST: SAST_SHORT_NAME,
SAST_IAC: SAST_IAC_NAME,

View File

@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
@ -12,6 +13,7 @@ export const initSecurityConfiguration = (el) => {
}
Vue.use(VueApollo);
Vue.use(GlToast);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@ -45,6 +47,10 @@ export const initSecurityConfiguration = (el) => {
autoDevopsPath,
vulnerabilityTrainingDocsPath,
containerScanningForRegistryEnabled,
...parseBooleanDataAttributes(el, [
'preReceiveSecretDetectionAvailable',
'preReceiveSecretDetectionEnabled',
]),
},
render(createElement) {
return createElement(SecurityConfigurationApp, {

View File

@ -13,7 +13,7 @@
module Security
class SecurityJobsFinder < JobsFinder
def self.allowed_job_types
[:sast, :sast_iac, :breach_and_attack_simulation, :dast, :dependency_scanning, :container_scanning, :secret_detection, :coverage_fuzzing, :api_fuzzing, :cluster_image_scanning]
[:sast, :sast_iac, :breach_and_attack_simulation, :dast, :dependency_scanning, :container_scanning, :pre_receive_secret_detection, :secret_detection, :coverage_fuzzing, :api_fuzzing, :cluster_image_scanning]
end
end
end

View File

@ -6,30 +6,30 @@ module Resolvers
include LooksAhead
argument :iid, GraphQL::Types::String,
required: false,
description: 'IID of the alert. For example, "1".'
required: false,
description: 'IID of the alert. For example, "1".'
argument :statuses, [Types::AlertManagement::StatusEnum],
as: :status,
required: false,
description: 'Alerts with the specified statues. For example, `[TRIGGERED]`.'
as: :status,
required: false,
description: 'Alerts with the specified statues. For example, `[TRIGGERED]`.'
argument :sort, Types::AlertManagement::AlertSortEnum,
description: 'Sort alerts by this criteria.',
required: false
description: 'Sort alerts by this criteria.',
required: false
argument :domain, Types::AlertManagement::DomainFilterEnum,
description: 'Filter query for given domain.',
required: true,
default_value: 'operations'
description: 'Filter query for given domain.',
required: true,
default_value: 'operations'
argument :search, GraphQL::Types::String,
description: 'Search query for title, description, service, or monitoring_tool.',
required: false
description: 'Search query for title, description, service, or monitoring_tool.',
required: false
argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.'
required: false,
description: 'Username of a user assigned to the issue.'
type Types::AlertManagement::AlertType, null: true

View File

@ -6,12 +6,12 @@ module Resolvers
type Types::AlertManagement::AlertStatusCountsType, null: true
argument :search, GraphQL::Types::String,
description: 'Search query for title, description, service, or monitoring_tool.',
required: false
description: 'Search query for title, description, service, or monitoring_tool.',
required: false
argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.'
required: false,
description: 'Username of a user assigned to the issue.'
def resolve(**args)
::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args)

View File

@ -8,8 +8,8 @@ module Resolvers
alias_method :project, :object
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: false,
description: 'ID of the integration.'
required: false,
description: 'ID of the integration.'
type Types::AlertManagement::HttpIntegrationType.connection_type, null: true

View File

@ -8,8 +8,8 @@ module Resolvers
alias_method :project, :object
argument :id, ::Types::GlobalIDType,
required: false,
description: 'ID of the integration.'
required: false,
description: 'ID of the integration.'
type Types::AlertManagement::IntegrationType.connection_type, null: true

View File

@ -11,16 +11,16 @@ module Resolvers
alias_method :repository, :object
argument :paths, [GraphQL::Types::String],
required: true,
description: 'Array of desired blob paths.'
required: true,
description: 'Array of desired blob paths.'
argument :ref, GraphQL::Types::String,
required: false,
default_value: nil,
description: 'Commit ref to get the blobs from. Default value is HEAD.'
required: false,
default_value: nil,
description: 'Commit ref to get the blobs from. Default value is HEAD.'
argument :ref_type, Types::RefTypeEnum,
required: false,
default_value: nil,
description: 'Type of ref.'
required: false,
default_value: nil,
description: 'Type of ref.'
# We fetch blobs from Gitaly efficiently but it still scales O(N) with the
# number of paths being fetched, so apply a scaling limit to that.

View File

@ -5,8 +5,8 @@ module Resolvers
include BoardItemFilterable
argument :filters, Types::Boards::BoardIssueInputType,
required: false,
description: 'Filters applied when selecting issues in the board list.'
required: false,
description: 'Filters applied when selecting issues in the board list.'
type Types::IssueType, null: true

View File

@ -11,12 +11,12 @@ module Resolvers
authorize :read_issue_board_list
argument :id, Types::GlobalIDType[List],
required: true,
description: 'Global ID of the list.'
required: true,
description: 'Global ID of the list.'
argument :issue_filters, Types::Boards::BoardIssueInputType,
required: false,
description: 'Filters applied when getting issue metadata in the board list.'
required: false,
description: 'Filters applied when getting issue metadata in the board list.'
def resolve(id: nil, issue_filters: {})
Gitlab::Graphql::Lazy.with_value(find_list(id: id)) do |list|

View File

@ -11,12 +11,12 @@ module Resolvers
authorizes_object!
argument :id, Types::GlobalIDType[List],
required: false,
description: 'Find a list by its global ID.'
required: false,
description: 'Find a list by its global ID.'
argument :issue_filters, Types::Boards::BoardIssueInputType,
required: false,
description: 'Filters applied when getting issue metadata in the board list.'
required: false,
description: 'Filters applied when getting issue metadata in the board list.'
alias_method :board, :object

View File

@ -7,8 +7,8 @@ module Resolvers
type Types::BoardType, null: true
argument :id, ::Types::GlobalIDType[::Board],
required: true,
description: 'ID of the board.'
required: true,
description: 'ID of the board.'
def resolve(id: nil)
return unless parent

View File

@ -5,8 +5,8 @@ module Resolvers
type Types::BoardType, null: true
argument :id, ::Types::GlobalIDType[::Board],
required: false,
description: 'Find a board by its ID.'
required: false,
description: 'Find a board by its ID.'
def resolve(id: nil)
# The project or group could have been loaded in batch by `BatchLoader`.

View File

@ -8,14 +8,14 @@ module Resolvers
type ::Types::Ci::JobType.connection_type, null: true
argument :statuses, [::Types::Ci::JobStatusEnum],
required: false,
description: 'Filter jobs by status.'
required: false,
description: 'Filter jobs by status.'
argument :runner_types, [::Types::Ci::RunnerTypeEnum],
required: false,
alpha: { milestone: '16.4' },
description: 'Filter jobs by runner type if ' \
'feature flag `:admin_jobs_filter_runner_type` is enabled.'
required: false,
alpha: { milestone: '16.4' },
description: 'Filter jobs by runner type if ' \
'feature flag `:admin_jobs_filter_runner_type` is enabled.'
def resolve_with_lookahead(**args)
jobs = ::Ci::JobsFinder.new(current_user: current_user, params: params_data(args)).execute

View File

@ -15,35 +15,35 @@ module Resolvers
authorize :create_pipeline
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project of the CI config.'
required: true,
description: 'Project of the CI config.'
argument :sha, GraphQL::Types::String,
required: false,
description: "Sha for the pipeline."
required: false,
description: "Sha for the pipeline."
argument :content, GraphQL::Types::String,
required: true,
description: "Contents of `.gitlab-ci.yml`."
required: true,
description: "Contents of `.gitlab-ci.yml`."
argument :dry_run, GraphQL::Types::Boolean,
required: false,
description: 'Run pipeline creation simulation, or only do static check.'
required: false,
description: 'Run pipeline creation simulation, or only do static check.'
argument :skip_verify_project_sha, GraphQL::Types::Boolean,
required: false,
alpha: { milestone: '16.5' },
description: "If the provided `sha` is found in the project's repository but is not " \
"associated with a Git reference (a detached commit), the verification fails and a " \
"validation error is returned. Otherwise, verification passes, even if the `sha` is " \
"invalid. Set to `true` to skip this verification process."
required: false,
alpha: { milestone: '16.5' },
description: "If the provided `sha` is found in the project's repository but is not " \
"associated with a Git reference (a detached commit), the verification fails and a " \
"validation error is returned. Otherwise, verification passes, even if the `sha` is " \
"invalid. Set to `true` to skip this verification process."
def resolve(project_path:, content:, sha: nil, dry_run: false, skip_verify_project_sha: false)
project = authorized_find!(project_path: project_path)
result = ::Gitlab::Ci::Lint
.new(project: project, current_user: context[:current_user], sha: sha,
verify_project_sha: !skip_verify_project_sha)
verify_project_sha: !skip_verify_project_sha)
.validate(content, dry_run: dry_run)
response(result)

View File

@ -6,9 +6,9 @@ module Resolvers
type Types::Ci::RunnerType.connection_type, null: true
argument :membership, ::Types::Ci::RunnerMembershipFilterEnum,
required: false,
default_value: :descendants,
description: 'Control which runners to include in the results.'
required: false,
default_value: :descendants,
description: 'Control which runners to include in the results.'
protected

View File

@ -8,24 +8,24 @@ module Resolvers
type ::Types::Ci::JobType.connection_type, null: true
argument :security_report_types, [Types::Security::ReportTypeEnum],
required: false,
description: 'Filter jobs by the type of security report they produce.'
required: false,
description: 'Filter jobs by the type of security report they produce.'
argument :statuses, [::Types::Ci::JobStatusEnum],
required: false,
description: 'Filter jobs by status.'
required: false,
description: 'Filter jobs by status.'
argument :retried, ::GraphQL::Types::Boolean,
required: false,
description: 'Filter jobs by retry-status.'
required: false,
description: 'Filter jobs by retry-status.'
argument :when_executed, [::GraphQL::Types::String],
required: false,
description: 'Filter jobs by when they are executed.'
required: false,
description: 'Filter jobs by when they are executed.'
argument :job_kind, ::Types::Ci::JobKindEnum,
required: false,
description: 'Filter jobs by kind.'
required: false,
description: 'Filter jobs by kind.'
def resolve(statuses: nil, security_report_types: [], retried: nil, when_executed: nil, job_kind: nil)
jobs = init_collection(security_report_types)

View File

@ -6,19 +6,19 @@ module Resolvers
type Types::Ci::PipelineCountsType, null: true
argument :ref,
GraphQL::Types::String,
required: false,
description: "Filter pipelines by the ref they are run for."
GraphQL::Types::String,
required: false,
description: "Filter pipelines by the ref they are run for."
argument :sha,
GraphQL::Types::String,
required: false,
description: "Filter pipelines by the SHA of the commit they are run for."
GraphQL::Types::String,
required: false,
description: "Filter pipelines by the SHA of the commit they are run for."
argument :source,
GraphQL::Types::String,
required: false,
description: "Filter pipelines by their source."
GraphQL::Types::String,
required: false,
description: "Filter pipelines by their source."
def resolve(**args)
::Gitlab::PipelineScopeCounts.new(context[:current_user], object, args)

View File

@ -12,8 +12,8 @@ module Resolvers
extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
argument :statuses, [::Types::Ci::JobStatusEnum],
required: false,
description: 'Filter jobs by status.'
required: false,
description: 'Filter jobs by status.'
alias_method :runner, :object

View File

@ -6,36 +6,36 @@ module Types
graphql_name 'NegatedIssueFilterInput'
argument :assignee_id, GraphQL::Types::String,
required: false,
description: 'ID of a user not assigned to the issues.'
required: false,
description: 'ID of a user not assigned to the issues.'
argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users not assigned to the issue.'
required: false,
description: 'Usernames of users not assigned to the issue.'
argument :author_username, [GraphQL::Types::String],
required: false,
description: "Username of a user who didn't author the issue."
required: false,
description: "Username of a user who didn't author the issue."
argument :iids, [GraphQL::Types::String],
required: false,
description: 'List of IIDs of issues to exclude. For example, `[1, 2]`.'
required: false,
description: 'List of IIDs of issues to exclude. For example, `[1, 2]`.'
argument :label_name, [GraphQL::Types::String],
required: false,
description: 'Labels not applied to this issue.'
required: false,
description: 'Labels not applied to this issue.'
argument :milestone_title, [GraphQL::Types::String],
required: false,
description: 'Milestone not applied to this issue.'
required: false,
description: 'Milestone not applied to this issue.'
argument :milestone_wildcard_id, ::Types::NegatedMilestoneWildcardIdEnum,
required: false,
description: 'Filter by negated milestone wildcard values.'
required: false,
description: 'Filter by negated milestone wildcard values.'
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user.'
required: false,
description: 'Filter by reaction emoji applied by the current user.'
argument :release_tag, [GraphQL::Types::String],
required: false,
description: "Release tag not associated with the issue's milestone. Ignored when parent is a group."
required: false,
description: "Release tag not associated with the issue's milestone. Ignored when parent is a group."
argument :types, [Types::IssueTypeEnum],
as: :issue_types,
description: 'Filters out issues by the given issue types.',
required: false
as: :issue_types,
description: 'Filters out issues by the given issue types.',
required: false
end
end
end

View File

@ -6,14 +6,14 @@ module Types
graphql_name 'UnionedIssueFilterInput'
argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Filters issues that are assigned to at least one of the given users.'
required: false,
description: 'Filters issues that are assigned to at least one of the given users.'
argument :author_usernames, [GraphQL::Types::String],
required: false,
description: 'Filters issues that are authored by one of the given users.'
required: false,
description: 'Filters issues that are authored by one of the given users.'
argument :label_names, [GraphQL::Types::String],
required: false,
description: 'Filters issues that have at least one of the given labels.'
required: false,
description: 'Filters issues that have at least one of the given labels.'
end
end
end

View File

@ -7,19 +7,19 @@ module Types
graphql_name 'JiraImport'
field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created.'
description: 'Timestamp of when the Jira import was created.'
field :failed_to_import_count, GraphQL::Types::Int, null: false,
description: 'Count of issues that failed to import.'
description: 'Count of issues that failed to import.'
field :imported_issues_count, GraphQL::Types::Int, null: false,
description: 'Count of issues that were successfully imported.'
description: 'Count of issues that were successfully imported.'
field :jira_project_key, GraphQL::Types::String, null: false,
description: 'Project key for the imported Jira project.'
description: 'Project key for the imported Jira project.'
field :scheduled_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was scheduled.'
description: 'Timestamp of when the Jira import was scheduled.'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import.'
description: 'User that started the Jira import.'
field :total_issue_count, GraphQL::Types::Int, null: false,
description: 'Total count of issues that were attempted to import.'
description: 'Total count of issues that were attempted to import.'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -7,19 +7,19 @@ module Types
graphql_name 'JiraUser'
field :gitlab_id, GraphQL::Types::Int, null: true,
description: 'ID of the matched GitLab user.'
description: 'ID of the matched GitLab user.'
field :gitlab_name, GraphQL::Types::String, null: true,
description: 'Name of the matched GitLab user.'
description: 'Name of the matched GitLab user.'
field :gitlab_username, GraphQL::Types::String, null: true,
description: 'Username of the matched GitLab user.'
description: 'Username of the matched GitLab user.'
field :jira_account_id, GraphQL::Types::String, null: false,
description: 'Account ID of the Jira user.'
description: 'Account ID of the Jira user.'
field :jira_display_name, GraphQL::Types::String, null: false,
description: 'Display name of the Jira user.'
description: 'Display name of the Jira user.'
field :jira_email,
GraphQL::Types::String,
null: true,
description: 'Email of the Jira user, returned only for users with public emails.'
GraphQL::Types::String,
null: true,
description: 'Email of the Jira user, returned only for users with public emails.'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -5,12 +5,12 @@ module Types
graphql_name 'JiraUsersMappingInputType'
argument :gitlab_id,
GraphQL::Types::Int,
required: false,
description: 'ID of the GitLab user.'
GraphQL::Types::Int,
required: false,
description: 'ID of the GitLab user.'
argument :jira_account_id,
GraphQL::Types::String,
required: true,
description: 'Jira account ID of the user.'
GraphQL::Types::String,
required: true,
description: 'Jira account ID of the user.'
end
end

View File

@ -8,9 +8,9 @@ module Types
description 'Configuration details for an Agent'
field :agent_name,
GraphQL::Types::String,
null: true,
description: 'Name of the agent.'
GraphQL::Types::String,
null: true,
description: 'Name of the agent.'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -8,20 +8,20 @@ module Types
description 'Connection details for an Agent'
field :connected_at,
Types::TimeType,
null: true,
description: 'When the connection was established.'
Types::TimeType,
null: true,
description: 'When the connection was established.'
field :connection_id,
GraphQL::Types::BigInt,
null: true,
description: 'ID of the connection.'
GraphQL::Types::BigInt,
null: true,
description: 'ID of the connection.'
field :metadata,
Types::Kas::AgentMetadataType,
method: :agent_meta,
null: true,
description: 'Information about the Agent.'
Types::Kas::AgentMetadataType,
method: :agent_meta,
null: true,
description: 'Information about the Agent.'
def connected_at
Time.at(object.connected_at.seconds)

View File

@ -8,25 +8,25 @@ module Types
description 'Information about a connected Agent'
field :version,
GraphQL::Types::String,
null: true,
description: 'Agent version tag.'
GraphQL::Types::String,
null: true,
description: 'Agent version tag.'
field :commit,
GraphQL::Types::String,
method: :commit_id,
null: true,
description: 'Agent version commit.'
GraphQL::Types::String,
method: :commit_id,
null: true,
description: 'Agent version commit.'
field :pod_namespace,
GraphQL::Types::String,
null: true,
description: 'Namespace of the pod running the Agent.'
GraphQL::Types::String,
null: true,
description: 'Namespace of the pod running the Agent.'
field :pod_name,
GraphQL::Types::String,
null: true,
description: 'Name of the pod running the Agent.'
GraphQL::Types::String,
null: true,
description: 'Name of the pod running the Agent.'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -6,12 +6,12 @@ module Types
description 'Represents an SSH key.'
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of when the key was created.'
description: 'Timestamp of when the key was created.'
field :expires_at, Types::TimeType, null: false,
description: "Timestamp of when the key expires. It's null if it never expires."
description: "Timestamp of when the key expires. It's null if it never expires."
field :id, GraphQL::Types::ID, null: false, description: 'ID of the key.'
field :key, GraphQL::Types::String, null: false, method: :publishable_key,
description: 'Public key of the key pair.'
description: 'Public key of the key pair.'
field :title, GraphQL::Types::String, null: false, description: 'Title of the key.'
end
end

View File

@ -9,24 +9,24 @@ module Types
authorize :read_label
field :color, GraphQL::Types::String, null: false,
description: 'Background color of the label.'
description: 'Background color of the label.'
field :created_at, Types::TimeType, null: false,
description: 'When this label was created.'
description: 'When this label was created.'
field :description,
GraphQL::Types::String,
null: true,
description: 'Description of the label (Markdown rendered as HTML for caching).'
GraphQL::Types::String,
null: true,
description: 'Description of the label (Markdown rendered as HTML for caching).'
field :id, GraphQL::Types::ID, null: false,
description: 'Label ID.'
description: 'Label ID.'
field :lock_on_merge, GraphQL::Types::Boolean, null: false,
description: 'Indicates this label is locked for merge requests ' \
'that have been merged.'
description: 'Indicates this label is locked for merge requests ' \
'that have been merged.'
field :text_color, GraphQL::Types::String, null: false,
description: 'Text color of the label.'
description: 'Text color of the label.'
field :title, GraphQL::Types::String, null: false,
description: 'Content of the label.'
description: 'Content of the label.'
field :updated_at, Types::TimeType, null: false,
description: 'When this label was last updated.'
description: 'When this label was last updated.'
markdown_field :description_html, null: true
end

View File

@ -5,31 +5,31 @@ module Types
include BaseInterface
field :id, GraphQL::Types::ID, null: false,
description: 'ID of the member.'
description: 'ID of the member.'
field :access_level, Types::AccessLevelType, null: true,
description: 'GitLab::Access level.'
description: 'GitLab::Access level.'
field :created_by, Types::UserType, null: true,
description: 'User that authorized membership.'
description: 'User that authorized membership.'
field :created_at, Types::TimeType, null: true,
description: 'Date and time the membership was created.'
description: 'Date and time the membership was created.'
field :updated_at, Types::TimeType, null: true,
description: 'Date and time the membership was last updated.'
description: 'Date and time the membership was last updated.'
field :expires_at, Types::TimeType, null: true,
description: 'Date and time the membership expires.'
description: 'Date and time the membership expires.'
field :user, Types::UserType, null: true,
description: 'User that is associated with the member object.'
description: 'User that is associated with the member object.'
field :merge_request_interaction, Types::UserMergeRequestInteractionType,
null: true,
description: 'Find a merge request.' do
argument :id, ::Types::GlobalIDType[::MergeRequest], required: true, description: 'Global ID of the merge request.'
end
null: true,
description: 'Find a merge request.' do
argument :id, ::Types::GlobalIDType[::MergeRequest], required: true, description: 'Global ID of the merge request.'
end
definition_methods do
def resolve_type(object, context)

View File

@ -4,9 +4,9 @@ module Types
# rubocop: disable Graphql/AuthorizeTypes
class MergeRequestConnectionType < Types::CountableConnectionType
field :total_time_to_merge,
GraphQL::Types::Float,
null: true,
description: 'Total sum of time to merge, in seconds, for the collection of merge requests.'
GraphQL::Types::Float,
null: true,
description: 'Total sum of time to merge, in seconds, for the collection of merge requests.'
# rubocop: disable CodeReuse/ActiveRecord
def total_time_to_merge

View File

@ -17,219 +17,219 @@ module Types
present_using MergeRequestPresenter
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of when the merge request was created.'
description: 'Timestamp of when the merge request was created.'
field :description, GraphQL::Types::String, null: true,
description: 'Description of the merge request (Markdown rendered as HTML for caching).'
description: 'Description of the merge request (Markdown rendered as HTML for caching).'
field :diff_head_sha, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Diff head SHA of the merge request.'
description: 'Diff head SHA of the merge request.'
field :diff_refs, Types::DiffRefsType, null: true,
description: 'References of the base SHA, the head SHA, and the start SHA for this merge request.'
description: 'References of the base SHA, the head SHA, and the start SHA for this merge request.'
field :diff_stats, [Types::DiffStatsType], null: true, calls_gitaly: true,
description: 'Details about which files were changed in this merge request.' do
description: 'Details about which files were changed in this merge request.' do
argument :path, GraphQL::Types::String, required: false, description: 'Specific file path.'
end
field :draft, GraphQL::Types::Boolean, method: :draft?, null: false,
description: 'Indicates if the merge request is a draft.'
description: 'Indicates if the merge request is a draft.'
field :id, GraphQL::Types::ID, null: false,
description: 'ID of the merge request.'
description: 'ID of the merge request.'
field :iid, GraphQL::Types::String, null: false,
description: 'Internal ID of the merge request.'
description: 'Internal ID of the merge request.'
field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the merge has been set to auto-merge.'
description: 'Indicates if the merge has been set to auto-merge.'
field :merged_at, Types::TimeType, null: true, complexity: 5,
description: 'Timestamp of when the merge request was merged, null if not merged.'
description: 'Timestamp of when the merge request was merged, null if not merged.'
field :project, Types::ProjectType, null: false,
description: 'Alias for target_project.'
description: 'Alias for target_project.'
field :project_id, GraphQL::Types::Int, null: false, method: :target_project_id,
description: 'ID of the merge request project.'
description: 'ID of the merge request project.'
field :source_branch, GraphQL::Types::String, null: false,
description: 'Source branch of the merge request.'
description: 'Source branch of the merge request.'
field :source_branch_protected, GraphQL::Types::Boolean, null: false, calls_gitaly: true,
description: 'Indicates if the source branch is protected.'
description: 'Indicates if the source branch is protected.'
field :source_project, Types::ProjectType, null: true,
description: 'Source project of the merge request.'
description: 'Source project of the merge request.'
field :source_project_id, GraphQL::Types::Int, null: true,
description: 'ID of the merge request source project.'
description: 'ID of the merge request source project.'
field :state, MergeRequestStateEnum, null: false,
description: 'State of the merge request.'
description: 'State of the merge request.'
field :target_branch, GraphQL::Types::String, null: false,
description: 'Target branch of the merge request.'
description: 'Target branch of the merge request.'
field :target_project, Types::ProjectType, null: false,
description: 'Target project of the merge request.'
description: 'Target project of the merge request.'
field :target_project_id, GraphQL::Types::Int, null: false,
description: 'ID of the merge request target project.'
description: 'ID of the merge request target project.'
field :title, GraphQL::Types::String, null: false,
description: 'Title of the merge request.'
description: 'Title of the merge request.'
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of when the merge request was last updated.'
description: 'Timestamp of when the merge request was last updated.'
field :allow_collaboration, GraphQL::Types::Boolean, null: true,
description: 'Indicates if members of the target project can push to the fork.'
description: 'Indicates if members of the target project can push to the fork.'
field :default_merge_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default merge commit message of the merge request.'
description: 'Default merge commit message of the merge request.'
field :default_squash_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default squash commit message of the merge request.'
description: 'Default squash commit message of the merge request.'
field :diff_stats_summary, Types::DiffStatsSummaryType, null: true, calls_gitaly: true,
description: 'Summary of which files were changed in this merge request.'
description: 'Summary of which files were changed in this merge request.'
field :diverged_from_target_branch, GraphQL::Types::Boolean,
null: false, calls_gitaly: true,
method: :diverged_from_target_branch?,
description: 'Indicates if the source branch is behind the target branch.'
null: false, calls_gitaly: true,
method: :diverged_from_target_branch?,
description: 'Indicates if the source branch is behind the target branch.'
field :downvotes, GraphQL::Types::Int,
null: false,
description: 'Number of downvotes for the merge request.',
resolver: Resolvers::DownVotesCountResolver
null: false,
description: 'Number of downvotes for the merge request.',
resolver: Resolvers::DownVotesCountResolver
field :force_remove_source_branch, GraphQL::Types::Boolean, method: :force_remove_source_branch?, null: true,
description: 'Indicates if the project settings will lead to source branch deletion after merge.'
description: 'Indicates if the project settings will lead to source branch deletion after merge.'
field :in_progress_merge_commit_sha, GraphQL::Types::String, null: true,
description: 'Commit SHA of the merge request if merge is in progress.'
description: 'Commit SHA of the merge request if merge is in progress.'
field :merge_commit_sha, GraphQL::Types::String, null: true,
description: 'SHA of the merge request commit (set once merged).'
description: 'SHA of the merge request commit (set once merged).'
field :merge_error, GraphQL::Types::String, null: true,
description: 'Error message due to a merge error.'
description: 'Error message due to a merge error.'
field :merge_ongoing, GraphQL::Types::Boolean, method: :merge_ongoing?, null: false,
description: 'Indicates if a merge is currently occurring.'
description: 'Indicates if a merge is currently occurring.'
field :merge_status, GraphQL::Types::String, method: :public_merge_status, null: true,
description: 'Status of the merge request.',
deprecated: { reason: :renamed, replacement: 'MergeRequest.mergeStatusEnum', milestone: '14.0' }
description: 'Status of the merge request.',
deprecated: { reason: :renamed, replacement: 'MergeRequest.mergeStatusEnum', milestone: '14.0' }
field :merge_status_enum, ::Types::MergeRequests::MergeStatusEnum,
method: :public_merge_status, null: true,
description: 'Merge status of the merge request.'
method: :public_merge_status, null: true,
description: 'Merge status of the merge request.'
field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, null: true,
calls_gitaly: true,
description: 'Detailed merge status of the merge request.'
calls_gitaly: true,
description: 'Detailed merge status of the merge request.'
field :mergeability_checks, [::Types::MergeRequests::MergeabilityCheckType],
null: false,
description: 'Status of all mergeability checks of the merge request.',
method: :all_mergeability_checks_results,
alpha: { milestone: '16.5' },
calls_gitaly: true
null: false,
description: 'Status of all mergeability checks of the merge request.',
method: :all_mergeability_checks_results,
alpha: { milestone: '16.5' },
calls_gitaly: true
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'
calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'
field :rebase_commit_sha, GraphQL::Types::String, null: true,
description: 'Rebase commit SHA of the merge request.'
description: 'Rebase commit SHA of the merge request.'
field :rebase_in_progress, GraphQL::Types::Boolean, method: :rebase_in_progress?, null: false, calls_gitaly: true,
description: 'Indicates if there is a rebase currently in progress for the merge request.'
description: 'Indicates if there is a rebase currently in progress for the merge request.'
field :should_be_rebased, GraphQL::Types::Boolean, method: :should_be_rebased?, null: false, calls_gitaly: true,
description: 'Indicates if the merge request will be rebased.'
description: 'Indicates if the merge request will be rebased.'
field :should_remove_source_branch, GraphQL::Types::Boolean, method: :should_remove_source_branch?, null: true,
description: 'Indicates if the source branch of the merge request will be deleted after merge.'
description: 'Indicates if the source branch of the merge request will be deleted after merge.'
field :source_branch_exists, GraphQL::Types::Boolean,
null: false, calls_gitaly: true,
method: :source_branch_exists?,
description: 'Indicates if the source branch of the merge request exists.'
null: false, calls_gitaly: true,
method: :source_branch_exists?,
description: 'Indicates if the source branch of the merge request exists.'
field :target_branch_exists, GraphQL::Types::Boolean,
null: false, calls_gitaly: true,
method: :target_branch_exists?,
description: 'Indicates if the target branch of the merge request exists.'
null: false, calls_gitaly: true,
method: :target_branch_exists?,
description: 'Indicates if the target branch of the merge request exists.'
field :upvotes, GraphQL::Types::Int,
null: false,
description: 'Number of upvotes for the merge request.',
resolver: Resolvers::UpVotesCountResolver
null: false,
description: 'Number of upvotes for the merge request.',
resolver: Resolvers::UpVotesCountResolver
field :user_discussions_count, GraphQL::Types::Int, null: true,
description: 'Number of user discussions in the merge request.',
resolver: Resolvers::UserDiscussionsCountResolver
description: 'Number of user discussions in the merge request.',
resolver: Resolvers::UserDiscussionsCountResolver
field :user_notes_count, GraphQL::Types::Int, null: true,
description: 'User notes count of the merge request.',
resolver: Resolvers::UserNotesCountResolver
description: 'User notes count of the merge request.',
resolver: Resolvers::UserNotesCountResolver
field :web_path,
GraphQL::Types::String,
null: false,
description: 'Web path of the merge request.'
GraphQL::Types::String,
null: false,
description: 'Web path of the merge request.'
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the merge request.'
description: 'Web URL of the merge request.'
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :diff_head_pipeline,
description: 'Pipeline running on the branch HEAD of the merge request.'
description: 'Pipeline running on the branch HEAD of the merge request.'
field :pipelines,
null: true,
description: 'Pipelines for the merge request. Note: for performance reasons, no more than the most recent 500 pipelines will be returned.',
resolver: Resolvers::MergeRequestPipelinesResolver
null: true,
description: 'Pipelines for the merge request. Note: for performance reasons, no more than the most recent 500 pipelines will be returned.',
resolver: Resolvers::MergeRequestPipelinesResolver
field :assignees,
type: Types::MergeRequests::AssigneeType.connection_type,
null: true,
complexity: 5,
description: 'Assignees of the merge request.'
type: Types::MergeRequests::AssigneeType.connection_type,
null: true,
complexity: 5,
description: 'Assignees of the merge request.'
field :author, Types::MergeRequests::AuthorType, null: true,
description: 'User who created this merge request.'
description: 'User who created this merge request.'
field :discussion_locked, GraphQL::Types::Boolean,
description: 'Indicates if comments on the merge request are locked to members only.',
null: false
description: 'Indicates if comments on the merge request are locked to members only.',
null: false
field :human_time_estimate, GraphQL::Types::String, null: true,
description: 'Human-readable time estimate of the merge request.'
description: 'Human-readable time estimate of the merge request.'
field :human_total_time_spent, GraphQL::Types::String, null: true,
description: 'Human-readable total time reported as spent on the merge request.'
description: 'Human-readable total time reported as spent on the merge request.'
field :labels, Types::LabelType.connection_type,
null: true, complexity: 5,
description: 'Labels of the merge request.',
resolver: Resolvers::BulkLabelsResolver
null: true, complexity: 5,
description: 'Labels of the merge request.',
resolver: Resolvers::BulkLabelsResolver
field :milestone, Types::MilestoneType, null: true,
description: 'Milestone of the merge request.'
description: 'Milestone of the merge request.'
field :participants, Types::MergeRequests::ParticipantType.connection_type, null: true, complexity: 15,
description: 'Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes.',
resolver: Resolvers::Users::ParticipantsResolver
description: 'Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes.',
resolver: Resolvers::Users::ParticipantsResolver
field :reference, GraphQL::Types::String, null: false, method: :to_reference,
description: 'Internal reference of the merge request. Returned in shortened format by default.' do
description: 'Internal reference of the merge request. Returned in shortened format by default.' do
argument :full, GraphQL::Types::Boolean, required: false, default_value: false,
description: 'Boolean option specifying whether the reference should be returned in full.'
description: 'Boolean option specifying whether the reference should be returned in full.'
end
field :auto_merge_enabled, GraphQL::Types::Boolean, null: false,
description: 'Indicates if auto merge is enabled for the merge request.'
description: 'Indicates if auto merge is enabled for the merge request.'
field :commit_count, GraphQL::Types::Int, null: true, method: :commits_count,
description: 'Number of commits in the merge request.'
description: 'Number of commits in the merge request.'
field :conflicts, GraphQL::Types::Boolean, null: false, method: :cannot_be_merged?,
description: 'Indicates if the merge request has conflicts.'
description: 'Indicates if the merge request has conflicts.'
field :reviewers,
type: Types::MergeRequests::ReviewerType.connection_type,
null: true,
complexity: 5,
description: 'Users from whom a review has been requested.'
type: Types::MergeRequests::ReviewerType.connection_type,
null: true,
complexity: 5,
description: 'Users from whom a review has been requested.'
field :subscribed, GraphQL::Types::Boolean, method: :subscribed?, null: false, complexity: 5,
description: 'Indicates if the currently logged in user is subscribed to this merge request.'
description: 'Indicates if the currently logged in user is subscribed to this merge request.'
field :supports_lock_on_merge, GraphQL::Types::Boolean, null: false, method: :supports_lock_on_merge?,
description: 'Indicates if the merge request supports locked labels.'
description: 'Indicates if the merge request supports locked labels.'
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: Types::TaskCompletionStatus.description
description: Types::TaskCompletionStatus.description
field :time_estimate, GraphQL::Types::Int, null: false,
description: 'Time estimate of the merge request.'
description: 'Time estimate of the merge request.'
field :total_time_spent, GraphQL::Types::Int, null: false,
description: 'Total time (in seconds) reported as spent on the merge request.'
description: 'Total time (in seconds) reported as spent on the merge request.'
field :approved, GraphQL::Types::Boolean,
method: :approved?,
null: false, calls_gitaly: true,
description: 'Indicates if the merge request has all the required approvals.'
method: :approved?,
null: false, calls_gitaly: true,
description: 'Indicates if the merge request has all the required approvals.'
field :approved_by, Types::UserType.connection_type, null: true,
description: 'Users who approved the merge request.', method: :approved_by_users
description: 'Users who approved the merge request.', method: :approved_by_users
field :auto_merge_strategy, GraphQL::Types::String, null: true,
description: 'Selected auto merge strategy.'
description: 'Selected auto merge strategy.'
field :available_auto_merge_strategies, [GraphQL::Types::String], null: true, calls_gitaly: true,
description: 'Array of available auto merge strategies.'
description: 'Array of available auto merge strategies.'
field :commits, Types::CommitType.connection_type, null: true,
calls_gitaly: true, description: 'Merge request commits.'
calls_gitaly: true, description: 'Merge request commits.'
field :commits_without_merge_commits, Types::CommitType.connection_type, null: true,
calls_gitaly: true, description: 'Merge request commits excluding merge commits.'
calls_gitaly: true, description: 'Merge request commits excluding merge commits.'
field :committers, Types::UserType.connection_type, null: true, complexity: 5,
calls_gitaly: true, description: 'Users who have added commits to the merge request.'
calls_gitaly: true, description: 'Users who have added commits to the merge request.'
field :has_ci, GraphQL::Types::Boolean, null: false, method: :has_ci?,
description: 'Indicates if the merge request has CI.'
description: 'Indicates if the merge request has CI.'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request or set it to auto-merge.'
description: 'User who merged this merge request or set it to auto-merge.'
field :mergeable, GraphQL::Types::Boolean, null: false, method: :mergeable?, calls_gitaly: true,
description: 'Indicates if the merge request is mergeable.'
description: 'Indicates if the merge request is mergeable.'
field :security_auto_fix, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.', deprecated: { reason: 'Security Auto Fix experiment feature was removed. It was always hidden behind `security_auto_fix` feature flag', milestone: '16.11' }
@ -238,7 +238,7 @@ module Types
field :squash_on_merge, GraphQL::Types::Boolean, null: false, method: :squash_on_merge?,
description: 'Indicates if the merge request will be squashed when merged.'
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the merge request.'
description: 'Timelogs on the merge request.'
field :award_emoji, Types::AwardEmojis::AwardEmojiType.connection_type,
null: true,
@ -251,7 +251,7 @@ module Types
resolver: ::Resolvers::CodequalityReportsComparerResolver
field :prepared_at, Types::TimeType, null: true,
description: 'Timestamp of when the merge request was prepared.'
description: 'Timestamp of when the merge request was prepared.'
field :allows_multiple_assignees,
GraphQL::Types::Boolean,

View File

@ -7,53 +7,53 @@ module Types
description 'Detailed representation of whether a GitLab merge request can be merged.'
value 'UNCHECKED',
value: :unchecked,
description: 'Merge status has not been checked.'
value: :unchecked,
description: 'Merge status has not been checked.'
value 'CHECKING',
value: :checking,
description: 'Currently checking for mergeability.'
value: :checking,
description: 'Currently checking for mergeability.'
value 'MERGEABLE',
value: :mergeable,
description: 'Branch can be merged.'
value: :mergeable,
description: 'Branch can be merged.'
value 'COMMITS_STATUS',
value: :commits_status,
description: 'Source branch exists and contains commits.'
value: :commits_status,
description: 'Source branch exists and contains commits.'
value 'CI_MUST_PASS',
value: :ci_must_pass,
description: 'Pipeline must succeed before merging.'
value: :ci_must_pass,
description: 'Pipeline must succeed before merging.'
value 'CI_STILL_RUNNING',
value: :ci_still_running,
description: 'Pipeline is still running.'
value: :ci_still_running,
description: 'Pipeline is still running.'
value 'DISCUSSIONS_NOT_RESOLVED',
value: :discussions_not_resolved,
description: 'Discussions must be resolved before merging.'
value: :discussions_not_resolved,
description: 'Discussions must be resolved before merging.'
value 'DRAFT_STATUS',
value: :draft_status,
description: 'Merge request must not be draft before merging.'
value: :draft_status,
description: 'Merge request must not be draft before merging.'
value 'NOT_OPEN',
value: :not_open,
description: 'Merge request must be open before merging.'
value: :not_open,
description: 'Merge request must be open before merging.'
value 'NOT_APPROVED',
value: :not_approved,
description: 'Merge request must be approved before merging.'
value: :not_approved,
description: 'Merge request must be approved before merging.'
value 'BLOCKED_STATUS',
value: :merge_request_blocked,
description: 'Merge request dependencies must be merged.'
value: :merge_request_blocked,
description: 'Merge request dependencies must be merged.'
value 'EXTERNAL_STATUS_CHECKS',
value: :status_checks_must_pass,
description: 'Status checks must pass.'
value: :status_checks_must_pass,
description: 'Status checks must pass.'
value 'PREPARING',
value: :preparing,
description: 'Merge request diff is being created.'
value: :preparing,
description: 'Merge request diff is being created.'
value 'JIRA_ASSOCIATION',
value: :jira_association_missing,
description: 'Either the title or description must reference a Jira issue.'
value: :jira_association_missing,
description: 'Either the title or description must reference a Jira issue.'
value 'CONFLICT',
value: :conflict,
description: 'There are conflicts between the source and target branches.'
value: :conflict,
description: 'There are conflicts between the source and target branches.'
value 'NEED_REBASE',
value: :need_rebase,
description: 'Merge request needs to be rebased.'
value: :need_rebase,
description: 'Merge request needs to be rebased.'
end
end
end

View File

@ -7,10 +7,10 @@ module Types
included do
field :merge_request_interaction,
type: ::Types::UserMergeRequestInteractionType,
null: true,
extras: [:parent],
description: "Details of this user's interactions with the merge request."
type: ::Types::UserMergeRequestInteractionType,
null: true,
extras: [:parent],
description: "Details of this user's interactions with the merge request."
end
def merge_request_interaction(parent:, id: nil)

View File

@ -7,20 +7,20 @@ module Types
description 'Representation of whether a GitLab merge request can be merged.'
value 'UNCHECKED',
value: 'unchecked',
description: 'Merge status has not been checked.'
value: 'unchecked',
description: 'Merge status has not been checked.'
value 'CHECKING',
value: 'checking',
description: 'Currently checking for mergeability.'
value: 'checking',
description: 'Currently checking for mergeability.'
value 'CAN_BE_MERGED',
value: 'can_be_merged',
description: 'There are no conflicts between the source and target branches.'
value: 'can_be_merged',
description: 'There are no conflicts between the source and target branches.'
value 'CANNOT_BE_MERGED',
value: 'cannot_be_merged',
description: 'There are conflicts between the source and target branches.'
value: 'cannot_be_merged',
description: 'There are conflicts between the source and target branches.'
value 'CANNOT_BE_MERGED_RECHECK',
value: 'cannot_be_merged_recheck',
description: 'Currently unchecked. The previous state was `CANNOT_BE_MERGED`.'
value: 'cannot_be_merged_recheck',
description: 'Currently unchecked. The previous state was `CANNOT_BE_MERGED`.'
end
end
end

View File

@ -25,8 +25,6 @@ class ContainerRepository < ApplicationRecord
# from the cache expiration time.
AUTH_TOKEN_USAGE_RESERVED_TIME_IN_SECS = 5
TooManyImportsError = Class.new(StandardError)
belongs_to :project
validates :name, length: { minimum: 0, allow_nil: false }

View File

@ -22,6 +22,8 @@ module Projects
security_training_enabled: project.security_training_available?,
continuous_vulnerability_scans_enabled: continuous_vulnerability_scans_enabled,
container_scanning_for_registry_enabled: container_scanning_for_registry_enabled,
pre_receive_secret_detection_available:
Gitlab::CurrentSettings.current_application_settings.pre_receive_secret_detection_enabled,
pre_receive_secret_detection_enabled: pre_receive_secret_detection_enabled
}
end
@ -80,7 +82,14 @@ module Projects
end
def scan_types
::Security::SecurityJobsFinder.allowed_job_types + ::Security::LicenseComplianceJobsFinder.allowed_job_types
job_types = ::Security::SecurityJobsFinder.allowed_job_types +
::Security::LicenseComplianceJobsFinder.allowed_job_types
unless Feature.enabled?(:pre_receive_secret_detection_beta_release)
job_types.delete(:pre_receive_secret_detection)
end
job_types
end
def project_settings

View File

@ -37,10 +37,6 @@ module Auth
access_token(names_and_actions)
end
def self.import_access_token
access_token({ 'import' => %w[*] }, 'registry')
end
def self.pull_access_token(*names)
names_and_actions = names.index_with { %w[pull] }
access_token(names_and_actions)

View File

@ -31,7 +31,6 @@
= render_if_exists 'groups/settings/merge_requests/merge_requests', expanded: expanded, group: @group
= render_if_exists 'groups/settings/merge_requests/merge_request_approval_settings', expanded: expanded, group: @group, user: current_user
= render_if_exists 'groups/analytics', expanded: expanded
%section.settings.no-animate#js-badge-settings{ class: ('expanded' if expanded) }
.settings-header

View File

@ -0,0 +1,9 @@
---
name: bitbucket_server_notes_separate_worker
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/451129
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151126
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/456262
milestone: '17.0'
type: development
group: group::import and integrate
default_enabled: false

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class PrepareAsyncIndexRemovalForVulnerabilities < Gitlab::Database::Migration[2.2]
milestone '17.0'
disable_ddl_transaction!
INDEX_NAME = 'index_vulnerabilities_on_detected_at_and_id'
# TODO: Index to be destroyed synchronously in follow-up issue in https://gitlab.com/gitlab-org/gitlab/-/issues/458022
def up
prepare_async_index_removal :vulnerabilities, [:detected_at, :id], name: INDEX_NAME
end
def down
unprepare_async_index :vulnerabilities, [:detected_at, :id], name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
69e556ca82e36b84303e3d3da5b66c95f7fb71762e33b8b5d312642166491f76

View File

@ -34261,6 +34261,7 @@ The status of the security scan.
| <a id="securityreporttypeenumcoverage_fuzzing"></a>`COVERAGE_FUZZING` | COVERAGE FUZZING scan report. |
| <a id="securityreporttypeenumdast"></a>`DAST` | DAST scan report. |
| <a id="securityreporttypeenumdependency_scanning"></a>`DEPENDENCY_SCANNING` | DEPENDENCY SCANNING scan report. |
| <a id="securityreporttypeenumpre_receive_secret_detection"></a>`PRE_RECEIVE_SECRET_DETECTION` | PRE RECEIVE SECRET DETECTION scan report. |
| <a id="securityreporttypeenumsast"></a>`SAST` | SAST scan report. |
| <a id="securityreporttypeenumsast_iac"></a>`SAST_IAC` | SAST IAC scan report. |
| <a id="securityreporttypeenumsecret_detection"></a>`SECRET_DETECTION` | SECRET DETECTION scan report. |
@ -34278,6 +34279,7 @@ The type of the security scanner.
| <a id="securityscannertypecoverage_fuzzing"></a>`COVERAGE_FUZZING` | Coverage Fuzzing scanner. |
| <a id="securityscannertypedast"></a>`DAST` | DAST scanner. |
| <a id="securityscannertypedependency_scanning"></a>`DEPENDENCY_SCANNING` | Dependency Scanning scanner. |
| <a id="securityscannertypepre_receive_secret_detection"></a>`PRE_RECEIVE_SECRET_DETECTION` | Pre Receive Secret Detection scanner. |
| <a id="securityscannertypesast"></a>`SAST` | SAST scanner. |
| <a id="securityscannertypesast_iac"></a>`SAST_IAC` | Sast Iac scanner. |
| <a id="securityscannertypesecret_detection"></a>`SECRET_DETECTION` | Secret Detection scanner. |

View File

@ -0,0 +1,55 @@
---
stage: AI-powered
group: Custom Models
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
---
# Serve Large Language Models APIs Locally
There are several ways to serve large language models (LLMs) for local or self-deployment purposes.
[MistralAI](https://docs.mistral.ai/deployment/self-deployment/overview/) recommends two different serving frameworks for their models:
- [vLLM](https://docs.vllm.ai/en/latest/): A Python-only serving framework which deploys an API matching OpenAI's spec. vLLM provides a paged attention kernel to improve serving throughput.
- Nvidia's [TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) served with Nvidia's Triton Inference Server: TensorRT-LLM provides a DSL to build fast inference engines with dedicated kernels for large language models. Triton Inference Server allows efficient serving of these inference engines.
These solutions require access to an Nvidia GPU as they rely on the [CUDA](https://developer.nvidia.com/cuda-gpus) graphics API for computation. However, [Ollama](https://ollama.com/download) offers a low configuration cross-platform solution to do it. This is the solution we are going to explore.
## Ollama
[Ollama](https://ollama.com/download) is an open-source framework to help you get up and running with large language models locally. You can serve any [supported LLMs](https://ollama.com/library). You can also make your own and push it to [Hugging Face](https://huggingface.co/).
Be aware that LLMs are usually very heavy to run.
Therefore, we are just going to focus on serving one model, namely [`mistral:instruct`](https://ollama.com/library/mistral:instruct) as it is relatively lightweight to run given its accuracy.
### Setup Ollama
Install Ollama by following these [instructions](https://ollama.com/download) for your OS.
On MacOS, you can alternatively use [Homebrew](https://brew.sh/) by running `brew install ollama` in your terminal.
Once installed, pull the model with `ollama pull mistral:instruct` in your terminal.
If the model was successfully pulled, give it a run with `ollama run mistral:instruct`. Exit the process once you've tested the model.
Now you can use the Ollama server. Visit [`http://localhost:11434/`](http://localhost:11434/); you should see `Ollama is running`. This means your server is already running. If that's not the case, you can run `ollama serve` in your terminal. Use `brew services start ollama` if you installed it with Homebrew.
The Ollama serving framework has an OpenAI-compatible API. The API reference is documented [here](https://github.com/ollama/ollama/blob/main/docs/api.md).
Here is a simple example you can try:
```shell
curl "http://localhost:11434/api/chat" \
--data '{
"model": "mistral:instruct",
"messages": [
{
"role": "user",
"content": "why is the sky blue?"
}
],
"stream": false
}'
```
It runs on the `11434` by default. If you are running into issues because this port is already in use by another application, you can follow [these instructions](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server).

View File

@ -129,7 +129,7 @@ end
### Alternatives
Instead of writing any of these:
Instead, use any of these:
- `expect_next_instance_of`
- `allow_next_instance_of`

View File

@ -4,7 +4,7 @@ group: Source Code
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
---
# Use additional Git commands
# Common Git commands
You can do many Git operations directly in GitLab. However, the command line is required for advanced tasks.

View File

@ -49,10 +49,12 @@ To enable GitLab Duo on a self-managed instance, you must ensure connectivity ex
## Disable GitLab Duo features
You can disable GitLab Duo AI features for a group, project, or instance.
When GitLab Duo is disabled, any attempt to use GitLab Duo features on the group,
When GitLab Duo is disabled, any attempt to use GitLab Duo features on resources like epics,
issues, and vulnerabilities of the group,
project, or instance is blocked and an error is displayed.
GitLab Duo features are also blocked for resources in the group or project, like epics,
issues, and vulnerabilities.
However, the **GitLab Duo Chat** button continues to be displayed in the upper-right corner
and on the left sidebar under **Help**, and users can continue to ask generic questions about GitLab or ask generic code questions.
### Disable for a group

View File

@ -172,8 +172,7 @@ To change the location of a group's dashboards:
1. On the left sidebar, select **Search or go to** and find the project you want to store your dashboard files in.
The project must belong to the group for which you create the dashboards.
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > General**.
1. Expand **Analytics**.
1. Select **Settings > Analytics**.
1. In the **Analytics Dashboards** section, select your dashboard files project.
1. Select **Save changes**.
@ -192,8 +191,8 @@ To change the location of project dashboards:
or select **Create new** (**{plus}**) and **New project/repository**
to create the project to store your dashboard files.
1. On the left sidebar, select **Search or go to** and find the analytics project.
1. Select **Settings > General**.
1. Expand **Analytics**.
1. Select **Settings > Analytics**.
1. Select **Expand** to see custom dashboard projects.
1. In the **Analytics Dashboards** section, select your dashboard files project.
1. Select **Save changes**.

View File

@ -172,8 +172,7 @@ DETAILS:
To enable or disable the overview count aggregation for the Value Streams Dashboard:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > General**.
1. Expand **Analytics**.
1. Select **Settings > Analytics**.
1. In **Value Streams Dashboard**, select or clear the **Enable overview background aggregation for Value Streams Dashboard** checkbox.
To retrieve aggregated usage counts in the group, use the [GraphQL API](../../api/graphql/reference/index.md#groupvaluestreamdashboardusageoverview).
@ -232,8 +231,7 @@ Prerequisites:
- You must have at least the Maintainer role for the group.
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > General**.
1. Expand **Analytics**.
1. Select **Settings > Analytics**.
1. Select the project where you would like to store your YAML configuration file.
1. Select **Save changes**.

View File

@ -46,8 +46,7 @@ You can also use a [project](../../../project/settings/project_access_tokens.md)
## Complete a bootstrap installation
In this section, you'll bootstrap Flux into an empty GitLab repository with the
[`flux bootstrap`](https://fluxcd.io/flux/installation/#gitlab-and-gitlab-enterprise)
command.
[`flux bootstrap`](https://fluxcd.io/flux/installation/bootstrap/gitlab/) command.
To bootstrap a Flux installation:
@ -55,6 +54,7 @@ To bootstrap a Flux installation:
```shell
flux bootstrap gitlab \
--hostname=gitlab.example.org \
--owner=example-org \
--repository=my-repository \
--branch=master \
@ -62,12 +62,22 @@ To bootstrap a Flux installation:
--deploy-token-auth
```
The arguments of `bootstrap` are:
| Argument | Description |
|--------------|-------------|
|`hostname` | Hostname of your GitLab instance. |
|`owner` | GitLab group containing the Flux repository. |
|`repository` | GitLab project containing the Flux repository. |
|`branch` | Git branch the changes are committed to. |
|`path` | File path to a folder where the Flux configuration is stored. |
The bootstrap script does the following:
1. Creates a deploy token and saves it as a Kubernetes `secret`.
1. Creates an empty GitLab project, if the project specified by `--repository` doesn't exist.
1. Generates Flux definition files for your project.
1. Commits the definition files to the specified branch.
1. Creates an empty GitLab project, if the project specified by the `--repository` argument doesn't exist.
1. Generates Flux definition files for your project in a folder specified by the `--path` argument.
1. Commits the definition files to the branch specified by the `--branch` argument.
1. Applies the definition files to your cluster.
After you run the script, Flux will be ready to manage itself and any other resources
@ -104,6 +114,7 @@ In the next step, you'll use Flux to install `agentk` in your cluster.
## Install `agentk`
Next, use Flux to create a namespace for `agentk` and install it in your cluster.
Keep in mind it takes a few minutes for Flux to pick up and apply configuration changes defined in the repository.
This tutorial uses the namespace `gitlab` for `agentk`.

View File

@ -65,6 +65,19 @@ module BitbucketServer
self.class.convert_timestamp(created_date)
end
def to_hash
{
id: id,
committer_user: committer_user,
committer_email: committer_email,
merge_timestamp: merge_timestamp,
merge_commit: merge_commit,
approver_username: approver_username,
approver_email: approver_email,
created_at: created_at
}
end
private
def commit

View File

@ -76,6 +76,21 @@ module BitbucketServer
@comments ||= flatten_comments
end
def to_hash
parent_comment_note = { note: parent_comment.note } if parent_comment
{
id: id,
author_email: author_email,
author_username: author_username,
note: note,
created_at: created_at,
updated_at: updated_at,
comments: comments.map(&:to_hash),
parent_comment: parent_comment_note
}
end
private
# In order to provide context for each reply, we need to track

View File

@ -66,6 +66,16 @@ module BitbucketServer
comment_anchor.fetch('path')
end
def to_hash
super.merge(
from_sha: from_sha,
to_sha: to_sha,
file_path: file_path,
old_pos: old_pos,
new_pos: new_pos
)
end
private
def file_type

View File

@ -8,18 +8,6 @@ module ContainerRegistry
CANCEL_RESPONSE_STATUS_HEADER = 'status'
GITLAB_REPOSITORIES_PATH = '/gitlab/v1/repositories'
IMPORT_RESPONSES = {
200 => :already_imported,
202 => :ok,
400 => :bad_request,
401 => :unauthorized,
404 => :not_found,
409 => :already_being_imported,
424 => :pre_import_failed,
425 => :already_being_imported,
429 => :too_many_imports
}.freeze
RENAME_RESPONSES = {
202 => :accepted,
204 => :ok,
@ -118,46 +106,6 @@ module ContainerRegistry
false
end
# Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def pre_import_repository(path)
response = start_import_for(path, pre: true)
IMPORT_RESPONSES.fetch(response.status, :error)
end
# Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def import_repository(path)
response = start_import_for(path, pre: false)
IMPORT_RESPONSES.fetch(response.status, :error)
end
# Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def cancel_repository_import(path, force: false)
response = with_import_token_faraday do |faraday_client|
faraday_client.delete(import_url_for(path)) do |req|
req.params['force'] = true if force
end
end
status = IMPORT_RESPONSES.fetch(response.status, :error)
actual_state = response.body[CANCEL_RESPONSE_STATUS_HEADER]
{ status: status, migration_state: actual_state }
end
# Deprecated. Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/409873.
def import_status(path)
with_import_token_faraday do |faraday_client|
response = faraday_client.get(import_url_for(path))
# Temporary solution for https://gitlab.com/gitlab-org/gitlab/-/issues/356085#solutions
# this will trigger a `retry_pre_import`
break 'pre_import_failed' unless response.success?
body_hash = response_body(response)
body_hash&.fetch('status') || 'error'
end
end
# https://gitlab.com/gitlab-org/container-registry/-/blob/master/docs/spec/gitlab/api.md#get-repository-details
def repository_details(path, sizing: nil)
with_token_faraday do |faraday_client|
@ -263,33 +211,10 @@ module ContainerRegistry
private
def start_import_for(path, pre:)
with_import_token_faraday do |faraday_client|
faraday_client.put(import_url_for(path)) do |req|
req.params['import_type'] = pre ? 'pre' : 'final'
end
end
end
def with_token_faraday
yield faraday
end
def with_import_token_faraday
yield faraday_with_import_token
end
def faraday_with_import_token(timeout_enabled: true)
@faraday_with_import_token ||= faraday_base(timeout_enabled: timeout_enabled) do |conn|
# initialize the connection with the :import_token instead of :token
initialize_connection(conn, @options.merge(token: @options[:import_token]), &method(:configure_connection))
end
end
def import_url_for(path)
"/gitlab/v1/import/#{path}/"
end
# overrides the default configuration
def configure_connection(conn)
conn.headers['Accept'] = [JSON_TYPE]

View File

@ -9,9 +9,7 @@ module ContainerRegistry
@options = options
@path = @options[:path] || default_path
@client = ContainerRegistry::Client.new(@uri, @options)
import_token = Auth::ContainerRegistryAuthenticationService.import_access_token
@gitlab_api_client = ContainerRegistry::GitlabApiClient.new(@uri, @options.merge(import_token: import_token))
@gitlab_api_client = ContainerRegistry::GitlabApiClient.new(@uri, @options)
end
private

View File

@ -7,6 +7,23 @@ module Gitlab
include ParallelScheduling
def execute
bitbucket_server_notes_separate_worker_enabled =
project.import_data&.data&.dig('bitbucket_server_notes_separate_worker')
if bitbucket_server_notes_separate_worker_enabled
import_notes_individually
else
import_notes_in_batch
end
job_waiter
end
private
attr_reader :project
def import_notes_in_batch
project.merge_requests.find_each do |merge_request|
# Needs to come before `already_processed?` as `jobs_remaining` resets to zero when the job restarts and
# jobs_remaining needs to be the total amount of enqueued jobs
@ -24,21 +41,95 @@ module Gitlab
job_waiter
end
private
def import_notes_individually
merge_request_collection.find_each do |merge_request|
log_info(
import_stage: 'import_notes',
message: "importing merge request #{merge_request.iid} notes"
)
attr_reader :project
activities = client.activities(project_key, repository_slug, merge_request.iid)
activities.each do |activity|
process_comment(merge_request, activity)
end
mark_merge_request_processed(merge_request)
end
end
def process_comment(merge_request, activity)
if activity.comment?
return enqueue_comment_import(merge_request, 'inline', activity.comment) if activity.inline_comment?
return enqueue_comment_import(merge_request, 'standalone_notes', activity.comment)
end
return enqueue_comment_import(merge_request, 'merge_event', activity) if activity.merge_event?
enqueue_comment_import(merge_request, 'approved_event', activity) if activity.approved_event?
end
def enqueue_comment_import(merge_request, comment_type, comment)
job_waiter.jobs_remaining = Gitlab::Cache::Import::Caching.increment(job_waiter_remaining_cache_key)
return if already_processed?(comment)
job_delay = calculate_job_delay(job_waiter.jobs_remaining)
object_hash = {
iid: merge_request.iid,
comment_type: comment_type,
comment_id: comment.id,
comment: comment.to_hash.deep_stringify_keys
}
sidekiq_worker_class.perform_in(job_delay, project.id, object_hash, job_waiter.key)
mark_as_processed(comment)
end
def sidekiq_worker_class
ImportPullRequestNotesWorker
end
def id_for_already_processed_cache(merge_request)
merge_request.iid
def id_for_already_processed_cache(object)
# :iid is used for the `import_notes_in_batch` which uses `merge_request` as the `object`
# it can be cleaned up after `import_notes_in_batch` is removed
object.try(:iid) || generate_activity_key(object)
end
def generate_activity_key(object)
# we need to add key prefix to avoid `id` collision between `activity` and `comment`
key_prefix = if object.try(:approved_event?) || object.try(:merge_event?)
"activity"
else
"comment"
end
"#{key_prefix}-#{object.id}"
end
def collection_method
:notes
end
def merge_request_processed_cache_key
"bitbucket-server-importer/already-processed/merge_request/#{project.id}"
end
def mark_merge_request_processed(merge_request)
Gitlab::Cache::Import::Caching.set_add(
merge_request_processed_cache_key,
merge_request.iid
)
end
def already_processed_merge_requests
Gitlab::Cache::Import::Caching.values_from_set(merge_request_processed_cache_key)
end
def merge_request_collection
project.merge_requests.where.not(iid: already_processed_merge_requests) # rubocop: disable CodeReuse/ActiveRecord -- no need to move this to ActiveRecord model
end
end
end
end

View File

@ -23,20 +23,14 @@ module Gitlab
merge_request = project.merge_requests.find_by(iid: object[:iid]) # rubocop: disable CodeReuse/ActiveRecord
if merge_request
activities = client.activities(project_key, repository_slug, merge_request.iid)
bitbucket_server_notes_separate_worker_enabled =
project.import_data&.data&.dig('bitbucket_server_notes_separate_worker')
comments, other_activities = activities.partition(&:comment?)
merge_event = other_activities.find(&:merge_event?)
import_merge_event(merge_request, merge_event) if merge_event
inline_comments, pr_comments = comments.partition(&:inline_comment?)
import_inline_comments(inline_comments.map(&:comment), merge_request)
import_standalone_pr_comments(pr_comments.map(&:comment), merge_request)
approved_events = other_activities.select(&:approved_event?)
approved_events.each { |event| import_approved_event(merge_request, event) }
if bitbucket_server_notes_separate_worker_enabled
import_notes_individually(merge_request, object)
else
import_notes_in_batch(merge_request)
end
end
log_info(import_stage: 'import_pull_request_notes', message: 'finished', iid: object[:iid])
@ -46,6 +40,44 @@ module Gitlab
attr_reader :object, :project, :formatter, :user_finder, :mentions_converter
def import_notes_individually(merge_request, object)
# We should not use "OpenStruct"
# currently it is used under development feature flag
object_representation = Gitlab::Json.parse(
object[:comment].to_json,
symbolize_names: true,
object_class: 'OpenStruct'.constantize
)
case object[:comment_type]
when 'merge_event'
import_merge_event(merge_request, object_representation)
when 'inline'
import_inline_comments([object_representation], merge_request)
when 'standalone_notes'
import_standalone_pr_comments([object_representation], merge_request)
when 'approved_event'
import_approved_event(merge_request, object_representation)
end
end
def import_notes_in_batch(merge_request)
activities = client.activities(project_key, repository_slug, merge_request.iid)
comments, other_activities = activities.partition(&:comment?)
merge_event = other_activities.find(&:merge_event?)
import_merge_event(merge_request, merge_event) if merge_event
inline_comments, pr_comments = comments.partition(&:inline_comment?)
import_inline_comments(inline_comments.map(&:comment), merge_request)
import_standalone_pr_comments(pr_comments.map(&:comment), merge_request)
approved_events = other_activities.select(&:approved_event?)
approved_events.each { |event| import_approved_event(merge_request, event) }
end
def import_data_valid?
project.import_data&.credentials && project.import_data&.data
end

View File

@ -17,6 +17,9 @@ module Gitlab
end
def execute
bitbucket_server_notes_separate_worker_enabled =
Feature.enabled?(:bitbucket_server_notes_separate_worker, current_user)
::Projects::CreateService.new(
current_user,
name: name,
@ -29,7 +32,12 @@ module Gitlab
import_url: repo.clone_url,
import_data: {
credentials: session_data,
data: { project_key: project_key, repo_slug: repo_slug, timeout_strategy: timeout_strategy }
data: {
project_key: project_key,
repo_slug: repo_slug,
timeout_strategy: timeout_strategy,
bitbucket_server_notes_separate_worker: bitbucket_server_notes_separate_worker_enabled
}
},
skip_wiki: true
).execute

View File

@ -69,9 +69,18 @@ module Gitlab
'user/application_security/container_scanning/index', anchor: 'configuration'),
type: 'container_scanning'
},
pre_receive_secret_detection: {
name: _('Pre-receive Secret Detection'),
description: _('Block secrets such as keys and API tokens from being pushed to your repositories. ' \
'Pre-receive secret detection is triggered when commits are pushed to a repository. ' \
'If any secrets are detected, the push is blocked.'),
help_path: Gitlab::Routing.url_helpers.help_page_path(
'user/application_security/secret_detection/pre_receive/index'),
type: 'pre_receive_secret_detection'
},
secret_detection: {
name: _('Secret Detection'),
description: _('Analyze your source code and Git history for secrets.'),
name: _('Pipeline Secret Detection'),
description: _('Analyze your source code and Git history for secrets by using CI/CD pipelines.'),
help_path: Gitlab::Routing.url_helpers.help_page_path(
'user/application_security/secret_detection/pipeline/index'),
configuration_help_path: Gitlab::Routing.url_helpers.help_page_path(

View File

@ -5707,6 +5707,9 @@ msgstr ""
msgid "Analytics|Analytics dashboards"
msgstr ""
msgid "Analytics|Analytics settings for '%{group_name}' were successfully updated."
msgstr ""
msgid "Analytics|Analytics settings for '%{project_name}' were successfully updated."
msgstr ""
@ -5968,6 +5971,9 @@ msgstr ""
msgid "Analytics|URL"
msgstr ""
msgid "Analytics|Unable to update analytics settings. Please try again."
msgstr ""
msgid "Analytics|Updating dashboard %{dashboardSlug}"
msgstr ""
@ -6028,7 +6034,7 @@ msgstr ""
msgid "Analyze your infrastructure as code configuration files for known vulnerabilities."
msgstr ""
msgid "Analyze your source code and Git history for secrets."
msgid "Analyze your source code and Git history for secrets by using CI/CD pipelines."
msgstr ""
msgid "Analyze your source code for known vulnerabilities."
@ -8501,6 +8507,9 @@ msgstr ""
msgid "BlobViewer|View on %{environmentName}"
msgstr ""
msgid "Block secrets such as keys and API tokens from being pushed to your repositories. Pre-receive secret detection is triggered when commits are pushed to a repository. If any secrets are detected, the push is blocked."
msgstr ""
msgid "Block user"
msgstr ""
@ -10868,9 +10877,6 @@ msgstr ""
msgid "CiCatalog|GitLab-maintained"
msgstr ""
msgid "CiCatalog|Go to the project"
msgstr ""
msgid "CiCatalog|How do I publish a component?"
msgstr ""
@ -10898,9 +10904,6 @@ msgstr ""
msgid "CiCatalog|Released %{date}"
msgstr ""
msgid "CiCatalog|Released %{timeAgo}"
msgstr ""
msgid "CiCatalog|Released %{timeAgo} by %{author}"
msgstr ""
@ -19845,6 +19848,9 @@ msgstr ""
msgid "Enter a number"
msgstr ""
msgid "Enter a number from 0 to 100."
msgstr ""
msgid "Enter admin mode"
msgstr ""
@ -35946,12 +35952,6 @@ msgstr ""
msgid "Organization|Current organization"
msgstr ""
msgid "Organization|Frequently visited groups"
msgstr ""
msgid "Organization|Frequently visited projects"
msgstr ""
msgid "Organization|Get started with organizations"
msgstr ""
@ -36033,6 +36033,18 @@ msgstr ""
msgid "Organization|Public - The organization can be accessed without any authentication."
msgstr ""
msgid "Organization|Recently created groups"
msgstr ""
msgid "Organization|Recently created projects"
msgstr ""
msgid "Organization|Recently updated groups"
msgstr ""
msgid "Organization|Recently updated projects"
msgstr ""
msgid "Organization|Search for an organization"
msgstr ""
@ -37373,6 +37385,9 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
msgid "Pipeline Secret Detection"
msgstr ""
msgid "Pipeline URL"
msgstr ""
@ -38636,6 +38651,9 @@ msgstr ""
msgid "Pre-defined push rules"
msgstr ""
msgid "Pre-receive Secret Detection"
msgstr ""
msgid "Pre-receive secret detection skipped via"
msgstr ""
@ -45922,12 +45940,24 @@ msgstr ""
msgid "Secret token."
msgstr ""
msgid "SecretDetection|Pre-receive Secret Detection is disabled"
msgstr ""
msgid "SecretDetection|Pre-receive Secret Detection is enabled"
msgstr ""
msgid "SecretDetection|This comment appears to have a token in it. Are you sure you want to add it?"
msgstr ""
msgid "SecretDetection|This description appears to have a token in it. Are you sure you want to add it?"
msgstr ""
msgid "SecretDetection|This feature has been disabled at the instance level. Please reach out to your instance administrator to request activation."
msgstr ""
msgid "SecretDetection||Feature not available"
msgstr ""
msgid "Secrets"
msgstr ""
@ -46234,6 +46264,9 @@ msgstr ""
msgid "SecurityConfiguration|The status of the tools only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}."
msgstr ""
msgid "SecurityConfiguration|Toggle Pre-receive secret detection"
msgstr ""
msgid "SecurityConfiguration|Upgrade or start a free trial"
msgstr ""

View File

@ -63,7 +63,7 @@
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.97.0",
"@gitlab/ui": "80.0.0",
"@gitlab/web-ide": "^0.0.1-dev-20240422132849",
"@gitlab/web-ide": "^0.0.1-dev-20240501001436",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-1",
"@rails/ujs": "7.0.8-1",

View File

@ -48,13 +48,15 @@ module QA
"/personal_access_tokens/#{id}"
rescue NoValueError
user.reload! unless user.id
# Filtering on create_after significantly reduces query time. Set to 3 days ago as expiry date is 2 days.
created_after = Time.now.utc.to_date - 3
api_client = Runtime::API::Client.new(:gitlab,
is_new_session: false,
user: user,
personal_access_token: token)
request_url = Runtime::API::Request.new(api_client,
"/personal_access_tokens?user_id=#{user.id}",
"/personal_access_tokens?user_id=#{user.id}&created_after=#{created_after}&revoked=false&state=active",
per_page: '100').url
token = auto_paginated_response(request_url).find { |t| t[:name] == name }

View File

@ -69,11 +69,11 @@ module QA
end
end
def public_email
@public_email ||= begin
api_public_email = api_resource&.dig(:public_email)
def commit_email
@commit_email ||= begin
api_commit_email = api_resource&.dig(:commit_email)
api_public_email && !api_public_email.empty? ? api_public_email : Runtime::User.default_email
api_commit_email && !api_commit_email.empty? ? api_commit_email : Runtime::User.default_email
end
end

View File

@ -43,7 +43,7 @@ module QA
Page::Project::Commit::Show.perform(&:select_email_patches)
expect(page).to have_content(/From: "?#{Regexp.escape(@user.name)}"? <#{@user.public_email}>/)
expect(page).to have_content(/From: "?#{Regexp.escape(@user.name)}"? <#{@user.commit_email}>/)
expect(page).to have_content('Subject: [PATCH] Add second file')
expect(page).to have_content('diff --git a/second b/second')
end

View File

@ -6,7 +6,7 @@ RSpec.describe QA::Resource::User do
name: "GitLab QA",
username: "gitlab-qa",
web_url: "https://staging.gitlab.com/gitlab-qa",
public_email: "1614863-gitlab-qa@users.noreply.staging.gitlab.com"
commit_email: "1614863-gitlab-qa@users.noreply.staging.gitlab.com"
}
end
@ -65,21 +65,21 @@ RSpec.describe QA::Resource::User do
end
end
describe '#public_email' do
describe '#commit_email' do
it 'defaults to QA::Runtime::User.default_email' do
expect(subject.public_email).to eq(QA::Runtime::User.default_email)
expect(subject.commit_email).to eq(QA::Runtime::User.default_email)
end
it 'retrieves the public_email from the api_resource if present' do
it 'retrieves the commit_email from the api_resource if present' do
subject.__send__(:api_resource=, api_resource)
expect(subject.public_email).to eq(api_resource[:public_email])
expect(subject.commit_email).to eq(api_resource[:commit_email])
end
it 'defaults to QA::Runtime::User.default_email if the public_email from the api_resource is blank' do
subject.__send__(:api_resource=, api_resource.merge(public_email: ''))
it 'defaults to QA::Runtime::User.default_email if the commit_email from the api_resource is blank' do
subject.__send__(:api_resource=, api_resource.merge(commit_email: ''))
expect(subject.public_email).to eq(QA::Runtime::User.default_email)
expect(subject.commit_email).to eq(QA::Runtime::User.default_email)
end
end

View File

@ -4,12 +4,27 @@
require 'gitlab'
class SetPipelineName
DOCS = ['docs lint', 'docs-lint links'].freeze
DOCS = ['docs-lint markdown', 'docs-lint links'].freeze
RSPEC_PREDICTIVE = ['rspec:predictive:trigger', 'rspec-ee:predictive:trigger'].freeze
CODE = ['retrieve-tests-metadata'].freeze
QA_GDK = ['e2e:test-on-gdk'].freeze
REVIEW_APP = ['start-review-app-pipeline'].freeze
QA = ['package-and-qa', 'e2e:package-and-test-ee', 'e2e:package-and-test-ce'].freeze
# TODO: Please remove `trigger-omnibus-and-follow-up-e2e` and `follow-up-e2e:package-and-test-ee`
# after 2025-04-08 in this project
#
# `trigger-omnibus-and-follow-up-e2e` was renamed to `follow-up:trigger-omnibus` on 2024-04-08 via
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147908/diffs?pin=c11467759d7eae77ed84e02a5445e21704c8d8e5#c11467759d7eae77ed84e02a5445e21704c8d8e5_105_104
#
# `follow-up-e2e:package-and-test-ee` was renamed to `follow-up:e2e:package-and-test-ee` on 2024-04-08 via
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147908/diffs?pin=c11467759d7eae77ed84e02a5445e21704c8d8e5#c11467759d7eae77ed84e02a5445e21704c8d8e5_136_137
QA = [
'e2e:package-and-test-ce',
'e2e:package-and-test-ee',
'follow-up-e2e:package-and-test-ee',
'follow-up:e2e:package-and-test-ee',
'follow-up:trigger-omnibus',
'trigger-omnibus-and-follow-up-e2e'
].freeze
# Ordered by expected duration, DESC
PIPELINE_TYPES_ORDERED = %w[qa review-app qa-gdk code rspec-predictive docs].freeze

View File

@ -1,118 +0,0 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
describe('CiResourceAbout', () => {
let wrapper;
const defaultProps = {
isLoadingSharedData: false,
isLoadingDetails: false,
openIssuesCount: 4,
openMergeRequestsCount: 9,
latestVersion: {
id: 1,
name: 'v1.0.0',
path: 'path/to/release',
createdAt: '2022-08-23T17:19:09Z',
},
webPath: 'path/to/project',
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(CiResourceAbout, {
propsData: {
...defaultProps,
...props,
},
});
};
const findProjectLink = () => wrapper.findByText('Go to the project');
const findIssueCount = () => wrapper.findByText(`${defaultProps.openIssuesCount} issues`);
const findMergeRequestCount = () =>
wrapper.findByText(`${defaultProps.openMergeRequestsCount} merge requests`);
const findLastRelease = () =>
wrapper.findByText(`Released ${getTimeago().format(defaultProps.latestVersion.createdAt)}`);
const findAllLoadingItems = () => wrapper.findAllByTestId('skeleton-loading-line');
// Shared data items are items which gets their data from the index page query.
const sharedDataItems = [findProjectLink, findLastRelease];
// additional details items gets their state only when on the details page
const additionalDetailsItems = [findIssueCount, findMergeRequestCount];
const allItems = [...sharedDataItems, ...additionalDetailsItems];
describe('when loading shared data', () => {
beforeEach(() => {
createComponent({ props: { isLoadingSharedData: true, isLoadingDetails: true } });
});
it('renders all server-side data as loading', () => {
allItems.forEach((finder) => {
expect(finder().exists()).toBe(false);
});
expect(findAllLoadingItems()).toHaveLength(allItems.length);
});
});
describe('when loading additional details', () => {
beforeEach(() => {
createComponent({ props: { isLoadingDetails: true } });
});
it('renders only the details query as loading', () => {
sharedDataItems.forEach((finder) => {
expect(finder().exists()).toBe(true);
});
additionalDetailsItems.forEach((finder) => {
expect(finder().exists()).toBe(false);
});
expect(findAllLoadingItems()).toHaveLength(additionalDetailsItems.length);
});
});
describe('when has loaded', () => {
beforeEach(() => {
createComponent();
});
it('renders project link', () => {
expect(findProjectLink().exists()).toBe(true);
});
it('renders the number of issues opened', () => {
expect(findIssueCount().exists()).toBe(true);
});
it('renders the number of merge requests opened', () => {
expect(findMergeRequestCount().exists()).toBe(true);
});
it('renders the last release date', () => {
expect(findLastRelease().exists()).toBe(true);
});
describe('links', () => {
it('has the correct project link', () => {
expect(findProjectLink().attributes('href')).toBe(defaultProps.webPath);
});
it('has the correct issues link', () => {
expect(findIssueCount().attributes('href')).toBe(`${defaultProps.webPath}/issues`);
});
it('has the correct merge request link', () => {
expect(findMergeRequestCount().attributes('href')).toBe(
`${defaultProps.webPath}/merge_requests`,
);
});
it('has no link for release data', () => {
expect(findLastRelease().attributes('href')).toBe(undefined);
});
});
});
});

View File

@ -3,25 +3,19 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
import CiVerificationBadge from '~/ci/catalog/components/shared/ci_verification_badge.vue';
import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
import { catalogSharedDataMock } from '../../mock';
describe('CiResourceHeader', () => {
let wrapper;
const resource = { ...catalogSharedDataMock.data.ciCatalogResource };
const resourceAdditionalData = { ...catalogAdditionalDetailsMock.data.ciCatalogResource };
const defaultProps = {
openIssuesCount: resourceAdditionalData.openIssuesCount,
openMergeRequestsCount: resourceAdditionalData.openMergeRequestsCount,
isLoadingDetails: false,
isLoadingSharedData: false,
isLoadingData: false,
resource,
};
const findAboutComponent = () => wrapper.findComponent(CiResourceAbout);
const findReportAbuseButton = () => wrapper.findByTestId('report-abuse-button');
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findAvatar = () => wrapper.findComponent(GlAvatar);
@ -66,10 +60,6 @@ describe('CiResourceHeader', () => {
entityName: name,
});
});
it('does not render the catalog about section', () => {
expect(findAboutComponent().exists()).toBe(false);
});
});
describe('Version badge', () => {

View File

@ -9,7 +9,6 @@ import { cacheConfig } from '~/ci/catalog/graphql/settings';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import getCiCatalogResourceSharedData from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
import getCiCatalogResourceDetails from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql';
import CiResourceDetails from '~/ci/catalog/components/details/ci_resource_details.vue';
import CiResourceDetailsPage from '~/ci/catalog/components/pages/ci_resource_details_page.vue';
@ -18,7 +17,7 @@ import CiResourceHeaderSkeletonLoader from '~/ci/catalog/components/details/ci_r
import { createRouter } from '~/ci/catalog/router/index';
import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
import { catalogSharedDataMock } from '../../mock';
Vue.use(VueApollo);
Vue.use(VueRouter);
@ -26,12 +25,10 @@ Vue.use(VueRouter);
let router;
const defaultSharedData = { ...catalogSharedDataMock.data.ciCatalogResource };
const defaultAdditionalData = { ...catalogAdditionalDetailsMock.data.ciCatalogResource };
describe('CiResourceDetailsPage', () => {
let wrapper;
let sharedDataResponse;
let additionalDataResponse;
const defaultProps = {};
@ -45,10 +42,7 @@ describe('CiResourceDetailsPage', () => {
const findHeaderSkeletonLoader = () => wrapper.findComponent(CiResourceHeaderSkeletonLoader);
const createComponent = ({ props = {} } = {}) => {
const handlers = [
[getCiCatalogResourceSharedData, sharedDataResponse],
[getCiCatalogResourceDetails, additionalDataResponse],
];
const handlers = [[getCiCatalogResourceSharedData, sharedDataResponse]];
const mockApollo = createMockApollo(handlers, undefined, cacheConfig);
@ -70,7 +64,6 @@ describe('CiResourceDetailsPage', () => {
beforeEach(async () => {
sharedDataResponse = jest.fn();
additionalDataResponse = jest.fn();
router = createRouter();
await router.push({
@ -85,7 +78,6 @@ describe('CiResourceDetailsPage', () => {
// By mocking a return value and not a promise, we skip the loading
// to simulate having the pre-fetched query
sharedDataResponse.mockReturnValueOnce(catalogSharedDataMock);
additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
createComponent();
});
@ -97,8 +89,7 @@ describe('CiResourceDetailsPage', () => {
sharedDataResponse.mockReturnValueOnce(catalogSharedDataMock);
expect(findHeaderComponent().props()).toMatchObject({
isLoadingDetails: true,
isLoadingSharedData: false,
isLoadingData: false,
});
});
});
@ -106,7 +97,6 @@ describe('CiResourceDetailsPage', () => {
describe('and shared data is not pre-fetched', () => {
beforeEach(() => {
sharedDataResponse.mockResolvedValue(catalogSharedDataMock);
additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
createComponent();
});
@ -116,8 +106,7 @@ describe('CiResourceDetailsPage', () => {
it('passes all loading state to the header component as true', () => {
expect(findHeaderComponent().props()).toMatchObject({
isLoadingDetails: true,
isLoadingSharedData: true,
isLoadingData: true,
});
});
});
@ -127,7 +116,6 @@ describe('CiResourceDetailsPage', () => {
beforeEach(async () => {
const mockError = new Error('error');
sharedDataResponse.mockRejectedValue(mockError);
additionalDataResponse.mockRejectedValue(mockError);
createComponent();
await waitForPromises();
@ -143,7 +131,6 @@ describe('CiResourceDetailsPage', () => {
describe('when data has loaded', () => {
beforeEach(async () => {
sharedDataResponse.mockResolvedValue(catalogSharedDataMock);
additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
createComponent();
await waitForPromises();
@ -160,10 +147,7 @@ describe('CiResourceDetailsPage', () => {
it('passes expected props', () => {
expect(findHeaderComponent().props()).toMatchObject({
isLoadingDetails: false,
isLoadingSharedData: false,
openIssuesCount: defaultAdditionalData.openIssuesCount,
openMergeRequestsCount: defaultAdditionalData.openMergeRequestsCount,
isLoadingData: false,
resource: defaultSharedData,
});
});

View File

@ -533,19 +533,6 @@ export const catalogSharedDataMock = {
},
};
export const catalogAdditionalDetailsMock = {
data: {
ciCatalogResource: {
__typename: 'CiCatalogResource',
id: `gid://gitlab/CiCatalogResource/1`,
webPath: '/twitter/project',
openIssuesCount: 4,
openMergeRequestsCount: 10,
readmeHtml: '<h1>Hello world</h1>',
},
},
};
const generateResourcesNodes = (count = 20, startId = 0) => {
const nodes = [];
for (let i = startId; i < startId + count; i += 1) {

View File

@ -4,8 +4,9 @@ import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
import {
RESOURCE_TYPE_GROUPS,
RESOURCE_TYPE_PROJECTS,
SORT_ITEM_NAME,
SORT_ITEM_CREATED_AT,
SORT_DIRECTION_DESC,

View File

@ -32,23 +32,33 @@ describe('OrganizationShowGroupsAndProjects', () => {
expect(findCollapsibleListbox().props()).toMatchObject({
items: [
{
value: 'frequently_visited_projects',
text: 'Frequently visited projects',
value: 'updated_at_groups',
text: 'Recently updated groups',
},
{
value: 'frequently_visited_groups',
text: 'Frequently visited groups',
value: 'created_at_groups',
text: 'Recently created groups',
},
{
value: 'updated_at_projects',
text: 'Recently updated projects',
},
{
value: 'created_at_projects',
text: 'Recently created projects',
},
],
selected: 'frequently_visited_projects',
selected: 'updated_at_groups',
});
});
describe.each`
displayQueryParam | expectedViewAllLinkQuery | expectedViewComponent | expectedDisplayListboxSelectedProp
${'frequently_visited_projects'} | ${'?display=projects'} | ${ProjectsView} | ${'frequently_visited_projects'}
${'frequently_visited_groups'} | ${'?display=groups'} | ${GroupsView} | ${'frequently_visited_groups'}
${'unsupported'} | ${'?display=projects'} | ${ProjectsView} | ${'frequently_visited_projects'}
displayQueryParam | expectedViewAllLinkQuery | expectedViewComponent | expectedDisplayListboxSelectedProp
${'created_at_projects'} | ${'?display=projects'} | ${ProjectsView} | ${'created_at_projects'}
${'updated_at_projects'} | ${'?display=projects'} | ${ProjectsView} | ${'updated_at_projects'}
${'created_at_groups'} | ${'?display=groups'} | ${GroupsView} | ${'created_at_groups'}
${'updated_at_groups'} | ${'?display=groups'} | ${GroupsView} | ${'updated_at_groups'}
${'unsupported'} | ${'?display=groups'} | ${GroupsView} | ${'updated_at_groups'}
`(
'when display query param is $displayQueryParam',
({

View File

@ -1,20 +1,20 @@
import { buildDisplayListboxItem } from '~/organizations/show/utils';
import { RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
import { FILTER_FREQUENTLY_VISITED } from '~/organizations/show/constants';
import { SORT_CREATED_AT, RESOURCE_TYPE_PROJECTS } from '~/organizations/shared/constants';
describe('buildDisplayListboxItem', () => {
it('returns list item in correct format', () => {
const text = 'Frequently visited projects';
const text = 'Recently created projects';
expect(
buildDisplayListboxItem({
filter: FILTER_FREQUENTLY_VISITED,
sortName: SORT_CREATED_AT,
resourceType: RESOURCE_TYPE_PROJECTS,
text,
}),
).toEqual({
sortName: SORT_CREATED_AT,
text,
value: 'frequently_visited_projects',
value: 'created_at_projects',
});
});
});

View File

@ -4,10 +4,10 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants';
import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql';
import OrganizationsUsersApp from '~/organizations/users/components/app.vue';
import OrganizationsUsersView from '~/organizations/users/components/users_view.vue';
import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/users/constants';
import {
MOCK_ORGANIZATION_GID,
MOCK_USERS,

View File

@ -10,8 +10,9 @@ import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_al
import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from '~/security_configuration/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import PreReceiveSecretDetectionFeatureCard from '~/security_configuration/components/pre_receive_secret_detection_feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { securityFeaturesMock, provideMock } from '../mock_data';
import { securityFeaturesMock, provideMock, preReceiveSecretDetectionMock } from '../mock_data';
const gitlabCiHistoryPath = 'test/historyPath';
const { vulnerabilityTrainingDocsPath, projectFullPath } = provideMock;
@ -55,6 +56,8 @@ describe('~/security_configuration/components/app', () => {
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findByTestId = (id) => wrapper.findByTestId(id);
const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
const findPreReceiveSecretDetection = () =>
wrapper.findComponent(PreReceiveSecretDetectionFeatureCard);
const findTrainingProviderList = () => wrapper.findComponent(TrainingProviderList);
const findManageViaMRErrorAlert = () => wrapper.findByTestId('manage-via-mr-error-alert');
const findLink = ({ href, text, container = wrapper }) => {
@ -280,6 +283,24 @@ describe('~/security_configuration/components/app', () => {
});
});
describe('With pre receive secret detection', () => {
beforeEach(() => {
createComponent({
augmentedSecurityFeatures: [preReceiveSecretDetectionMock],
});
});
it('does not render feature card component', () => {
expect(findFeatureCards().length).toBe(0);
});
it('renders component with correct props', () => {
expect(findPreReceiveSecretDetection().exists()).toBe(true);
expect(findPreReceiveSecretDetection().props('feature')).toEqual(
preReceiveSecretDetectionMock,
);
});
});
describe('given gitlabCiPresent & gitlabCiHistoryPath props', () => {
beforeEach(() => {
createComponent({

View File

@ -0,0 +1,159 @@
import { GlToggle, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PreReceiveSecretDetectionFeatureCard from '~/security_configuration/components/pre_receive_secret_detection_feature_card.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import ProjectSetPreReceiveSecretDetection from '~/security_configuration/graphql/set_pre_receive_secret_detection.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { preReceiveSecretDetectionMock } from '../mock_data';
Vue.use(VueApollo);
const setMockResponse = {
data: {
setPreReceiveSecretDetection: {
preReceiveSecretDetectionEnabled: true,
errors: [],
},
},
};
const feature = preReceiveSecretDetectionMock;
const defaultProvide = {
preReceiveSecretDetectionAvailable: true,
preReceiveSecretDetectionEnabled: false,
projectFullPath: 'flightjs/flight',
};
describe('PreReceiveSecretDetectionFeatureCard component', () => {
let wrapper;
let apolloProvider;
let requestHandlers;
const createMockApolloProvider = () => {
requestHandlers = {
setMutationHandler: jest.fn().mockResolvedValue(setMockResponse),
};
return createMockApollo([
[ProjectSetPreReceiveSecretDetection, requestHandlers.setMutationHandler],
]);
};
const createComponent = ({ props = {}, provide = {} } = {}) => {
apolloProvider = createMockApolloProvider();
wrapper = extendedWrapper(
shallowMount(PreReceiveSecretDetectionFeatureCard, {
propsData: {
feature,
...props,
},
provide: {
...defaultProvide,
...provide,
},
apolloProvider,
}),
);
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
apolloProvider = null;
});
const findToggle = () => wrapper.findComponent(GlToggle);
const findLink = () => wrapper.findComponent(GlLink);
const findLockIcon = () => wrapper.findComponent(GlIcon);
it('renders correct name and description', () => {
expect(wrapper.text()).toContain(feature.name);
expect(wrapper.text()).toContain(feature.description);
});
it('shows the help link', () => {
const link = findLink();
expect(link.text()).toBe('Learn more');
expect(link.attributes('href')).toBe(feature.helpPath);
});
describe('when feature is available', () => {
beforeEach(() => {
createComponent({
provide: {
preReceiveSecretDetectionAvailable: true,
},
});
});
it('renders toggle in correct default state', () => {
expect(findToggle().props('disabled')).toBe(false);
expect(findToggle().props('value')).toBe(false);
});
it('does not render lock icon', () => {
expect(findLockIcon().exists()).toBe(false);
});
it('calls mutation on toggle change with correct payload', async () => {
expect(findToggle().props('value')).toBe(false);
findToggle().vm.$emit('change', true);
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: defaultProvide.projectFullPath,
enable: true,
},
});
await waitForPromises();
expect(findToggle().props('value')).toBe(true);
expect(wrapper.text()).toContain('Enabled');
});
});
describe('when feature is not available', () => {
beforeEach(() => {
createComponent({
provide: {
preReceiveSecretDetectionAvailable: false,
},
});
});
it('renders correct text', () => {
expect(wrapper.text()).toContain('Not enabled');
});
it('should disable toggle when feature is not configured', () => {
expect(findToggle().props('disabled')).toBe(true);
});
it('renders lock icon', () => {
expect(findLockIcon().exists()).toBe(true);
expect(findLockIcon(wrapper).props('name')).toBe('lock');
});
});
describe('when feature is not available with current license', () => {
beforeEach(() => {
createComponent({
props: {
feature: {
...preReceiveSecretDetectionMock,
available: false,
},
},
});
});
it('should display correct message', () => {
expect(wrapper.text()).toContain('Available with Ultimate');
});
it('should not render toggle', () => {
expect(findToggle().exists()).toBe(false);
});
});
});

View File

@ -3,6 +3,7 @@ import {
SAST_SHORT_NAME,
SAST_IAC_NAME,
SAST_IAC_SHORT_NAME,
PRE_RECEIVE_SECRET_DETECTION,
} from '~/security_configuration/constants';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
@ -194,6 +195,19 @@ export const securityFeaturesMock = [
},
];
export const preReceiveSecretDetectionMock = {
name: 'Pre-receive Secret Detection',
description: `Block secrets such as keys and API tokens from being pushed to your repositories.
'Pre-receive secret detection is triggered when commits are pushed to a repository. ' \
'If any secrets are detected, the push is blocked.`,
helpPath: SAST_HELP_PATH,
configurationHelpPath: helpPagePath(
'user/application_security/secret_detection/pre_receive/index',
),
type: PRE_RECEIVE_SECRET_DETECTION,
available: true,
};
export const provideMock = {
upgradePath: '/upgrade',
autoDevopsHelpPagePath: '/autoDevopsHelpPagePath',

View File

@ -12,24 +12,39 @@ RSpec.describe BitbucketServer::Representation::Activity, feature_category: :imp
describe 'regular comment' do
subject { described_class.new(comment) }
it { expect(subject.id).to eq(11) }
it { expect(subject.comment?).to be_truthy }
it { expect(subject.inline_comment?).to be_falsey }
it { expect(subject.comment).to be_a(BitbucketServer::Representation::Comment) }
it { expect(subject.created_at).to be_a(Time) }
describe '#to_hash' do
it do
expect(subject.to_hash).to match(a_hash_including(id: 11))
end
end
end
describe 'inline comment' do
subject { described_class.new(inline_comment) }
it { expect(subject.id).to eq(19) }
it { expect(subject.comment?).to be_truthy }
it { expect(subject.inline_comment?).to be_truthy }
it { expect(subject.comment).to be_a(BitbucketServer::Representation::PullRequestComment) }
it { expect(subject.created_at).to be_a(Time) }
describe '#to_hash' do
it do
expect(subject.to_hash).to match(a_hash_including(id: 19))
end
end
end
describe 'merge event' do
subject { described_class.new(merge_event) }
it { expect(subject.id).to eq(7) }
it { expect(subject.comment?).to be_falsey }
it { expect(subject.inline_comment?).to be_falsey }
it { expect(subject.committer_user).to eq('root') }
@ -37,6 +52,19 @@ RSpec.describe BitbucketServer::Representation::Activity, feature_category: :imp
it { expect(subject.merge_timestamp).to be_a(Time) }
it { expect(subject.created_at).to be_a(Time) }
it { expect(subject.merge_commit).to eq('839fa9a2d434eb697815b8fcafaecc51accfdbbc') }
describe '#to_hash' do
it do
expect(subject.to_hash).to match(
a_hash_including(
id: 7,
committer_user: 'root',
committer_email: 'test.user@example.com',
merge_commit: '839fa9a2d434eb697815b8fcafaecc51accfdbbc'
)
)
end
end
end
describe 'approved event' do
@ -50,5 +78,17 @@ RSpec.describe BitbucketServer::Representation::Activity, feature_category: :imp
it { expect(subject.approver_username).to eq('slug') }
it { expect(subject.approver_email).to eq('test.user@example.com') }
it { expect(subject.created_at).to be_a(Time) }
describe '#to_hash' do
it do
expect(subject.to_hash).to match(
a_hash_including(
id: 15,
approver_username: 'slug',
approver_email: 'test.user@example.com'
)
)
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe BitbucketServer::Representation::Comment do
RSpec.describe BitbucketServer::Representation::Comment, feature_category: :importers do
let(:activities) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
let(:comment) { activities.first }
@ -77,4 +77,39 @@ RSpec.describe BitbucketServer::Representation::Comment do
expect(fourth.parent_comment).to eq(first)
end
end
describe '#to_hash' do
it do
expect(subject.to_hash).to match(
a_hash_including(
id: 9,
author_email: 'test.user@example.com',
author_username: 'username',
note: 'is this a new line?',
comments: array_including(
hash_including(
note: 'Hello world',
comments: [],
parent_comment: { note: 'is this a new line?' }
),
hash_including(
note: 'Ok',
comments: [],
parent_comment: { note: 'Hello world' }
),
hash_including(
note: 'hi',
comments: [],
parent_comment: { note: 'Hello world' }
),
hash_including(
note: 'hello',
comments: [],
parent_comment: { note: 'is this a new line?' }
)
)
)
)
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe BitbucketServer::Representation::PullRequestComment do
RSpec.describe BitbucketServer::Representation::PullRequestComment, feature_category: :importers do
let(:activities) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
let(:comment) { activities.second }
@ -47,4 +47,19 @@ RSpec.describe BitbucketServer::Representation::PullRequestComment do
describe '#file_path' do
it { expect(subject.file_path).to eq('CHANGELOG.md') }
end
describe '#to_hash' do
it do
expect(subject.to_hash).to match(
a_hash_including(
id: 7,
from_sha: 'c5f4288162e2e6218180779c7f6ac1735bb56eab',
to_sha: 'a4c2164330f2549f67c13f36a93884cf66e976be',
file_path: 'CHANGELOG.md',
old_pos: 9,
new_pos: 11
)
)
end
end
end

View File

@ -9,8 +9,6 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
include_context 'container registry client stubs'
let(:path) { 'namespace/path/to/repository' }
let(:import_token) { 'import_token' }
let(:options) { { token: token, import_token: import_token } }
describe '#supports_gitlab_api?' do
subject { client.supports_gitlab_api? }
@ -67,133 +65,6 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
end
end
describe '#pre_import_repository' do
subject { client.pre_import_repository(path) }
where(:status_code, :expected_result) do
200 | :already_imported
202 | :ok
400 | :bad_request
401 | :unauthorized
404 | :not_found
409 | :already_being_imported
418 | :error
424 | :pre_import_failed
425 | :already_being_imported
429 | :too_many_imports
end
with_them do
before do
stub_pre_import(path, status_code, pre: true)
end
it { is_expected.to eq(expected_result) }
end
end
describe '#import_repository' do
subject { client.import_repository(path) }
where(:status_code, :expected_result) do
200 | :already_imported
202 | :ok
400 | :bad_request
401 | :unauthorized
404 | :not_found
409 | :already_being_imported
418 | :error
424 | :pre_import_failed
425 | :already_being_imported
429 | :too_many_imports
end
with_them do
before do
stub_pre_import(path, status_code, pre: false)
end
it { is_expected.to eq(expected_result) }
end
end
describe '#cancel_repository_import' do
let(:force) { false }
subject { client.cancel_repository_import(path, force: force) }
where(:status_code, :expected_result) do
200 | :already_imported
202 | :ok
400 | :bad_request
401 | :unauthorized
404 | :not_found
409 | :already_being_imported
418 | :error
424 | :pre_import_failed
425 | :already_being_imported
429 | :too_many_imports
end
with_them do
before do
stub_import_cancel(path, status_code, force: force)
end
it { is_expected.to eq({ status: expected_result, migration_state: nil }) }
end
context 'bad request' do
let(:status) { 'this_is_a_test' }
before do
stub_import_cancel(path, 400, status: status, force: force)
end
it { is_expected.to eq({ status: :bad_request, migration_state: status }) }
end
context 'force cancel' do
let(:force) { true }
before do
stub_import_cancel(path, 202, force: force)
end
it { is_expected.to eq({ status: :ok, migration_state: nil }) }
end
end
describe '#import_status' do
subject { client.import_status(path) }
context 'with successful response' do
before do
stub_import_status(path, status)
end
context 'with a status' do
let(:status) { 'this_is_a_test' }
it { is_expected.to eq(status) }
end
context 'with no status' do
let(:status) { nil }
it { is_expected.to eq('error') }
end
end
context 'with non successful response' do
before do
stub_import_status(path, nil, status_code: 404)
end
it { is_expected.to eq('pre_import_failed') }
end
end
describe '#repository_details' do
let(:path) { 'namespace/path/to/repository' }
let(:response) { { foo: :bar, this: :is_a_test } }
@ -927,13 +798,6 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
end
end
def stub_pre_import(path, status_code, pre:)
import_type = pre ? 'pre' : 'final'
stub_request(:put, "#{registry_api_url}/gitlab/v1/import/#{path}/?import_type=#{import_type}")
.with(headers: { 'Accept' => described_class::JSON_TYPE, 'Authorization' => "bearer #{import_token}" })
.to_return(status: status_code, body: '')
end
def stub_registry_gitlab_api_support(supported = true)
status_code = supported ? 200 : 404
stub_request(:get, "#{registry_api_url}/gitlab/v1/")
@ -941,41 +805,6 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
.to_return(status: status_code, body: '')
end
def stub_import_status(path, status, status_code: 200)
stub_request(:get, "#{registry_api_url}/gitlab/v1/import/#{path}/")
.with(headers: { 'Accept' => described_class::JSON_TYPE, 'Authorization' => "bearer #{import_token}" })
.to_return(
status: status_code,
body: { status: status }.to_json,
headers: { content_type: 'application/json' }
)
end
def stub_import_cancel(path, http_status, status: nil, force: false)
body = {}
if http_status == 400
body = { status: status }
end
headers = {
'Accept' => described_class::JSON_TYPE,
'Authorization' => "bearer #{import_token}",
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
}
params = force ? '?force=true' : ''
stub_request(:delete, "#{registry_api_url}/gitlab/v1/import/#{path}/#{params}")
.with(headers: headers)
.to_return(
status: http_status,
body: body.to_json,
headers: { content_type: 'application/json' }
)
end
def stub_repository_details(path, sizing: nil, status_code: 200, respond_with: {})
url = "#{registry_api_url}/gitlab/v1/repositories/#{path}/"
url += "?size=#{sizing}" if sizing

Some files were not shown because too many files have changed in this diff Show More