Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8a2fe0af21
commit
995fb1cd0f
|
|
@ -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'
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
query getCiCatalogResourceDetails($fullPath: ID!) {
|
||||
ciCatalogResource(fullPath: $fullPath) {
|
||||
id
|
||||
webPath
|
||||
openIssuesCount
|
||||
openMergeRequestsCount
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export const FILTER_FREQUENTLY_VISITED = 'frequently_visited';
|
||||
|
||||
export const GROUPS_AND_PROJECTS_PER_PAGE = 5;
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const buildDisplayListboxItem = ({ filter, resourceType, text }) => ({
|
||||
export const buildDisplayListboxItem = ({ sortName, resourceType, text }) => ({
|
||||
text,
|
||||
value: `${filter}_${resourceType}`,
|
||||
value: `${sortName}_${resourceType}`,
|
||||
sortName,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const ORGANIZATION_USERS_PER_PAGE = 20;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
69e556ca82e36b84303e3d3da5b66c95f7fb71762e33b8b5d312642166491f76
|
||||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue