Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
65963de2ae
commit
eb90d59b8c
|
|
@ -1 +1 @@
|
|||
d62dac6422b1a1e4198a14b7b4e831633e85a79a
|
||||
8fa071ac4eb77db878e85ce1fa6bb7bbad18e963
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default {
|
|||
onSuccess(event) {
|
||||
this.beforeDisplayResults();
|
||||
|
||||
const [{ newToken }] = convertEventDetail(event);
|
||||
const [{ newToken, total }] = convertEventDetail(event);
|
||||
this.newToken = newToken;
|
||||
|
||||
this.alert = createAlert({ message: this.alertInfoMessage, variant: VARIANT_INFO });
|
||||
|
|
@ -124,8 +124,7 @@ export default {
|
|||
document.querySelectorAll('.js-token-card').forEach((el) => {
|
||||
el.querySelector('.js-add-new-token-form').style.display = '';
|
||||
el.querySelector('.js-toggle-button').style.display = 'block';
|
||||
el.querySelector('.js-token-count').innerText =
|
||||
parseInt(el.querySelector('.js-token-count').innerText, 10) + 1;
|
||||
el.querySelector('.js-token-count').innerText = total;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export function getActionFromHref(pathName) {
|
|||
export const pageBundles = {
|
||||
show: () => import(/* webpackPrefetch: true */ '~/mr_notes/mount_app'),
|
||||
diffs: () => import(/* webpackPrefetch: true */ '~/diffs'),
|
||||
reports: () => import('ee_else_ce/merge_requests/reports'),
|
||||
reports: () => import('~/merge_requests/reports'),
|
||||
};
|
||||
|
||||
export default class MergeRequestTabs {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
<script>
|
||||
import { GlIcon, GlBadge } from '@gitlab/ui';
|
||||
import {
|
||||
BLOCKERS_ROUTE,
|
||||
CODE_QUALITY_ROUTE,
|
||||
LICENSE_COMPLIANCE_ROUTE,
|
||||
SECURITY_ROUTE,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'MergeRequestReportsApp',
|
||||
components: {
|
||||
GlIcon,
|
||||
GlBadge,
|
||||
},
|
||||
routeNames: {
|
||||
BLOCKERS_ROUTE,
|
||||
CODE_QUALITY_ROUTE,
|
||||
SECURITY_ROUTE,
|
||||
LICENSE_COMPLIANCE_ROUTE,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
blockersCounter: 2,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-grid gl-grid-cols-[1fr] gl-gap-5 md:gl-min-h-31 md:gl-grid-cols-[200px,1fr]">
|
||||
<h2 class="gl-sr-only">{{ s__('MrReports|Reports') }}</h2>
|
||||
<aside
|
||||
class="gl-border-b gl-border-default gl-pb-3 gl-pt-5 md:gl-border-r md:gl-border-0 md:gl-pr-5"
|
||||
>
|
||||
<nav>
|
||||
<router-link
|
||||
:to="{ name: $options.routeNames.BLOCKERS_ROUTE }"
|
||||
active-class="gl-font-bold gl-bg-gray-50"
|
||||
exact
|
||||
class="gl-flex gl-items-center gl-rounded-base gl-p-2 gl-text-default hover:gl-bg-gray-50 hover:gl-text-default hover:gl-no-underline"
|
||||
>
|
||||
<span class="gl-mr-3 gl-flex gl-rounded-full gl-bg-red-100 gl-p-2">
|
||||
<gl-icon class="gl-rounded-full gl-bg-white gl-text-red-500" name="status-failed" />
|
||||
</span>
|
||||
{{ s__('MrReports|Blockers') }}
|
||||
<gl-badge class="gl-ml-auto gl-mr-2" variant="neutral"
|
||||
><span data-testid="blockers-counter" class="gl-font-bold">{{
|
||||
blockersCounter
|
||||
}}</span></gl-badge
|
||||
>
|
||||
</router-link>
|
||||
<div class="gl-border-t gl-mt-2 gl-border-default gl-pt-1">
|
||||
<h3
|
||||
class="gl-heading-6 gl-mb-0 gl-py-3 gl-pl-3 gl-text-sm gl-font-[700] gl-leading-normal"
|
||||
>
|
||||
{{ s__('MrReports|All reports') }}
|
||||
</h3>
|
||||
<ul class="gl-m-0 gl-list-none gl-p-0">
|
||||
<li class="gl-my-1">
|
||||
<router-link
|
||||
:to="{ name: $options.routeNames.CODE_QUALITY_ROUTE }"
|
||||
active-class="gl-font-bold gl-bg-gray-50"
|
||||
class="gl-flex gl-items-center gl-rounded-base gl-p-2 gl-text-default hover:gl-bg-gray-50 hover:gl-text-default hover:gl-no-underline"
|
||||
>
|
||||
<span class="gl-mr-3 gl-flex gl-p-2">
|
||||
<gl-icon
|
||||
class="gl-rounded-full gl-bg-white gl-text-orange-500"
|
||||
name="status-alert"
|
||||
/>
|
||||
</span>
|
||||
{{ s__('MrReports|Code quality') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="gl-my-1">
|
||||
<router-link
|
||||
:to="{ name: $options.routeNames.SECURITY_ROUTE }"
|
||||
active-class="gl-font-bold gl-bg-gray-50"
|
||||
class="gl-flex gl-items-center gl-rounded-base gl-p-2 gl-text-default hover:gl-bg-gray-50 hover:gl-text-default hover:gl-no-underline"
|
||||
>
|
||||
<span class="gl-mr-3 gl-flex gl-p-2">
|
||||
<gl-icon
|
||||
class="gl-rounded-full gl-bg-white gl-text-red-500"
|
||||
name="status-failed"
|
||||
/>
|
||||
</span>
|
||||
{{ s__('MrReports|Security') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="gl-my-1">
|
||||
<router-link
|
||||
:to="{ name: $options.routeNames.LICENSE_COMPLIANCE_ROUTE }"
|
||||
active-class="gl-font-bold gl-bg-gray-50"
|
||||
class="gl-flex gl-items-center gl-rounded-base gl-p-2 gl-text-default hover:gl-bg-gray-50 hover:gl-text-default hover:gl-no-underline"
|
||||
>
|
||||
<span class="gl-mr-3 gl-flex gl-p-2">
|
||||
<gl-icon
|
||||
class="gl-rounded-full gl-bg-white gl-text-red-500"
|
||||
name="status-failed"
|
||||
/>
|
||||
</span>
|
||||
{{ s__('MrReports|License Compliance') }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<section class="md:gl-pt-5">
|
||||
<router-view />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export const BLOCKERS_ROUTE = 'index';
|
||||
export const CODE_QUALITY_ROUTE = 'code-quality';
|
||||
export const SECURITY_ROUTE = 'security';
|
||||
export const LICENSE_COMPLIANCE_ROUTE = 'license-compliance';
|
||||
|
|
@ -1 +1,36 @@
|
|||
export default () => {};
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import routes from './routes';
|
||||
import MergeRequestReportsApp from './components/app.vue';
|
||||
|
||||
export default () => {
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const el = document.getElementById('js-reports-tab');
|
||||
const { projectPath, iid, basePath } = el.dataset;
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
const router = new VueRouter({
|
||||
base: basePath,
|
||||
mode: 'history',
|
||||
routes,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
router,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
projectPath,
|
||||
iid,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(MergeRequestReportsApp);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'MergeRequestReportsBlockersPage',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'MergeRequestReportsIndexPage',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import BlockersPage from 'ee_else_ce/merge_requests/reports/pages/blockers_page.vue';
|
||||
import IndexComponent from './pages/index.vue';
|
||||
import {
|
||||
BLOCKERS_ROUTE,
|
||||
CODE_QUALITY_ROUTE,
|
||||
LICENSE_COMPLIANCE_ROUTE,
|
||||
SECURITY_ROUTE,
|
||||
} from './constants';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
name: BLOCKERS_ROUTE,
|
||||
component: BlockersPage,
|
||||
},
|
||||
{
|
||||
path: '/?type=code-quality',
|
||||
name: CODE_QUALITY_ROUTE,
|
||||
component: IndexComponent,
|
||||
},
|
||||
{
|
||||
path: '/?type=security',
|
||||
name: SECURITY_ROUTE,
|
||||
component: IndexComponent,
|
||||
},
|
||||
{
|
||||
path: '/?type=license-compliance',
|
||||
name: LICENSE_COMPLIANCE_ROUTE,
|
||||
component: IndexComponent,
|
||||
},
|
||||
];
|
||||
|
|
@ -229,7 +229,6 @@ const initTreeHistoryLinkApp = (el) => {
|
|||
{
|
||||
attrs: {
|
||||
href: url.href,
|
||||
class: '!gl-ml-3',
|
||||
},
|
||||
},
|
||||
[__('History')],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { initMrPage } from '~/pages/projects/merge_requests/page';
|
||||
import initReportsApp from '~/merge_requests/reports';
|
||||
|
||||
initMrPage();
|
||||
initReportsApp();
|
||||
|
|
@ -99,13 +99,11 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-ml-5">
|
||||
<gl-loading-icon v-if="isLoading" size="sm" :label="__('Loading pipeline status')" />
|
||||
<ci-icon
|
||||
v-else-if="hasCiStatus"
|
||||
:status="ciStatus"
|
||||
:title="statusTitle"
|
||||
:aria-label="statusTitle"
|
||||
/>
|
||||
</div>
|
||||
<gl-loading-icon v-if="isLoading" size="sm" :label="__('Loading pipeline status')" />
|
||||
<ci-icon
|
||||
v-else-if="hasCiStatus"
|
||||
:status="ciStatus"
|
||||
:title="statusTitle"
|
||||
:aria-label="statusTitle"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const DEFAULT_FETCH_CHUNKS = 50;
|
||||
export const DEFAULT_FETCH_CHUNKS = 5;
|
||||
export const PROJECT_GRAPHQL_ID_TYPE = 'Project';
|
||||
export const GROUP_GRAPHQL_ID_TYPE = 'Group';
|
||||
export const SEARCH_RESULTS_DEBOUNCE = 500;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ fragment Todo on Todo {
|
|||
... on Design {
|
||||
issue {
|
||||
id
|
||||
name
|
||||
reference
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ export default {
|
|||
isAlert() {
|
||||
return this.todo.targetType === TODO_TARGET_TYPE_ALERT;
|
||||
},
|
||||
isDesign() {
|
||||
return this.todo.targetType === TODO_TARGET_TYPE_DESIGN;
|
||||
},
|
||||
isMemberAccessRequestAction() {
|
||||
return this.todo.action === TODO_ACTION_TYPE_MEMBER_ACCESS_REQUESTED;
|
||||
},
|
||||
|
|
@ -72,19 +75,49 @@ export default {
|
|||
(this.isMergeRequest || this.isIssue || this.isAlert) && this.issuableState !== STATUS_OPEN
|
||||
);
|
||||
},
|
||||
targetTitle() {
|
||||
/**
|
||||
* Full title line of the todo title + full reference, joined by a middot
|
||||
*/
|
||||
todoTitle() {
|
||||
return [this.targetName, this.targetFullReference].filter(Boolean).join(' · ');
|
||||
},
|
||||
/**
|
||||
* Right half of a todo title: Full reference to the todo (parentPath + Target Reference)
|
||||
*/
|
||||
targetFullReference() {
|
||||
return [this.parentPath, this.targetReference].filter(Boolean).join(' ');
|
||||
},
|
||||
/**
|
||||
* Left half of a To-Do title, often the entity name
|
||||
*/
|
||||
targetName() {
|
||||
if (this.isMemberAccessRequestAction) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.todo.targetEntity?.name ?? '';
|
||||
const name = this.todo.targetEntity?.name ?? '';
|
||||
|
||||
if (this.isDesign && this.todo.targetEntity?.issue?.name) {
|
||||
if (name) {
|
||||
return `${this.todo.targetEntity.issue.name} › ${name}`;
|
||||
}
|
||||
return this.todo.targetEntity.issue.name;
|
||||
}
|
||||
|
||||
return name;
|
||||
},
|
||||
/**
|
||||
* Reference of the target entity
|
||||
*/
|
||||
targetReference() {
|
||||
if (this.todo.targetEntity?.issue?.reference) {
|
||||
if (this.isDesign && this.todo.targetEntity?.issue?.reference) {
|
||||
return this.todo.targetEntity.issue.reference;
|
||||
}
|
||||
return this.todo.targetEntity?.reference ?? '';
|
||||
},
|
||||
/**
|
||||
* Parent path of the target entity Reference of the target entity
|
||||
*/
|
||||
parentPath() {
|
||||
if (this.todo.group) {
|
||||
return this.todo.group.fullName;
|
||||
|
|
@ -96,21 +129,6 @@ export default {
|
|||
|
||||
return '';
|
||||
},
|
||||
showSeparator() {
|
||||
if (!this.targetTitle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.parentPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.targetReference) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
icon() {
|
||||
switch (this.todo.targetType) {
|
||||
case TODO_TARGET_TYPE_ISSUE:
|
||||
|
|
@ -122,7 +140,7 @@ export default {
|
|||
case TODO_TARGET_TYPE_ALERT:
|
||||
return 'status-alert';
|
||||
case TODO_TARGET_TYPE_DESIGN:
|
||||
return 'issues';
|
||||
return 'media';
|
||||
case TODO_TARGET_TYPE_SSH_KEY:
|
||||
return 'token';
|
||||
case TODO_TARGET_TYPE_EPIC:
|
||||
|
|
@ -140,10 +158,7 @@ export default {
|
|||
<status-badge v-if="showStatusBadge" :issuable-type="issuableType" :state="issuableState" />
|
||||
<gl-icon v-if="icon" :name="icon" />
|
||||
<div class="gl-overflow-hidden gl-text-ellipsis" data-testid="todo-title">
|
||||
<span v-if="targetTitle" class="todo-target-title">{{ targetTitle }}</span>
|
||||
<span v-if="showSeparator">·</span>
|
||||
<span>{{ parentPath }}</span>
|
||||
<span v-if="targetReference">{{ targetReference }}</span>
|
||||
{{ todoTitle }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default {
|
|||
{{ $options.i18n.hiddenTodoBadgeText }}
|
||||
</gl-badge>
|
||||
<div class="gl-overflow-hidden gl-text-ellipsis" data-testid="todo-title">
|
||||
<span class="todo-target-title">{{ $options.i18n.hiddenTodoTitle }}</span>
|
||||
{{ $options.i18n.hiddenTodoTitle }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { GlAvatar, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlAvatarLabeled, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'GroupItem',
|
||||
components: {
|
||||
GlAvatar,
|
||||
GlAvatarLabeled,
|
||||
GlButton,
|
||||
HiddenGroupsItem: () => import('ee_component/approvals/components/hidden_groups_item.vue'),
|
||||
},
|
||||
|
|
@ -46,19 +46,17 @@ export default {
|
|||
<template>
|
||||
<div class="gl-flex gl-items-center gl-gap-3">
|
||||
<hidden-groups-item v-if="isHiddenGroups" class="gl-grow" />
|
||||
<div v-else class="gl-flex gl-grow gl-items-center gl-gap-3">
|
||||
<gl-avatar
|
||||
:alt="fullName"
|
||||
:entity-name="fullName"
|
||||
:size="32"
|
||||
:src="avatarUrl"
|
||||
fallback-on-error
|
||||
/>
|
||||
<span class="gl-flex gl-flex-col">
|
||||
<span class="gl-font-bold">{{ fullName }}</span>
|
||||
<span class="gl-text-subtle">@{{ name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<gl-avatar-labeled
|
||||
v-else
|
||||
class="gl-grow gl-break-all"
|
||||
:entity-name="fullName"
|
||||
:label="fullName"
|
||||
:sub-label="`@${name}`"
|
||||
:size="32"
|
||||
shape="rect"
|
||||
:src="avatarUrl"
|
||||
fallback-on-error
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
v-if="canDelete"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { GlAvatar, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlAvatarLabeled, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'ProjectItem',
|
||||
components: {
|
||||
GlAvatar,
|
||||
GlAvatarLabeled,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
|
|
@ -35,18 +35,16 @@ export default {
|
|||
|
||||
<template>
|
||||
<span class="gl-flex gl-items-center gl-gap-3" @click="$emit('select', name)">
|
||||
<gl-avatar
|
||||
:alt="name"
|
||||
<gl-avatar-labeled
|
||||
class="gl-grow gl-break-all"
|
||||
:entity-name="name"
|
||||
:label="name"
|
||||
:sub-label="data.nameWithNamespace"
|
||||
:size="32"
|
||||
shape="rect"
|
||||
:src="data.avatarUrl"
|
||||
fallback-on-error
|
||||
/>
|
||||
<span class="gl-flex gl-max-w-30 gl-grow gl-flex-col">
|
||||
<span class="gl-font-bold">{{ name }}</span>
|
||||
<span class="gl-text-subtle">{{ data.nameWithNamespace }}</span>
|
||||
</span>
|
||||
|
||||
<gl-button
|
||||
v-if="canDelete"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { GlAvatar, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlAvatarLabeled, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'UserItem',
|
||||
components: {
|
||||
GlAvatar,
|
||||
GlAvatarLabeled,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
|
|
@ -41,11 +41,15 @@ export default {
|
|||
|
||||
<template>
|
||||
<span class="gl-flex gl-items-center gl-gap-3">
|
||||
<gl-avatar :alt="name" :entity-name="name" :size="32" :src="avatarUrl" fallback-on-error />
|
||||
<span class="gl-flex gl-grow gl-flex-col">
|
||||
<span class="gl-font-bold">{{ name }}</span>
|
||||
<span class="gl-text-subtle">@{{ username }}</span>
|
||||
</span>
|
||||
<gl-avatar-labeled
|
||||
class="gl-grow gl-break-all"
|
||||
:entity-name="name"
|
||||
:label="name"
|
||||
:sub-label="`@${username}`"
|
||||
:size="32"
|
||||
:src="avatarUrl"
|
||||
fallback-on-error
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
v-if="canDelete"
|
||||
|
|
|
|||
|
|
@ -28,17 +28,17 @@
|
|||
&:not(.ProseMirror-hideselection) .content-editor-selection,
|
||||
a.ProseMirror-selectednode,
|
||||
span.ProseMirror-selectednode {
|
||||
background-color: $blue-100;
|
||||
box-shadow: 0 2px 0 $blue-100, 0 -2px 0 $blue-100;
|
||||
@apply gl-bg-blue-100;
|
||||
box-shadow: 0 2px 0 var(--blue-100, $blue-100), 0 -2px 0 var(--blue-100, $blue-100);
|
||||
|
||||
&.gfm-project_member, .gfm-project_member {
|
||||
&:not(.current-user) {
|
||||
background-color: $blue-200;
|
||||
@apply gl-bg-blue-200;
|
||||
}
|
||||
}
|
||||
|
||||
&.gfm-project_member {
|
||||
box-shadow: 0 0 0 2px $blue-100;
|
||||
box-shadow: 0 0 0 2px var(--blue-100, $blue-100);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@
|
|||
@import 'framework/sortable';
|
||||
@import 'framework/read_more';
|
||||
@import 'framework/system_messages';
|
||||
@import 'framework/spinner';
|
||||
@import 'framework/card';
|
||||
@import 'framework/source_editor';
|
||||
@import 'framework/diffs';
|
||||
|
|
|
|||
|
|
@ -56,15 +56,6 @@
|
|||
transition: $unfolded-transitions;
|
||||
}
|
||||
|
||||
@mixin disable-all-animation {
|
||||
/*CSS transitions*/
|
||||
transition-property: none !important;
|
||||
/*CSS transforms*/
|
||||
transform: none !important;
|
||||
/*CSS animations*/
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
@function unfold-transition ($transition) {
|
||||
// Default values
|
||||
$property: all;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
@keyframes source-editor-spinner-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
[data-editor-loading] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -10,8 +20,25 @@
|
|||
}
|
||||
|
||||
&::before {
|
||||
$size: 32px;
|
||||
$border-width: 3px;
|
||||
$color: $gray-700;
|
||||
|
||||
content: '';
|
||||
@include spinner-deprecated(32px, 3px);
|
||||
@include webkit-prefix(transform-origin, 50% 50% calc((#{$size} / 2) + #{$border-width}));
|
||||
width: $size;
|
||||
height: $size;
|
||||
border-width: $border-width;
|
||||
border-color: rgba($color, 0.25);
|
||||
border-top-color: $color;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
animation-name: source-editor-spinner-rotate;
|
||||
animation-duration: 0.6s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
border-style: solid;
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* Do not use these spinner mixins. Rely on GitLab UI
|
||||
* GlLoadingIcon component instead.
|
||||
*/
|
||||
@mixin spinner-color-deprecated($color) {
|
||||
border-color: rgba($color, 0.25);
|
||||
border-top-color: $color;
|
||||
}
|
||||
|
||||
@mixin spinner-size-deprecated($size, $border-width) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
border-width: $border-width;
|
||||
@include webkit-prefix(transform-origin, 50% 50% calc((#{$size} / 2) + #{$border-width}));
|
||||
}
|
||||
|
||||
@keyframes spinner-rotate-deprecated {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin spinner-deprecated($size: 16px, $border-width: 2px, $color: $gray-700) {
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
animation-name: spinner-rotate-deprecated;
|
||||
animation-duration: 0.6s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
border-style: solid;
|
||||
display: inline-flex;
|
||||
@include spinner-size-deprecated($size, $border-width);
|
||||
@include spinner-color-deprecated($color);
|
||||
}
|
||||
|
|
@ -88,6 +88,11 @@
|
|||
@apply gl-inline-block gl-mb-0;
|
||||
}
|
||||
|
||||
// make default paragraph of summary text behave nicely with the summary marker
|
||||
summary > p {
|
||||
max-width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
// Single code lines should wrap
|
||||
code {
|
||||
@apply gl-font-monospace;
|
||||
|
|
|
|||
|
|
@ -78,22 +78,6 @@ $white-gc-bg: #eaf2f5;
|
|||
background-color: $gl-color-neutral-10;
|
||||
}
|
||||
|
||||
@mixin diff-match-line {
|
||||
&.expansion {
|
||||
&.match .diff-td {
|
||||
color: $gl-color-neutral-400;
|
||||
}
|
||||
|
||||
.diff-td {
|
||||
background-color: $gl-color-neutral-50;
|
||||
|
||||
&:first-child {
|
||||
border-color: $gl-color-neutral-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin white-base {
|
||||
// Line numbers
|
||||
.file-line-blame {
|
||||
|
|
@ -155,7 +139,19 @@ $white-gc-bg: #eaf2f5;
|
|||
&.diff-grid-row {
|
||||
--diff-expansion-background-color: #{$gl-color-neutral-100};
|
||||
|
||||
@include diff-match-line;
|
||||
&.expansion {
|
||||
&.match .diff-td {
|
||||
color: $gl-color-neutral-400;
|
||||
}
|
||||
|
||||
.diff-td {
|
||||
background-color: $gl-color-neutral-50;
|
||||
|
||||
&:first-child {
|
||||
border-color: $gl-color-neutral-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.match) .diff-grid-left:hover,
|
||||
|
|
|
|||
|
|
@ -147,17 +147,6 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
}
|
||||
}
|
||||
|
||||
@mixin hierarchy-path-chevron {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
top: 0.39rem;
|
||||
right: px-to-rem(-9px);
|
||||
width: $disclosure-hierarchy-chevron-dimension;
|
||||
height: $disclosure-hierarchy-chevron-dimension;
|
||||
transform: rotate(45deg) skew(14deg, 14deg);
|
||||
}
|
||||
|
||||
.disclosure-hierarchy {
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-right: $gl-spacing-scale-7;
|
||||
|
|
@ -189,7 +178,14 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
|
||||
&::before,
|
||||
&::after {
|
||||
@include hierarchy-path-chevron;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
top: 0.39rem;
|
||||
right: px-to-rem(-9px);
|
||||
width: $disclosure-hierarchy-chevron-dimension;
|
||||
height: $disclosure-hierarchy-chevron-dimension;
|
||||
transform: rotate(45deg) skew(14deg, 14deg);
|
||||
border: 1px solid var(--gray-100, $gray-100);
|
||||
border-color: inherit;
|
||||
border-bottom-color: transparent;
|
||||
|
|
|
|||
|
|
@ -68,17 +68,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.commit-actions {
|
||||
.ci-status-icon svg {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
> .btn,
|
||||
> .commit-sha-group {
|
||||
margin-left: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.commit-nav-buttons {
|
||||
margin: 0 0.5rem;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@
|
|||
}
|
||||
|
||||
.gfm-form .note-textarea, .md .ProseMirror {
|
||||
resize: vertical !important;
|
||||
max-height: unset !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.note-image-attach {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
|
||||
.cur {
|
||||
.avatar {
|
||||
@include disable-all-animation;
|
||||
transition-property: none !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
border: 1px solid $white;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ class ApplicationController < BaseActionController
|
|||
render_403
|
||||
end
|
||||
|
||||
rescue_from ActionController::InvalidAuthenticityToken do
|
||||
render_403
|
||||
end
|
||||
|
||||
rescue_from Browser::Error do |e|
|
||||
render plain: e.message, status: :forbidden
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,10 +16,16 @@ module SessionlessAuthentication
|
|||
end
|
||||
|
||||
def sessionless_user?
|
||||
current_user && !session.key?('warden.user.user.key')
|
||||
if Feature.enabled?(:fix_graphql_csrf, Feature.current_request)
|
||||
current_user && @sessionless_sign_in # rubocop:disable Gitlab/ModuleWithInstanceVariables -- This is only used within this module
|
||||
else
|
||||
current_user && !session.key?('warden.user.user.key')
|
||||
end
|
||||
end
|
||||
|
||||
def sessionless_sign_in(user)
|
||||
@sessionless_sign_in = true # rubocop:disable Gitlab/ModuleWithInstanceVariables -- This is only used within this module
|
||||
|
||||
signed_in_user =
|
||||
if user.can_log_in_with_non_expired_password?
|
||||
# Notice we are passing store false, so the user is not
|
||||
|
|
@ -32,7 +38,9 @@ module SessionlessAuthentication
|
|||
sign_in(user, store: false, message: :sessionless_sign_in, run_callbacks: false)
|
||||
end
|
||||
|
||||
reset_auth_user! if respond_to?(:reset_auth_user!, true)
|
||||
if respond_to?(:reset_auth_user!, true) && Feature.disabled?(:fix_graphql_csrf, Feature.current_request)
|
||||
reset_auth_user!
|
||||
end
|
||||
|
||||
signed_in_user
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ class GraphqlController < ApplicationController
|
|||
|
||||
# Unauthenticated users have access to the API for public data
|
||||
skip_before_action :authenticate_user!
|
||||
# This is already handled by authorize_access_api!
|
||||
skip_before_action :active_user_check
|
||||
# CSRF protection is only necessary when the request is authenticated via a session cookie.
|
||||
# Also, we allow anonymous users to access the API without a CSRF token so that it is easier for users
|
||||
# to get started with our GraphQL API.
|
||||
skip_before_action :verify_authenticity_token, if: -> {
|
||||
Feature.enabled?(:fix_graphql_csrf, Feature.current_request) && (current_user.nil? || sessionless_user?)
|
||||
}
|
||||
|
||||
# Header can be passed by tests to disable SQL query limits.
|
||||
DISABLE_SQL_QUERY_LIMIT_HEADER = 'HTTP_X_GITLAB_DISABLE_SQL_QUERY_LIMIT'
|
||||
|
|
@ -20,17 +28,29 @@ class GraphqlController < ApplicationController
|
|||
# storage, since the admin-mode check is session wide.
|
||||
# We can't enable this for anonymous users because that would cause users using
|
||||
# enforced SSO from using an auth token to access the API.
|
||||
skip_around_action :set_session_storage, unless: :current_user
|
||||
skip_around_action :set_session_storage, if: -> {
|
||||
Feature.disabled?(:fix_graphql_csrf, Feature.current_request) && current_user.nil?
|
||||
}
|
||||
|
||||
# Allow missing CSRF tokens, this would mean that if a CSRF is invalid or missing,
|
||||
# the user won't be authenticated but can proceed as an anonymous user.
|
||||
#
|
||||
# If a CSRF is valid, the user is authenticated. This makes it easier to play
|
||||
# around in GraphiQL.
|
||||
protect_from_forgery with: :null_session, only: :execute
|
||||
prepend_before_action do
|
||||
if Feature.disabled?(:fix_graphql_csrf, Feature.current_request)
|
||||
self.forgery_protection_strategy = ProtectionMethods::NullSession
|
||||
end
|
||||
end
|
||||
|
||||
# must come first: current_user is set up here
|
||||
before_action(only: [:execute]) { authenticate_sessionless_user!(:graphql_api) }
|
||||
prepend_before_action(if: -> {
|
||||
Feature.enabled?(:fix_graphql_csrf, Feature.current_request)
|
||||
}) { authenticate_sessionless_user!(:graphql_api) }
|
||||
|
||||
before_action(if: -> {
|
||||
Feature.disabled?(:fix_graphql_csrf, Feature.current_request)
|
||||
}) { authenticate_sessionless_user!(:graphql_api) }
|
||||
|
||||
before_action :authorize_access_api!
|
||||
before_action :set_user_last_activity
|
||||
|
|
@ -82,6 +102,13 @@ class GraphqlController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# ApplicationController has similar rescues but we declare these again here because the
|
||||
# `rescue_from StandardError` above would prevent these from bubbling up to ApplicationController.
|
||||
# These also return errors in a JSON format similar to GraphQL errors.
|
||||
rescue_from ActionController::InvalidAuthenticityToken do |exception|
|
||||
render_error(exception.message, status: :forbidden)
|
||||
end
|
||||
|
||||
rescue_from Gitlab::Auth::TooManyIps do |exception|
|
||||
log_exception(exception)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module UserSettings
|
|||
|
||||
skip_before_action :check_password_expiration, only: [:new, :create]
|
||||
skip_before_action :check_two_factor_requirement, only: [:new, :create]
|
||||
skip_before_action :active_user_check, only: [:new, :create]
|
||||
|
||||
before_action :set_user
|
||||
before_action :authorize_change_password!
|
||||
|
|
@ -14,7 +15,9 @@ module UserSettings
|
|||
|
||||
feature_category :system_access
|
||||
|
||||
def new; end
|
||||
def new
|
||||
current_user.activate if user_signed_in? && current_user.deactivated?
|
||||
end
|
||||
|
||||
def create
|
||||
unless @user.password_automatically_set || @user.valid_password?(user_params[:password])
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class BuildSourceFinder
|
||||
def initialize(relation:, sources:, project:, params: {})
|
||||
raise ArgumentError, 'Only Ci::Builds are source searchable' unless relation.klass == Ci::Build
|
||||
|
||||
@relation = relation
|
||||
@sources = sources
|
||||
@project = project
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
return relation unless sources.present?
|
||||
|
||||
filter_by_source
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :relation, :sources, :project, :params
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord -- Need specialized queries for database optimizations
|
||||
def filter_by_source
|
||||
relation
|
||||
.from("(#{build_source_scope.to_sql}) p_ci_build_sources, LATERAL (#{ci_builds_query.to_sql}) p_ci_builds")
|
||||
end
|
||||
|
||||
def order
|
||||
Gitlab::Pagination::Keyset::Order.build(
|
||||
[
|
||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||
attribute_name: 'build_id',
|
||||
order_expression: Ci::BuildSource.arel_table[:build_id].desc
|
||||
),
|
||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||
attribute_name: 'partition_id',
|
||||
order_expression: Ci::BuildSource.arel_table[:partition_id].desc
|
||||
)
|
||||
])
|
||||
end
|
||||
|
||||
def build_source_scope
|
||||
Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
|
||||
scope: scope,
|
||||
array_scope: array_scope,
|
||||
array_mapping_scope: array_mapping_scope
|
||||
).execute
|
||||
end
|
||||
|
||||
def ci_builds_query
|
||||
relation
|
||||
.where("id = p_ci_build_sources.build_id and partition_id = p_ci_build_sources.partition_id")
|
||||
.limit(1)
|
||||
end
|
||||
|
||||
def scope
|
||||
Ci::BuildSource.where(project_id: project.id).order(order)
|
||||
end
|
||||
|
||||
def array_scope
|
||||
Ci::BuildSource
|
||||
.where(project_id: project.id)
|
||||
.loose_index_scan(column: :source)
|
||||
.select(:source).where(source: sources)
|
||||
end
|
||||
|
||||
def array_mapping_scope
|
||||
->(source) { Ci::BuildSource.where(Ci::BuildSource.arel_table[:source].eq(source)) }
|
||||
end
|
||||
|
||||
def get_source_ids_from_names(source_names)
|
||||
source_ids = []
|
||||
source_names.each do |source_name|
|
||||
source_ids << Ci::BuildSource.sources[source_name] if Ci::BuildSource.sources.key?(source_name)
|
||||
end
|
||||
|
||||
source_ids
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
|
@ -15,7 +15,10 @@ module Ci
|
|||
end
|
||||
|
||||
def execute
|
||||
builds = init_collection.order_id_desc
|
||||
# params[:skip_ordering] needed when using in conjunction with Ci::BuildSourceFinder
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170899
|
||||
builds = params[:skip_ordering] ? init_collection : init_collection.order_id_desc
|
||||
|
||||
filter_builds(builds)
|
||||
rescue Gitlab::Access::AccessDeniedError
|
||||
type.none
|
||||
|
|
|
|||
|
|
@ -23,16 +23,27 @@ module Resolvers
|
|||
experiment: { milestone: '17.1' },
|
||||
description: 'Filter jobs by name.'
|
||||
|
||||
argument :sources, [::Types::Ci::JobSourceEnum],
|
||||
required: false,
|
||||
experiment: { milestone: '17.7' },
|
||||
description: "Filter jobs by source. Ignored if " \
|
||||
"'populate_and_use_build_source_table' feature flag is disabled."
|
||||
|
||||
alias_method :project, :object
|
||||
|
||||
def resolve_with_lookahead(**args)
|
||||
filter_by_name = Feature.enabled?(:populate_and_use_build_names_table, project) && args[:name].to_s.present?
|
||||
filter_by_sources = Feature.enabled?(:populate_and_use_build_source_table, project) && args[:sources].present?
|
||||
|
||||
jobs = ::Ci::JobsFinder.new(
|
||||
current_user: current_user, project: project, params: {
|
||||
scope: args[:statuses], with_artifacts: args[:with_artifacts]
|
||||
scope: args[:statuses], with_artifacts: args[:with_artifacts],
|
||||
skip_ordering: filter_by_sources
|
||||
}
|
||||
).execute
|
||||
|
||||
if Feature.enabled?(:populate_and_use_build_names_table, project)
|
||||
# These job filters are currently exclusive with each other
|
||||
if filter_by_name
|
||||
jobs = ::Ci::BuildNameFinder.new(
|
||||
relation: jobs,
|
||||
name: args[:name],
|
||||
|
|
@ -42,6 +53,14 @@ module Resolvers
|
|||
asc: args[:last].present?, invert_ordering: true
|
||||
}
|
||||
).execute
|
||||
elsif filter_by_sources
|
||||
jobs = ::Ci::BuildSourceFinder.new(
|
||||
relation: jobs,
|
||||
sources: args[:sources],
|
||||
project: project
|
||||
).execute
|
||||
|
||||
return offset_pagination(apply_lookahead(jobs))
|
||||
end
|
||||
|
||||
apply_lookahead(jobs)
|
||||
|
|
@ -61,7 +80,8 @@ module Resolvers
|
|||
{
|
||||
previous_stage_jobs_or_needs: [:needs, :pipeline],
|
||||
artifacts: [:job_artifacts],
|
||||
pipeline: [:user]
|
||||
pipeline: [:user],
|
||||
build_source: [:source]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
class JobSourceEnum < BaseEnum
|
||||
graphql_name 'CiJobSource'
|
||||
|
||||
::Ci::BuildSource.sources.keys.map(&:to_s).each do |source|
|
||||
value source.upcase,
|
||||
description: "A job initiated by #{source.tr('_', ' ')}.",
|
||||
value: source
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -135,7 +135,6 @@ module ApplicationHelper
|
|||
{
|
||||
page: body_data_page,
|
||||
page_type_id: controller.params[:id],
|
||||
find_file: find_file_path(ref_type: @ref_type),
|
||||
group: @group&.path,
|
||||
group_full_path: @group&.full_path
|
||||
}.merge(project_data)
|
||||
|
|
|
|||
|
|
@ -58,8 +58,7 @@ module Ci
|
|||
render_ci_icon(
|
||||
status,
|
||||
path,
|
||||
tooltip_placement: tooltip_placement,
|
||||
option_css_classes: 'gl-ml-3'
|
||||
tooltip_placement: tooltip_placement
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -67,13 +66,12 @@ module Ci
|
|||
status,
|
||||
path = nil,
|
||||
tooltip_placement: 'left',
|
||||
option_css_classes: '',
|
||||
container: 'body',
|
||||
show_status_text: false
|
||||
)
|
||||
content_tag_variant = path ? :a : :span
|
||||
variant = badge_variant(status)
|
||||
badge_classes = "ci-icon ci-icon-variant-#{variant} gl-inline-flex gl-items-center gl-text-sm #{option_css_classes}"
|
||||
badge_classes = "ci-icon ci-icon-variant-#{variant} gl-inline-flex gl-items-center gl-text-sm"
|
||||
title = "#{_('Pipeline')}: #{ci_label_for_status(status)}"
|
||||
data = { toggle: 'tooltip', placement: tooltip_placement, container: container, testid: 'ci-icon' }
|
||||
|
||||
|
|
|
|||
|
|
@ -912,15 +912,6 @@ module ProjectsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def find_file_path(ref_type: nil)
|
||||
return unless @project && !@project.empty_repo?
|
||||
return unless can?(current_user, :read_code, @project)
|
||||
|
||||
ref = @ref || @project.repository.root_ref
|
||||
|
||||
project_find_file_path(@project, ref, ref_type: ref_type)
|
||||
end
|
||||
|
||||
def can_show_last_commit_in_list?(project)
|
||||
can?(current_user, :read_cross_project) &&
|
||||
can?(current_user, :read_commit_status, project) &&
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@
|
|||
module Ci
|
||||
class BuildSource < Ci::ApplicationRecord
|
||||
include Ci::Partitionable
|
||||
include EachBatch
|
||||
|
||||
self.table_name = :p_ci_build_sources
|
||||
self.primary_key = :build_id
|
||||
|
||||
enum source: {
|
||||
scan_execution_policy: 1,
|
||||
pipeline_execution_policy: 2
|
||||
}
|
||||
ignore_column :pipeline_source, remove_with: '17.9', remove_after: '2025-01-15'
|
||||
|
||||
enum pipeline_source: Enums::Ci::Pipeline.sources
|
||||
enum source: {
|
||||
scan_execution_policy: 1001,
|
||||
pipeline_execution_policy: 1002
|
||||
}.merge(::Enums::Ci::Pipeline.sources)
|
||||
|
||||
query_constraints :build_id, :partition_id
|
||||
partitionable scope: :build, partitioned: true
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class OauthAccessToken < Doorkeeper::AccessToken
|
|||
scope :preload_application, -> { preload(:application) }
|
||||
|
||||
# user scope format is: `user:$USER_ID`
|
||||
SCOPED_USER_REGEX = /user:(\d+)(?:\s|$)/
|
||||
SCOPED_USER_REGEX = /\Auser:(\d+)\z/
|
||||
|
||||
def scopes=(value)
|
||||
if value.is_a?(Array)
|
||||
|
|
@ -53,8 +53,10 @@ class OauthAccessToken < Doorkeeper::AccessToken
|
|||
private
|
||||
|
||||
def extract_user_id_from_scopes
|
||||
return false unless scopes.present?
|
||||
# scopes are an instance of Doorkeeper:OAuth::Scopes class
|
||||
matches = scopes.grep(SCOPED_USER_REGEX)
|
||||
return unless matches.length == 1
|
||||
|
||||
scopes.to_s[SCOPED_USER_REGEX, 1]&.to_i
|
||||
matches[0][SCOPED_USER_REGEX, 1].to_i
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -123,7 +123,16 @@ module Groups
|
|||
def group_with_namespaced_npm_packages?
|
||||
return false unless group.packages_feature_enabled?
|
||||
|
||||
npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm, preload_pipelines: false).execute
|
||||
npm_packages = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, packages_class: ::Packages::Npm::Package, preload_pipelines: false)
|
||||
.execute
|
||||
else
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, package_type: :npm, preload_pipelines: false)
|
||||
.execute
|
||||
end
|
||||
|
||||
npm_packages = npm_packages.with_npm_scope(group.root_ancestor.path)
|
||||
|
||||
different_root_ancestor? && npm_packages.exists?
|
||||
|
|
|
|||
|
|
@ -78,9 +78,17 @@ module Groups
|
|||
|
||||
# we have a path change on a root group:
|
||||
# check that we don't have any npm package with a scope set to the group path
|
||||
npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
npm_packages = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, packages_class: ::Packages::Npm::Package, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
else
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, package_type: :npm, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
end
|
||||
|
||||
return true unless npm_packages.exists?
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@ module Import
|
|||
MEMBER_DELETE_BATCH_SIZE = 1_000
|
||||
GROUP_FINDER_MEMBER_RELATIONS = %i[direct inherited shared_from_groups].freeze
|
||||
PROJECT_FINDER_MEMBER_RELATIONS = %i[direct inherited invited_groups shared_into_ancestors].freeze
|
||||
RELATION_BATCH_SLEEP = 5
|
||||
RELATION_BATCH_SLEEP = 5 # TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/504995
|
||||
DATABASE_TABLE_HEALTH_INDICATORS = [Gitlab::Database::HealthStatus::Indicators::AutovacuumActiveOnTable].freeze
|
||||
GLOBAL_DATABASE_HEALTH_INDICATORS = [
|
||||
Gitlab::Database::HealthStatus::Indicators::WriteAheadLog,
|
||||
Gitlab::Database::HealthStatus::Indicators::PatroniApdex,
|
||||
Gitlab::Database::HealthStatus::Indicators::WalReceiverSaturation
|
||||
].freeze
|
||||
|
||||
DatabaseHealthStatusChecker = Struct.new(:id, :job_class_name)
|
||||
DatabaseHealthError = Class.new(StandardError)
|
||||
|
||||
def initialize(import_source_user)
|
||||
@import_source_user = import_source_user
|
||||
|
|
@ -18,16 +27,32 @@ module Import
|
|||
return unless import_source_user.reassignment_in_progress?
|
||||
|
||||
warn_about_any_risky_reassignments
|
||||
reassign_placeholder_references
|
||||
|
||||
log_warn('Reassigned by user was not found, this may affect membership checks') unless reassigned_by_user
|
||||
|
||||
create_memberships
|
||||
delete_placeholder_memberships
|
||||
begin
|
||||
reassign_placeholder_references
|
||||
|
||||
if placeholder_memberships.any?
|
||||
db_table_health_check!(Member) if Feature.enabled?(:reassignment_throttling, reassigned_by_user)
|
||||
|
||||
create_memberships
|
||||
delete_placeholder_memberships
|
||||
end
|
||||
rescue DatabaseHealthError => error
|
||||
log_warn("#{error.message}. Rescheduling reassignment")
|
||||
|
||||
return reschedule_reassignment_response
|
||||
end
|
||||
|
||||
UserProjectAccessChangedService.new(import_source_user.reassign_to_user_id).execute if project_membership_created?
|
||||
|
||||
import_source_user.complete!
|
||||
|
||||
ServiceResponse.success(
|
||||
message: s_('Import|Placeholder user record reassignment complete'),
|
||||
payload: import_source_user
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -73,10 +98,15 @@ module Import
|
|||
user_reference_column: reference_group.user_reference_column,
|
||||
alias_version: reference_group.alias_version
|
||||
) do |model_relation, placeholder_references|
|
||||
if Feature.enabled?(:reassignment_throttling, reassigned_by_user)
|
||||
db_table_health_check!(model_relation)
|
||||
db_health_check!
|
||||
end
|
||||
|
||||
reassign_placeholder_records_batch(model_relation, placeholder_references)
|
||||
|
||||
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/493977
|
||||
Kernel.sleep RELATION_BATCH_SLEEP
|
||||
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/504995
|
||||
Kernel.sleep RELATION_BATCH_SLEEP unless Feature.enabled?(:reassignment_throttling, reassigned_by_user)
|
||||
end
|
||||
rescue Import::PlaceholderReferences::AliasResolver::MissingAlias => e
|
||||
::Import::Framework::Logger.error(
|
||||
|
|
@ -202,6 +232,47 @@ module Import
|
|||
@project_membership_created == true
|
||||
end
|
||||
|
||||
def db_table_health_check!(model)
|
||||
health_context = Gitlab::Database::HealthStatus::Context.new(
|
||||
DatabaseHealthStatusChecker.new(import_source_user.id, self.class.name),
|
||||
nil,
|
||||
[model.table_name],
|
||||
nil
|
||||
)
|
||||
|
||||
stop_signal = Gitlab::Database::HealthStatus
|
||||
.evaluate(health_context, DATABASE_TABLE_HEALTH_INDICATORS).any?(&:stop?)
|
||||
|
||||
raise DatabaseHealthError, "#{model.table_name} table unhealthy" if stop_signal
|
||||
end
|
||||
|
||||
def db_health_check!
|
||||
stop_signal = Rails.cache.fetch("reassign_placeholder_user_records_service_db_check", expires_in: 30.seconds) do
|
||||
gitlab_schema = :gitlab_main
|
||||
|
||||
health_context = Gitlab::Database::HealthStatus::Context.new(
|
||||
DatabaseHealthStatusChecker.new(import_source_user.id, self.class.name),
|
||||
Gitlab::Database.schemas_to_base_models[gitlab_schema].first,
|
||||
nil,
|
||||
gitlab_schema
|
||||
)
|
||||
|
||||
Gitlab::Database::HealthStatus
|
||||
.evaluate(health_context, GLOBAL_DATABASE_HEALTH_INDICATORS).any?(&:stop?)
|
||||
end
|
||||
|
||||
raise DatabaseHealthError, "Database unhealthy" if stop_signal
|
||||
end
|
||||
|
||||
def reschedule_reassignment_response
|
||||
ServiceResponse.new(
|
||||
status: :ok,
|
||||
message: s_('Import|Rescheduling placeholder user records reassignment: database health'),
|
||||
payload: import_source_user,
|
||||
reason: :db_health_check_failed
|
||||
)
|
||||
end
|
||||
|
||||
def log_create_membership_skipped(message, placeholder_membership, existing_membership)
|
||||
log_info(
|
||||
message,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Packages
|
|||
|
||||
package.mark_package_files_for_destruction
|
||||
package.sync_maven_metadata(current_user)
|
||||
package.sync_npm_metadata_cache
|
||||
package.sync_npm_metadata_cache if package.npm?
|
||||
|
||||
service_response_success('Package was successfully marked as pending destruction')
|
||||
rescue StandardError => e
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
- c.with_body do
|
||||
= render 'import_and_export'
|
||||
|
||||
= render_if_exists 'admin/application_settings/integrations_settings'
|
||||
|
||||
= render ::Layouts::SettingsBlockComponent.new(_('Diff limits'),
|
||||
id: 'js-merge-request-settings',
|
||||
expanded: expanded_by_default?) do |c|
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
- status = local_assigns.fetch(:status)
|
||||
- path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil)
|
||||
- tooltip_placement = local_assigns.fetch(:tooltip_placement, "left")
|
||||
- option_css_classes = local_assigns.fetch(:option_css_classes, nil)
|
||||
- show_status_text = local_assigns.fetch(:show_status_text, false)
|
||||
|
||||
= render_ci_icon(status, path, tooltip_placement: tooltip_placement, option_css_classes: option_css_classes, show_status_text: show_status_text)
|
||||
= render_ci_icon(status, path, tooltip_placement: tooltip_placement, show_status_text: show_status_text)
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@
|
|||
= author_avatar(commit, size: 32, has_tooltip: false, css_class: 'gl-inline-block')
|
||||
|
||||
.commit-detail.flex-list.gl-flex.gl-justify-between.gl-items-start.gl-grow.gl-min-w-0.gl-mb-3
|
||||
.commit-content{ data: { testid: 'commit-content' } }
|
||||
.commit-content.gl-self-center{ data: { testid: 'commit-content' } }
|
||||
- if is_blob_page
|
||||
.gl-flex.sm:gl-hidden.gl-gap-4.gl-pt-3
|
||||
.gl-flex.sm:gl-hidden.gl-gap-3.gl-items-center
|
||||
.committer.gl-text-sm
|
||||
- commit_authored_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
|
||||
= commit_authored_timeago.html_safe
|
||||
|
|
@ -57,8 +57,8 @@
|
|||
%pre{ class: ["commit-row-description gl-whitespace-pre-wrap", (collapsible ? "js-toggle-content" : "!gl-block")] }
|
||||
= preserve(markdown_field(commit, :description))
|
||||
|
||||
.commit-actions.gl-flex.gl-items-center
|
||||
%div{ class: is_blob_page ? "gl-hidden sm:gl-flex gl-items-center": "gl-block" }
|
||||
.commit-actions.gl-flex.gl-items-center.gl-gap-3
|
||||
%div{ class: is_blob_page ? "gl-hidden sm:gl-flex gl-items-center gl-gap-3": "gl-flex gl-items-center gl-gap-3" }
|
||||
- if tags.present?
|
||||
= gl_badge_tag(variant: :neutral, icon: 'tag', class: 'gl-font-monospace') do
|
||||
- if tags.size > 1
|
||||
|
|
@ -80,10 +80,9 @@
|
|||
= link_to_browse_code(project, commit)
|
||||
|
||||
- if is_blob_page
|
||||
.gl-block.sm:gl-hidden.gl-pt-3
|
||||
.gl-block.sm:gl-hidden
|
||||
= render Pajamas::ButtonComponent.new(icon: 'ellipsis_h', button_options: { class: 'button-ellipsis-horizontal text-expander js-toggle-button', data: { toggle: 'tooltip', container: 'body', collapse_title: toggle_commit_message, expand_title: toggle_commit_message }, title: toggle_commit_message, aria: { label: toggle_commit_message }})
|
||||
.gl-pt-3.sm:gl-pt-0
|
||||
#js-commit-history-link{ data: { history_link: project_commits_path(@project, @id, ref_type: @ref_type), event_tracking: 'click_history_control_on_blob_page' } }
|
||||
#js-commit-history-link{ data: { history_link: project_commits_path(@project, @id, ref_type: @ref_type), event_tracking: 'click_history_control_on_blob_page' } }
|
||||
|
||||
- if is_blob_page
|
||||
.gl-block.sm:gl-hidden
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
= render ::Layouts::CrudComponent.new(_('Active personal access tokens'),
|
||||
icon: 'token',
|
||||
count: @active_access_tokens_size,
|
||||
count_options: { class: 'js-token-count' },
|
||||
count_options: { class: 'js-token-count', data: { testid: 'active-token-count' } },
|
||||
toggle_text: _('Add new token'),
|
||||
toggle_options: { data: { testid: 'add-new-token-button' } },
|
||||
form_options: { class: 'js-add-new-token-form' },
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ module Import
|
|||
include ApplicationWorker
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
BACKOFF_PERIOD = 1.minute.freeze
|
||||
|
||||
idempotent!
|
||||
data_consistency :sticky
|
||||
feature_category :importers
|
||||
|
|
@ -12,19 +14,23 @@ module Import
|
|||
sidekiq_options retry: 5, dead: false
|
||||
sidekiq_options max_retries_after_interruption: 20
|
||||
|
||||
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/493977
|
||||
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/504995
|
||||
concurrency_limit -> { 4 }
|
||||
|
||||
sidekiq_retries_exhausted do |msg, exception|
|
||||
new.perform_failure(exception, msg['args'])
|
||||
end
|
||||
|
||||
def perform(import_source_user_id, _params = {})
|
||||
def perform(import_source_user_id, params = {})
|
||||
@import_source_user = Import::SourceUser.find_by_id(import_source_user_id)
|
||||
|
||||
return unless import_source_user_valid?
|
||||
|
||||
Import::ReassignPlaceholderUserRecordsService.new(import_source_user).execute
|
||||
response = Import::ReassignPlaceholderUserRecordsService.new(import_source_user).execute
|
||||
|
||||
if response&.reason == :db_health_check_failed
|
||||
return self.class.perform_in(BACKOFF_PERIOD, import_source_user.id, params)
|
||||
end
|
||||
|
||||
Import::DeletePlaceholderUserWorker.perform_async(import_source_user.id)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160650
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/478054
|
||||
milestone: '17.3'
|
||||
group: group::import and integrate
|
||||
type: wip
|
||||
default_enabled: false
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: fix_graphql_csrf
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/379326
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173972
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/508617
|
||||
milestone: '17.7'
|
||||
group: group::project management
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: reassignment_throttling
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/493977
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173292
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/504995
|
||||
milestone: '17.7'
|
||||
group: group::import and integrate
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: populate_and_use_build_source_table
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11796
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170899
|
||||
rollout_issue_url:
|
||||
milestone: '17.7'
|
||||
group: group::security policies
|
||||
type: wip
|
||||
default_enabled: false
|
||||
|
|
@ -5,4 +5,4 @@ description: Backfills resource_link_events table based off system_note_metadata
|
|||
feature_category: team_planning
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118605
|
||||
milestone: '16.1'
|
||||
finalized_by: '20241205232318'
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
|
|||
|
|
@ -9,5 +9,4 @@ description: Normalized software licenses to use in conjunction with License Com
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6246
|
||||
milestone: '11.2'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480578
|
||||
table_size: small
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToBuildSources < Gitlab::Database::Migration[2.2]
|
||||
include Gitlab::Database::PartitioningMigrationHelpers
|
||||
|
||||
milestone '17.7'
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = :p_ci_build_sources
|
||||
INDEX_NAME = :index_p_ci_build_sources_on_search_columns
|
||||
COLUMNS = %i[project_id source build_id partition_id]
|
||||
|
||||
def up
|
||||
add_concurrent_partitioned_index(TABLE_NAME, COLUMNS, name: INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_partitioned_index_by_name(TABLE_NAME, INDEX_NAME)
|
||||
end
|
||||
end
|
||||
|
|
@ -8,13 +8,8 @@ class FinalizeBackfillResourceLinkEvents < Gitlab::Database::Migration[2.2]
|
|||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'BackfillResourceLinkEvents',
|
||||
table_name: :resource_link_events,
|
||||
column_name: :id,
|
||||
job_arguments: [:namespace_id, :issues, :namespace_id, :issue_id],
|
||||
finalize: true
|
||||
)
|
||||
# no-op
|
||||
# See details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174621#note_2250503192
|
||||
end
|
||||
|
||||
def down; end
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
8794d04a370d7f8e310158310d903f75c641b61843bec74820e3f283aec24d1b
|
||||
|
|
@ -31804,6 +31804,8 @@ CREATE INDEX index_p_ci_build_sources_on_pipeline_source ON ONLY p_ci_build_sour
|
|||
|
||||
CREATE INDEX index_p_ci_build_sources_on_project_id_and_build_id ON ONLY p_ci_build_sources USING btree (project_id, build_id);
|
||||
|
||||
CREATE INDEX index_p_ci_build_sources_on_search_columns ON ONLY p_ci_build_sources USING btree (project_id, source, build_id, partition_id);
|
||||
|
||||
CREATE INDEX index_p_ci_build_tags_on_build_id_and_partition_id ON ONLY p_ci_build_tags USING btree (build_id, partition_id);
|
||||
|
||||
CREATE INDEX index_p_ci_build_tags_on_project_id ON ONLY p_ci_build_tags USING btree (project_id);
|
||||
|
|
|
|||
|
|
@ -32309,6 +32309,7 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectjobsname"></a>`name` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.1. **Status**: Experiment. Filter jobs by name. |
|
||||
| <a id="projectjobssources"></a>`sources` **{warning-solid}** | [`[CiJobSource!]`](#cijobsource) | **Introduced** in GitLab 17.7. **Status**: Experiment. Filter jobs by source. Ignored if 'populate_and_use_build_source_table' feature flag is disabled. |
|
||||
| <a id="projectjobsstatuses"></a>`statuses` | [`[CiJobStatus!]`](#cijobstatus) | Filter jobs by status. |
|
||||
| <a id="projectjobswithartifacts"></a>`withArtifacts` | [`Boolean`](#boolean) | Filter by artifacts presence. |
|
||||
|
||||
|
|
@ -34558,7 +34559,7 @@ JSON structure of a file with matches.
|
|||
| <a id="searchblobfiletypeblameurl"></a>`blameUrl` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.2. **Status**: Experiment. Blame URL of the file. |
|
||||
| <a id="searchblobfiletypechunks"></a>`chunks` **{warning-solid}** | [`[SearchBlobChunk!]`](#searchblobchunk) | **Introduced** in GitLab 17.2. **Status**: Experiment. Maximum matches per file. |
|
||||
| <a id="searchblobfiletypefileurl"></a>`fileUrl` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.2. **Status**: Experiment. URL of the file. |
|
||||
| <a id="searchblobfiletypematchcount"></a>`matchCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.2. **Status**: Experiment. Matches per file in maximum 50 chunks. |
|
||||
| <a id="searchblobfiletypematchcount"></a>`matchCount` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.2. **Status**: Experiment. Matches per file up to a max of 50 chunks. Default is 3. |
|
||||
| <a id="searchblobfiletypematchcounttotal"></a>`matchCountTotal` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.2. **Status**: Experiment. Total number of matches per file. |
|
||||
| <a id="searchblobfiletypepath"></a>`path` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.2. **Status**: Experiment. Path of the file. |
|
||||
| <a id="searchblobfiletypeprojectpath"></a>`projectPath` **{warning-solid}** | [`String`](#string) | **Introduced** in GitLab 17.2. **Status**: Experiment. Full path of the project. |
|
||||
|
|
@ -38227,6 +38228,31 @@ Values for sorting inherited variables.
|
|||
| <a id="cijobkindbridge"></a>`BRIDGE` | Bridge CI job connecting a parent and child pipeline. |
|
||||
| <a id="cijobkindbuild"></a>`BUILD` | Standard CI job. |
|
||||
|
||||
### `CiJobSource`
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="cijobsourceapi"></a>`API` | A job initiated by api. |
|
||||
| <a id="cijobsourcechat"></a>`CHAT` | A job initiated by chat. |
|
||||
| <a id="cijobsourcecontainer_registry_push"></a>`CONTAINER_REGISTRY_PUSH` | A job initiated by container registry push. |
|
||||
| <a id="cijobsourceduo_workflow"></a>`DUO_WORKFLOW` | A job initiated by duo workflow. |
|
||||
| <a id="cijobsourceexternal"></a>`EXTERNAL` | A job initiated by external. |
|
||||
| <a id="cijobsourceexternal_pull_request_event"></a>`EXTERNAL_PULL_REQUEST_EVENT` | A job initiated by external pull request event. |
|
||||
| <a id="cijobsourcemerge_request_event"></a>`MERGE_REQUEST_EVENT` | A job initiated by merge request event. |
|
||||
| <a id="cijobsourceondemand_dast_scan"></a>`ONDEMAND_DAST_SCAN` | A job initiated by ondemand dast scan. |
|
||||
| <a id="cijobsourceondemand_dast_validation"></a>`ONDEMAND_DAST_VALIDATION` | A job initiated by ondemand dast validation. |
|
||||
| <a id="cijobsourceparent_pipeline"></a>`PARENT_PIPELINE` | A job initiated by parent pipeline. |
|
||||
| <a id="cijobsourcepipeline"></a>`PIPELINE` | A job initiated by pipeline. |
|
||||
| <a id="cijobsourcepipeline_execution_policy"></a>`PIPELINE_EXECUTION_POLICY` | A job initiated by pipeline execution policy. |
|
||||
| <a id="cijobsourcepush"></a>`PUSH` | A job initiated by push. |
|
||||
| <a id="cijobsourcescan_execution_policy"></a>`SCAN_EXECUTION_POLICY` | A job initiated by scan execution policy. |
|
||||
| <a id="cijobsourceschedule"></a>`SCHEDULE` | A job initiated by schedule. |
|
||||
| <a id="cijobsourcesecurity_orchestration_policy"></a>`SECURITY_ORCHESTRATION_POLICY` | A job initiated by security orchestration policy. |
|
||||
| <a id="cijobsourcetrigger"></a>`TRIGGER` | A job initiated by trigger. |
|
||||
| <a id="cijobsourceunknown"></a>`UNKNOWN` | A job initiated by unknown. |
|
||||
| <a id="cijobsourceweb"></a>`WEB` | A job initiated by web. |
|
||||
| <a id="cijobsourcewebide"></a>`WEBIDE` | A job initiated by webide. |
|
||||
|
||||
### `CiJobStatus`
|
||||
|
||||
| Value | Description |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
stage: Secure
|
||||
group: Secret Detection
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
|
||||
---
|
||||
|
||||
# Group security settings API
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
Every API call to group security settings must be [authenticated](rest/authentication.md).
|
||||
|
||||
If a user isn't a member of a private group, requests to the private group return a `404 Not Found` status code.
|
||||
|
||||
## Update `secret_push_protection_enabled` setting
|
||||
|
||||
Update the `secret_push_protection_enabled` setting for the all projects in a group to the provided value.
|
||||
|
||||
Set to `true` to enable [secret push protection](../user/application_security/secret_detection/secret_push_protection/index.md) for the all projects in the group.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Maintainer role for the group.
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------- | ----------------- | ---------- | -----------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-paths) which the authenticated user is a member of |
|
||||
| `secret_push_protection_enabled` | boolean | yes | Whether secret push protection is enabled for the group. |
|
||||
| `projects_to_exclude` | array of integers | no | The IDs of projects to exclude from the feature. |
|
||||
|
||||
```shell
|
||||
curl --header PUT "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/7/security_settings?secret_push_protection_enabled=true&projects_to_exclude=1,2,3"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"secret_push_protection_enabled": true,
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
|
@ -433,6 +433,11 @@ Parameters:
|
|||
| `internal` | boolean | no | The internal flag of a note. Default is false. |
|
||||
| `merge_request_diff_head_sha` | string | no | Required for the `/merge` [quick action](../user/project/quick_actions.md). The SHA of the head commit, which ensures the merge request wasn't updated after the API request was sent. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--url "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/notes?body=note"
|
||||
```
|
||||
|
||||
### Modify existing merge request note
|
||||
|
||||
Modify existing note of a merge request.
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ The exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv
|
|||
This limit [can be changed on self-managed instances](../../administration/instance_limits.md#limit-dotenv-file-size).
|
||||
- On GitLab.com, [the maximum number of inherited variables](../../user/gitlab_com/index.md#gitlab-cicd)
|
||||
is 50 for Free, 100 for Premium and 150 for Ultimate. The default for
|
||||
self-managed instances is 150, and can be changed by changing the
|
||||
self-managed instances is 20, and can be changed by changing the
|
||||
`dotenv_variables` [application limit](../../administration/instance_limits.md#limit-dotenv-variables).
|
||||
- Variable substitution in the `.env` file is not supported.
|
||||
- [Multiline values in the `.env` file](https://github.com/motdotla/dotenv#multiline-values) are not supported.
|
||||
|
|
|
|||
|
|
@ -231,7 +231,8 @@ DETAILS:
|
|||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10779) in GitLab 17.6. This is a [beta](../../../policy/development_stages_support.md#beta) feature.
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/14862) in GitLab 17.6 with a flag named [`resolve_vulnerability_in_mr`](https://gitlab.com/gitlab-org/gitlab/-/issues/482753). Disabled by default.
|
||||
> [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175150) in GitLab 17.7.
|
||||
|
||||
Use GitLab Duo Vulnerability resolution to automatically create a merge request suggestion comment that
|
||||
resolves the vulnerability finding. By default, it is powered by the Anthropic [`claude-3.5-sonnet`](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet) model.
|
||||
|
|
|
|||
|
|
@ -64,8 +64,14 @@ module API
|
|||
get '*package_name/-/*file_name', format: false do
|
||||
authorize_read_package!(project)
|
||||
|
||||
package = project.packages.npm
|
||||
.by_name_and_file_name(params[:package_name], params[:file_name])
|
||||
package = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package
|
||||
.for_projects(project)
|
||||
.by_name_and_file_name(params[:package_name], params[:file_name])
|
||||
else
|
||||
project.packages.npm
|
||||
.by_name_and_file_name(params[:package_name], params[:file_name])
|
||||
end
|
||||
|
||||
not_found!('Package') unless package
|
||||
|
||||
|
|
|
|||
|
|
@ -1706,6 +1706,9 @@ msgid_plural "+%d more"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "+%{count} more"
|
||||
msgstr ""
|
||||
|
||||
msgid "+%{more_assignees_count}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4043,12 +4046,18 @@ msgstr ""
|
|||
msgid "AdminSettings|All new projects can use instance runners by default."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Allow all integrations"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Allow contribution mapping to administrators %{allow_contribution_mapping_to_admins_link_start}%{icon}%{allow_contribution_mapping_to_admins_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Allow migrating GitLab groups and projects by direct transfer"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Allow only integrations on this allowlist"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Allow runner registration token"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4220,6 +4229,9 @@ msgstr ""
|
|||
msgid "AdminSettings|Instance runners expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Integration settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -4232,6 +4244,9 @@ msgstr ""
|
|||
msgid "AdminSettings|Make new users' profiles private by default"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Manage which integrations can be enabled on this instance."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Maximum downstream pipeline trigger rate"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -28650,6 +28665,12 @@ msgstr ""
|
|||
msgid "Import|Partially completed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import|Placeholder user record reassignment complete"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import|Rescheduling placeholder user records reassignment: database health"
|
||||
msgstr ""
|
||||
|
||||
msgid "Import|Show errors"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50938,6 +50959,9 @@ msgstr ""
|
|||
msgid "SecurityReports|All tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|CVSS v%{version}:"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Change status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51013,6 +51037,9 @@ msgstr ""
|
|||
msgid "SecurityReports|Download scanned URLs"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|EPSS:"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Edit dismissal"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51079,6 +51106,9 @@ msgstr ""
|
|||
msgid "SecurityReports|Issues created from a vulnerability cannot be removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|KEV:"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Learn more about security configuration"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1139,6 +1139,19 @@ RSpec.describe ApplicationController, feature_category: :shared do
|
|||
.to raise_error(ActionController::InvalidAuthenticityToken)
|
||||
end
|
||||
end
|
||||
|
||||
controller(described_class) do
|
||||
self.allow_forgery_protection = true
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def create; end
|
||||
end
|
||||
|
||||
it 'returns a 403' do
|
||||
post :create
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#after_sign_in_path_for' do
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :with_build_source do
|
||||
after(:create) do |build, _|
|
||||
create(:ci_build_source, build: build)
|
||||
end
|
||||
end
|
||||
|
||||
trait :degenerated do
|
||||
options { nil }
|
||||
yaml_variables { nil }
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ RSpec.describe 'User Settings > Personal access tokens', :js, feature_category:
|
|||
|
||||
visit user_settings_personal_access_tokens_path
|
||||
|
||||
expect(active_access_tokens_counter).to have_text('0')
|
||||
|
||||
click_button 'Add new token'
|
||||
fill_in "Token name", with: name
|
||||
|
||||
|
|
@ -47,6 +49,7 @@ RSpec.describe 'User Settings > Personal access tokens', :js, feature_category:
|
|||
expect(active_access_tokens).to have_text('read_api')
|
||||
expect(active_access_tokens).to have_text('read_user')
|
||||
expect(created_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
end
|
||||
|
||||
context "when creation fails" do
|
||||
|
|
@ -126,11 +129,13 @@ RSpec.describe 'User Settings > Personal access tokens', :js, feature_category:
|
|||
|
||||
it "displays the newly created token" do
|
||||
visit user_settings_personal_access_tokens_path
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
accept_gl_confirm(button_text: s_('AccessTokens|Rotate')) { click_on s_('AccessTokens|Rotate') }
|
||||
wait_for_all_requests
|
||||
expect(page).to have_content("Your new personal access token has been created.")
|
||||
expect(active_access_tokens).to have_text(personal_access_token.name)
|
||||
expect(created_access_token).to match(/[\w-]{20}/)
|
||||
expect(active_access_tokens_counter).to have_text('1')
|
||||
end
|
||||
|
||||
context "when rotation fails" do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::BuildSourceFinder, feature_category: :continuous_integration do
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, source: "push") }
|
||||
|
||||
let_it_be(:build_non_relevant) { create(:ci_build, pipeline: pipeline, name: "unique-name") }
|
||||
let_it_be(:old_build) { create(:ci_build, pipeline: pipeline, name: "build1") }
|
||||
let_it_be(:old_middle_build) { create(:ci_build, pipeline: pipeline, name: "build2") }
|
||||
let_it_be(:middle_build) { create(:ci_build, pipeline: pipeline, name: "build3") }
|
||||
let_it_be(:middle_new_build) { create(:ci_build, pipeline: pipeline, name: "build4") }
|
||||
let_it_be(:new_build) { create(:ci_build, pipeline: pipeline, name: "build5") }
|
||||
|
||||
let_it_be(:old_build_source) { create(:ci_build_source, build: old_build, source: :scan_execution_policy) }
|
||||
let_it_be(:old_middle_build_source) { create(:ci_build_source, build: old_middle_build, source: :trigger) }
|
||||
let_it_be(:middle_build_source) { create(:ci_build_source, build: middle_build, source: :scan_execution_policy) }
|
||||
let_it_be(:middle_new_build_source) { create(:ci_build_source, build: middle_new_build, source: :push) }
|
||||
let_it_be(:new_build_source) { create(:ci_build_source, build: new_build, source: :scan_execution_policy) }
|
||||
|
||||
describe "#execute" do
|
||||
let(:main_relation) { Ci::Build.all }
|
||||
let(:sources) { ["scan_execution_policy"] }
|
||||
let(:cursor_id) { nil }
|
||||
|
||||
subject(:build_source_finder) do
|
||||
described_class.new(
|
||||
relation: main_relation,
|
||||
sources: sources,
|
||||
project: pipeline.project,
|
||||
params: {
|
||||
cursor_id: cursor_id
|
||||
}
|
||||
).execute
|
||||
end
|
||||
|
||||
it 'filters by source in desc order' do
|
||||
expect(build_source_finder)
|
||||
.to eq([new_build, middle_build, old_build])
|
||||
end
|
||||
|
||||
context 'when no source is passed in' do
|
||||
let(:sources) { [] }
|
||||
|
||||
it 'does not filter by source' do
|
||||
expect(build_source_finder.count).to eq(6)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple source query' do
|
||||
let(:sources) { %w[scan_execution_policy push] }
|
||||
|
||||
it 'returns builds from any of the given sources' do
|
||||
expect(build_source_finder)
|
||||
.to eq([new_build, middle_new_build, middle_build, old_build])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'argument errors' do
|
||||
context 'when relation is not Ci::Build' do
|
||||
let(:main_relation) { Ci::Bridge.all }
|
||||
|
||||
it 'raises argument error for relation' do
|
||||
expect { build_source_finder.execute }.to raise_error(ArgumentError, 'Only Ci::Builds are source searchable')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with status and ref' do
|
||||
let(:main_relation) { Ci::Build.pending.where(ref: 'master') }
|
||||
|
||||
it 'returns the correct builds with the filtered status and ref' do
|
||||
expect(build_source_finder.pluck(:name))
|
||||
.to eq(%w[build5 build3 build1])
|
||||
expect(build_source_finder.pluck(:ref).uniq)
|
||||
.to eq(%w[master])
|
||||
expect(build_source_finder.pluck(:status).uniq)
|
||||
.to eq(%w[pending])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import App from '~/merge_requests/reports/components/app.vue';
|
||||
import routes from '~/merge_requests/reports/routes';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
describe('Merge request reports App component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
const router = new VueRouter({ mode: 'history', routes });
|
||||
wrapper = shallowMountExtended(App, { router });
|
||||
};
|
||||
|
||||
it('should render sidebar navigation', () => {
|
||||
createComponent();
|
||||
expect(wrapper.findByText('Blockers').exists()).toBe(true);
|
||||
expect(wrapper.findByText('Code quality').exists()).toBe(true);
|
||||
expect(wrapper.findByText('Security').exists()).toBe(true);
|
||||
expect(wrapper.findByText('License Compliance').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render blockers counter', () => {
|
||||
createComponent();
|
||||
expect(wrapper.findByTestId('blockers-counter').text()).toBe('2');
|
||||
});
|
||||
});
|
||||
|
|
@ -100,7 +100,7 @@ describe('BlobFooter', () => {
|
|||
await nextTick();
|
||||
expect(findGlLink().exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain(
|
||||
'Show less - Too many matches found. Showing 50 chunks out of 200 results. Open the file to view all.',
|
||||
`Show less - Too many matches found. Showing 5 chunks out of 200 results. Open the file to view all.`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ describe('TodoItemTitle', () => {
|
|||
['to-do for MR', 'Update file .gitlab-ci.yml · Flightjs / Flight !17', MR_BUILD_FAILED_TODO],
|
||||
[
|
||||
'to-do for design',
|
||||
'Screenshot_2024-11-22_at_16.11.25.png · Flightjs / Flight #35',
|
||||
'Important issue › Screenshot_2024-11-22_at_16.11.25.png · Flightjs / Flight #35',
|
||||
DESIGN_TODO,
|
||||
],
|
||||
])(`renders %s as %s`, (_a, b, c) => {
|
||||
|
|
@ -66,7 +66,7 @@ describe('TodoItemTitle', () => {
|
|||
it.each`
|
||||
targetType | icon | showsIcon
|
||||
${TODO_TARGET_TYPE_ALERT} | ${'status-alert'} | ${true}
|
||||
${TODO_TARGET_TYPE_DESIGN} | ${'issues'} | ${true}
|
||||
${TODO_TARGET_TYPE_DESIGN} | ${'media'} | ${true}
|
||||
${TODO_TARGET_TYPE_EPIC} | ${'epic'} | ${true}
|
||||
${TODO_TARGET_TYPE_ISSUE} | ${'issues'} | ${true}
|
||||
${TODO_TARGET_TYPE_MERGE_REQUEST} | ${'merge-request'} | ${true}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,9 @@ export const DESIGN_TODO = {
|
|||
targetEntity: {
|
||||
name: 'Screenshot_2024-11-22_at_16.11.25.png',
|
||||
issue: {
|
||||
id: 'gid://gitlab/Issue/35',
|
||||
reference: '#35',
|
||||
name: 'Important issue',
|
||||
},
|
||||
__typename: 'Design',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { GlAvatar } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { GlAvatarLabeled } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GroupItem from '~/vue_shared/components/list_selector/group_item.vue';
|
||||
import HiddenGroupsItem from 'ee_component/approvals/components/hidden_groups_item.vue';
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ describe('GroupItem spec', () => {
|
|||
const MOCK_GROUP = { id: 123, fullName: 'Group 1', name: 'group1', avatarUrl: 'some/avatar.jpg' };
|
||||
|
||||
const createComponent = (props, options) => {
|
||||
wrapper = mountExtended(GroupItem, {
|
||||
wrapper = shallowMountExtended(GroupItem, {
|
||||
propsData: {
|
||||
data: MOCK_GROUP,
|
||||
...props,
|
||||
|
|
@ -18,20 +18,20 @@ describe('GroupItem spec', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete-group-btn');
|
||||
|
||||
beforeEach(() => createComponent());
|
||||
|
||||
it('renders an Avatar component', () => {
|
||||
expect(findAvatar().props('size')).toBe(32);
|
||||
expect(findAvatar().props('src')).toBe(MOCK_GROUP.avatarUrl);
|
||||
expect(findAvatar().attributes('alt')).toBe(MOCK_GROUP.fullName);
|
||||
});
|
||||
|
||||
it('renders a fullName and name', () => {
|
||||
expect(wrapper.text()).toContain('Group 1');
|
||||
expect(wrapper.text()).toContain('group1');
|
||||
it('renders AvatarLabeled component', () => {
|
||||
expect(findAvatarLabeled().props()).toMatchObject({
|
||||
label: 'Group 1',
|
||||
subLabel: '@group1',
|
||||
});
|
||||
expect(findAvatarLabeled().attributes()).toMatchObject({
|
||||
size: '32',
|
||||
src: 'some/avatar.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render a delete button by default', () => {
|
||||
|
|
@ -62,7 +62,7 @@ describe('GroupItem spec', () => {
|
|||
});
|
||||
|
||||
it('emits a delete event if the delete button is clicked', () => {
|
||||
findDeleteButton().trigger('click');
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('delete')).toEqual([[MOCK_GROUP.id]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlAvatar, GlButton } from '@gitlab/ui';
|
||||
import { GlAvatarLabeled, GlButton } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ProjectItem from '~/vue_shared/components/list_selector/project_item.vue';
|
||||
|
||||
|
|
@ -18,25 +18,26 @@ describe('GroupItem spec', () => {
|
|||
data: MOCK_PROJECT,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlAvatarLabeled,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
const findDeleteButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
beforeEach(() => createComponent());
|
||||
|
||||
it('renders an Avatar component', () => {
|
||||
expect(findAvatar().props('size')).toBe(32);
|
||||
expect(findAvatar().attributes()).toMatchObject({
|
||||
src: MOCK_PROJECT.avatarUrl,
|
||||
alt: MOCK_PROJECT.name,
|
||||
it('renders AvatarLabeled component', () => {
|
||||
expect(findAvatarLabeled().props()).toMatchObject({
|
||||
label: 'Project 1',
|
||||
subLabel: 'Group 1 / Project 1',
|
||||
});
|
||||
expect(findAvatarLabeled().attributes()).toMatchObject({
|
||||
size: '32',
|
||||
src: 'some/avatar.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a name and namespace', () => {
|
||||
expect(wrapper.text()).toContain(MOCK_PROJECT.name);
|
||||
expect(wrapper.text()).toContain(MOCK_PROJECT.nameWithNamespace);
|
||||
});
|
||||
|
||||
it('does not render a delete button by default', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlAvatar } from '@gitlab/ui';
|
||||
import { GlAvatarLabeled } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UserItem from '~/vue_shared/components/list_selector/user_item.vue';
|
||||
|
||||
|
|
@ -13,23 +13,26 @@ describe('UserItem spec', () => {
|
|||
data: MOCK_USER,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlAvatarLabeled,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete-user-btn');
|
||||
|
||||
beforeEach(() => createComponent());
|
||||
|
||||
it('renders an Avatar component', () => {
|
||||
expect(findAvatar().props('size')).toBe(32);
|
||||
expect(findAvatar().props('src')).toBe(MOCK_USER.avatarUrl);
|
||||
expect(findAvatar().attributes('alt')).toBe(MOCK_USER.name);
|
||||
});
|
||||
|
||||
it('renders a name and username', () => {
|
||||
expect(wrapper.text()).toContain('Admin');
|
||||
expect(wrapper.text()).toContain('@root');
|
||||
it('renders AvatarLabeled component', () => {
|
||||
expect(findAvatarLabeled().props()).toMatchObject({
|
||||
label: 'Admin',
|
||||
subLabel: '@root',
|
||||
});
|
||||
expect(findAvatarLabeled().attributes()).toMatchObject({
|
||||
size: '32',
|
||||
src: 'some/avatar.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render a delete button by default', () => {
|
||||
|
|
@ -45,7 +48,7 @@ describe('UserItem spec', () => {
|
|||
});
|
||||
|
||||
it('emits a delete event if the delete button is clicked', () => {
|
||||
findDeleteButton().trigger('click');
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('delete')).toEqual([[MOCK_USER.id]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,6 +39,68 @@ RSpec.describe Resolvers::ProjectJobsResolver, feature_category: :continuous_int
|
|||
it { is_expected.to contain_exactly(successful_build, successful_build_two, failed_build, pending_build) }
|
||||
end
|
||||
|
||||
context 'when filtering by source' do
|
||||
before do
|
||||
stub_feature_flags(populate_and_use_build_source_table: true)
|
||||
end
|
||||
|
||||
let_it_be(:successful_build_source) { create(:ci_build_source, build: successful_build, source: 'scan_execution_policy') }
|
||||
let_it_be(:pending_build_source) { create(:ci_build_source, build: pending_build, source: 'scan_execution_policy') }
|
||||
let(:args) { { sources: %w[scan_execution_policy] } }
|
||||
|
||||
it { is_expected.to contain_exactly(successful_build, pending_build) }
|
||||
|
||||
context 'with multiple sources' do
|
||||
let_it_be(:failed_build_source) { create(:ci_build_source, build: failed_build, source: 'trigger') }
|
||||
let(:args) { { sources: %w[scan_execution_policy trigger] } }
|
||||
|
||||
it { is_expected.to contain_exactly(successful_build, pending_build, failed_build) }
|
||||
end
|
||||
|
||||
context 'when FF is disabled' do
|
||||
before do
|
||||
stub_feature_flags(populate_and_use_build_source_table: false)
|
||||
end
|
||||
|
||||
it 'does not filter by source' do
|
||||
is_expected.to contain_exactly(successful_build, successful_build_two, failed_build, pending_build)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with name filter also present' do
|
||||
let(:args) { { name: "Three", sources: %w[scan_execution_policy] } }
|
||||
|
||||
before do
|
||||
stub_feature_flags(populate_and_use_build_names_table: true)
|
||||
end
|
||||
|
||||
it 'filters only by name' do
|
||||
is_expected.to contain_exactly(failed_build, pending_build)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given pagination params' do
|
||||
let(:cursor) { Base64.strict_encode64({ id: failed_build.id.to_s }.to_json) }
|
||||
let(:query) do
|
||||
<<~QUERY
|
||||
{
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
jobs(sources: [SCAN_EXECUTION_POLICY], first: 1, after: "#{cursor}") {
|
||||
nodes { id }
|
||||
}
|
||||
}
|
||||
}
|
||||
QUERY
|
||||
end
|
||||
|
||||
it "returns the paginated build" do
|
||||
graphq_response = GitlabSchema.execute(query, context: { current_user: current_user })
|
||||
parsed_response = graphql_dig_at(graphq_response, 'data', 'project', 'jobs', 'nodes')
|
||||
expect(parsed_response).to match_array([a_graphql_entity_for(pending_build)])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by build name' do
|
||||
let(:args) { { name: 'Three' } }
|
||||
|
||||
|
|
|
|||
|
|
@ -541,7 +541,11 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
|
|||
subject { described_class.fields['jobs'] }
|
||||
|
||||
it { is_expected.to have_graphql_type(Types::Ci::JobType.connection_type) }
|
||||
it { is_expected.to have_graphql_arguments(:statuses, :with_artifacts, :name, :after, :before, :first, :last) }
|
||||
|
||||
it do
|
||||
is_expected.to have_graphql_arguments(:statuses, :with_artifacts, :name, :sources,
|
||||
:after, :before, :first, :last)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ci_template field' do
|
||||
|
|
|
|||
|
|
@ -513,7 +513,6 @@ RSpec.describe ApplicationHelper do
|
|||
{
|
||||
page: 'application',
|
||||
page_type_id: nil,
|
||||
find_file: nil,
|
||||
group: nil,
|
||||
group_full_path: nil
|
||||
}
|
||||
|
|
@ -530,7 +529,6 @@ RSpec.describe ApplicationHelper do
|
|||
{
|
||||
page: 'application',
|
||||
page_type_id: nil,
|
||||
find_file: nil,
|
||||
group: group.path,
|
||||
group_full_path: group.full_path
|
||||
}
|
||||
|
|
@ -549,12 +547,10 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
|
||||
it 'includes all possible body data elements and associates the project elements with project' do
|
||||
expect(helper).to receive(:can?).with(nil, :read_code, project)
|
||||
expect(helper.body_data).to eq(
|
||||
{
|
||||
page: 'application',
|
||||
page_type_id: nil,
|
||||
find_file: nil,
|
||||
group: nil,
|
||||
group_full_path: nil,
|
||||
project_id: project.id,
|
||||
|
|
@ -569,12 +565,10 @@ RSpec.describe ApplicationHelper do
|
|||
let_it_be(:project) { create(:project, :repository, group: create(:group)) }
|
||||
|
||||
it 'includes all possible body data elements and associates the project elements with project' do
|
||||
expect(helper).to receive(:can?).with(nil, :read_code, project)
|
||||
expect(helper.body_data).to eq(
|
||||
{
|
||||
page: 'application',
|
||||
page_type_id: nil,
|
||||
find_file: nil,
|
||||
group: project.group.name,
|
||||
group_full_path: project.group.full_path,
|
||||
project_id: project.id,
|
||||
|
|
@ -597,12 +591,10 @@ RSpec.describe ApplicationHelper do
|
|||
stub_controller_method(:action_name, 'show')
|
||||
stub_controller_method(:params, { id: issue.id })
|
||||
|
||||
expect(helper).to receive(:can?).with(nil, :read_code, project).and_return(false)
|
||||
expect(helper.body_data).to eq(
|
||||
{
|
||||
page: 'projects:issues:show',
|
||||
page_type_id: issue.id,
|
||||
find_file: nil,
|
||||
group: nil,
|
||||
group_full_path: nil,
|
||||
project_id: issue.project.id,
|
||||
|
|
@ -614,37 +606,6 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'find_file attribute' do
|
||||
subject { helper.body_data[:find_file] }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
context 'when the project has no repository' do
|
||||
before do
|
||||
allow(project).to receive(:empty_repo?).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when user cannot read_code for the project' do
|
||||
before do
|
||||
allow(helper).to receive(:can?).with(user, :read_code, project).and_return(false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when current_user has read_code permission' do
|
||||
it 'returns find_file with the default branch' do
|
||||
expect(helper).to receive(:can?).with(user, :read_code, project).and_return(true)
|
||||
expect(subject).to end_with(project.default_branch)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stub_controller_method(method_name, value)
|
||||
|
|
|
|||
|
|
@ -78,40 +78,23 @@ RSpec.describe OauthAccessToken, feature_category: :system_access do
|
|||
|
||||
describe '#scope_user' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_refind(:oauth_access_token) { create(:oauth_access_token) }
|
||||
let(:user_id) { user.id }
|
||||
|
||||
before do
|
||||
allow(oauth_access_token).to receive(:scopes).and_return(scopes)
|
||||
end
|
||||
|
||||
context 'when scopes match expected format' do
|
||||
context 'when scopes only include the composite scope' do
|
||||
let(:scopes) { "user:#{user_id}" }
|
||||
|
||||
it 'returns the user' do
|
||||
expect(oauth_access_token.scope_user).to eq user
|
||||
end
|
||||
where(:scopes) do
|
||||
[
|
||||
"user:%{user_id}",
|
||||
"other:scope user:%{user_id}",
|
||||
"user:%{user_id} other:scope",
|
||||
"api user:%{user_id} read_api"
|
||||
]
|
||||
end
|
||||
|
||||
context 'when scopes include another scope before composite scope' do
|
||||
let(:scopes) { "other:scope user:#{user_id}" }
|
||||
|
||||
it 'returns the user' do
|
||||
expect(oauth_access_token.scope_user).to eq user
|
||||
with_them do
|
||||
let(:formatted_scopes) do
|
||||
format(scopes, user_id: user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when scopes include another scope after composite scope" do
|
||||
let(:scopes) { "user:#{user_id} other:scope" }
|
||||
|
||||
it 'returns the user' do
|
||||
expect(oauth_access_token.scope_user).to eq user
|
||||
end
|
||||
end
|
||||
|
||||
context 'when scopes include another scope before and after composite scope' do
|
||||
let(:scopes) { "api user:#{user_id} read_api" }
|
||||
let(:oauth_access_token) { create(:oauth_access_token, scopes: formatted_scopes) }
|
||||
|
||||
it 'returns the user' do
|
||||
expect(oauth_access_token.scope_user).to eq user
|
||||
|
|
@ -123,12 +106,24 @@ RSpec.describe OauthAccessToken, feature_category: :system_access do
|
|||
where(:scopes) do
|
||||
[
|
||||
"user:#{non_existing_record_id}",
|
||||
'fuser:%{user_id}',
|
||||
'user:%{user_id}f',
|
||||
'user:%{user_id} user:2',
|
||||
'user:not_a_number',
|
||||
'some:other:scope',
|
||||
nil,
|
||||
""
|
||||
]
|
||||
end
|
||||
let(:formatted_scopes) do
|
||||
if scopes.presence
|
||||
format(scopes, user_id: user.id)
|
||||
else
|
||||
scopes
|
||||
end
|
||||
end
|
||||
|
||||
let(:oauth_access_token) { create(:oauth_access_token, scopes: formatted_scopes) }
|
||||
|
||||
with_them do
|
||||
it 'returns false' do
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
|
|||
|
||||
context 'when the author is a member of the project', :snowplow do
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
project.add_developer(snippet.author)
|
||||
end
|
||||
|
||||
it_behaves_like 'graphql update actions'
|
||||
|
|
@ -196,17 +196,15 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
|
|||
end
|
||||
|
||||
context 'when not sessionless', :clean_gitlab_redis_sessions do
|
||||
let(:current_user) { nil }
|
||||
|
||||
before do
|
||||
stub_session(
|
||||
session_data: {
|
||||
'warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt]
|
||||
}
|
||||
)
|
||||
sign_in(snippet.author)
|
||||
end
|
||||
|
||||
it_behaves_like 'internal event tracking' do
|
||||
let(:event) { 'g_edit_by_snippet_ide' }
|
||||
let(:user) { current_user }
|
||||
let(:user) { snippet.author }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -235,7 +235,6 @@ RSpec.describe 'GraphQL', feature_category: :shared do
|
|||
stub_authentication_activity_metrics do |metrics|
|
||||
expect(metrics)
|
||||
.to increment(:user_authenticated_counter)
|
||||
.and increment(:user_session_destroyed_counter)
|
||||
|
||||
expect(metrics.user_csrf_token_invalid_counter)
|
||||
.to receive(:increment).with(controller: 'GraphqlController', auth: 'session')
|
||||
|
|
@ -243,7 +242,7 @@ RSpec.describe 'GraphQL', feature_category: :shared do
|
|||
|
||||
post_graphql(query, headers: { 'X-CSRF-Token' => 'invalid' })
|
||||
|
||||
expect(graphql_data['echo']).to eq('nil says: Hello world')
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'authenticates a user with a valid session token' do
|
||||
|
|
@ -259,6 +258,43 @@ RSpec.describe 'GraphQL', feature_category: :shared do
|
|||
|
||||
expect(graphql_data['echo']).to eq("\"#{user.username}\" says: Hello world")
|
||||
end
|
||||
|
||||
context 'when fix_graphql_csrf is disabled' do
|
||||
before do
|
||||
stub_feature_flags(fix_graphql_csrf: false)
|
||||
end
|
||||
|
||||
it 'does not authenticate a user with an invalid CSRF' do
|
||||
login_as(user)
|
||||
|
||||
stub_authentication_activity_metrics do |metrics|
|
||||
expect(metrics)
|
||||
.to increment(:user_authenticated_counter)
|
||||
.and increment(:user_session_destroyed_counter)
|
||||
|
||||
expect(metrics.user_csrf_token_invalid_counter)
|
||||
.to receive(:increment).with(controller: 'GraphqlController', auth: 'session')
|
||||
end
|
||||
|
||||
post_graphql(query, headers: { 'X-CSRF-Token' => 'invalid' })
|
||||
|
||||
expect(graphql_data['echo']).to eq('nil says: Hello world')
|
||||
end
|
||||
|
||||
it 'authenticates a user with a valid session token' do
|
||||
# Create a session to get a CSRF token from
|
||||
login_as(user)
|
||||
get('/')
|
||||
|
||||
stub_authentication_activity_metrics do |metrics|
|
||||
expect(metrics.user_csrf_token_invalid_counter).not_to receive(:increment)
|
||||
end
|
||||
|
||||
post '/api/graphql', params: { query: query }, headers: { 'X-CSRF-Token' => session['_csrf_token'] }
|
||||
|
||||
expect(graphql_data['echo']).to eq("\"#{user.username}\" says: Hello world")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with token authentication' do
|
||||
|
|
@ -271,13 +307,7 @@ RSpec.describe 'GraphQL', feature_category: :shared do
|
|||
.and increment(:user_session_override_counter)
|
||||
.and increment(:user_sessionless_authentication_counter)
|
||||
|
||||
##
|
||||
# TODO: PAT authentication should not trigger `handle_unverified_request` on CSRF token mismatch.
|
||||
#
|
||||
# `auth` type is 'other' here, becase we `handle_unverified_request` before we call sessionless sign in hooks.
|
||||
#
|
||||
expect(metrics.user_csrf_token_invalid_counter)
|
||||
.to receive(:increment).with(controller: 'GraphqlController', auth: 'other')
|
||||
expect(metrics.user_csrf_token_invalid_counter).not_to receive(:increment)
|
||||
end
|
||||
|
||||
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
|
||||
|
|
@ -285,6 +315,70 @@ RSpec.describe 'GraphQL', feature_category: :shared do
|
|||
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
|
||||
end
|
||||
|
||||
context 'when user also has a valid session' do
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
|
||||
before do
|
||||
login_as(other_user)
|
||||
get('/')
|
||||
end
|
||||
|
||||
it 'authenticates as PAT user' do
|
||||
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token, 'X-CSRF-Token' => session['_csrf_token'] })
|
||||
|
||||
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
|
||||
end
|
||||
|
||||
it 'authenticates as PAT user even when CSRF token is invalid' do
|
||||
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token, 'X-CSRF-Token' => 'invalid' })
|
||||
|
||||
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when fix_graphql_csrf is disabled' do
|
||||
before do
|
||||
stub_feature_flags(fix_graphql_csrf: false)
|
||||
end
|
||||
|
||||
it 'authenticates users with a PAT' do
|
||||
stub_authentication_activity_metrics(debug: false) do |metrics|
|
||||
expect(metrics)
|
||||
.to increment(:user_authenticated_counter)
|
||||
.and increment(:user_session_override_counter)
|
||||
.and increment(:user_sessionless_authentication_counter)
|
||||
|
||||
expect(metrics.user_csrf_token_invalid_counter)
|
||||
.to receive(:increment).with(controller: 'GraphqlController', auth: 'other')
|
||||
end
|
||||
|
||||
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
|
||||
|
||||
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
|
||||
end
|
||||
|
||||
context 'when user also has a valid session' do
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
|
||||
before do
|
||||
login_as(other_user)
|
||||
get('/')
|
||||
end
|
||||
|
||||
it 'authenticates as session user' do
|
||||
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token, 'X-CSRF-Token' => session['_csrf_token'] })
|
||||
|
||||
expect(graphql_data['echo']).to eq("\"#{other_user.username}\" says: Hello world")
|
||||
end
|
||||
|
||||
it 'authenticates as PAT user when CSRF token is invalid' do
|
||||
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token, 'X-CSRF-Token' => 'invalid' })
|
||||
|
||||
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'valid token' do
|
||||
it 'accepts from header' do
|
||||
post_graphql(query, headers: { 'Authorization' => "Bearer #{token}" })
|
||||
|
|
|
|||
|
|
@ -194,6 +194,16 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
|
|||
|
||||
it_behaves_like 'successfully downloads the file'
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
let_it_be(:package, reload: true) { create(:npm_package_legacy, project: project, name: FFaker::Lorem.word, version: '1.2.3') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'successfully downloads the file'
|
||||
end
|
||||
end
|
||||
|
||||
context 'private project' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require('spec_helper')
|
||||
|
||||
RSpec.describe UserSettings::PasswordsController, type: :request, feature_category: :system_access do
|
||||
let(:deactivated_user) { create(:user, :deactivated) }
|
||||
|
||||
let(:password) { User.random_password }
|
||||
let(:password_confirmation) { password }
|
||||
let(:reset_password_token) { deactivated_user.send_reset_password_instructions }
|
||||
|
||||
subject(:update_password) do
|
||||
put user_settings_password_path, params: {
|
||||
user: {
|
||||
password: password,
|
||||
password_confirmation: password_confirmation,
|
||||
reset_password_token: reset_password_token
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
describe '#new' do
|
||||
context 'when a deactivated user signs-in after an admin resets their password' do
|
||||
before do
|
||||
sign_in deactivated_user
|
||||
get new_user_settings_password_path
|
||||
deactivated_user.reload
|
||||
end
|
||||
|
||||
it 'reactivates the user', :aggregate_failures do
|
||||
expect(deactivated_user[:state]).to eq('active')
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'renders the update password page' do
|
||||
expect(response.body).to include('To continue, please update your password')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
context 'when a deactivated user signs-in after an admin resets their password' do
|
||||
before do
|
||||
sign_in deactivated_user
|
||||
get new_user_settings_password_path
|
||||
deactivated_user.reload
|
||||
end
|
||||
|
||||
context 'when the user updates their password' do
|
||||
before do
|
||||
update_password
|
||||
get new_user_session_path
|
||||
end
|
||||
|
||||
it 'redirects backs to the root path(sign-in page)', :aggregate_failures do
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -95,6 +95,29 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :grou
|
|||
|
||||
it_behaves_like 'transfer allowed'
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it 'does not allow transfer' do
|
||||
transfer_service.execute(new_group)
|
||||
|
||||
expect(transfer_service.error).to eq('Transfer failed: Group contains projects with NPM packages scoped to the current root level group.')
|
||||
expect(group.parent).not_to eq(new_group)
|
||||
end
|
||||
|
||||
context 'namespaced package is pending destruction' do
|
||||
let!(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
package.pending_destruction!
|
||||
end
|
||||
|
||||
it_behaves_like 'transfer allowed'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transferring a group into a root group' do
|
||||
|
|
|
|||
|
|
@ -334,6 +334,15 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
|
|
@ -352,6 +361,15 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
|
|
@ -370,6 +388,15 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
|
|
|
|||
|
|
@ -121,9 +121,7 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category:
|
|||
|
||||
describe '#execute', :aggregate_failures do
|
||||
before do
|
||||
# Decrease the sleep in this test, so the test suite runs faster.
|
||||
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/493977
|
||||
stub_const("#{described_class}::RELATION_BATCH_SLEEP", 0.01)
|
||||
allow(service).to receive_messages(db_health_check!: nil, db_table_health_check!: nil, check_db_health?: true)
|
||||
end
|
||||
|
||||
shared_examples 'a successful reassignment' do
|
||||
|
|
@ -151,10 +149,19 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category:
|
|||
context 'when a user can be reassigned without error' do
|
||||
it_behaves_like 'a successful reassignment'
|
||||
|
||||
it 'sleeps between processing each model relation batch' do
|
||||
expect(Kernel).to receive(:sleep).with(0.01).exactly(8).times
|
||||
context 'when reassignment throttling is disabled' do
|
||||
before do
|
||||
stub_feature_flags(reassignment_throttling: false)
|
||||
# Decrease the sleep in this test, so the test suite runs faster.
|
||||
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/493977
|
||||
stub_const("#{described_class}::RELATION_BATCH_SLEEP", 0.01)
|
||||
end
|
||||
|
||||
service.execute
|
||||
it 'sleeps between processing each model relation batch' do
|
||||
expect(Kernel).to receive(:sleep).with(0.01).exactly(8).times
|
||||
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates actual records from the source user\'s placeholder reference records' do
|
||||
|
|
@ -194,6 +201,35 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category:
|
|||
)
|
||||
end
|
||||
|
||||
context 'when the membership table is unhealthy' do
|
||||
let(:db_members_table_health_failure) do
|
||||
described_class::DatabaseHealthError.new("#{Member.table_name} table unhealthy")
|
||||
end
|
||||
|
||||
it 'returns a reschedule response' do
|
||||
allow(service).to receive(:db_table_health_check!).with(Member).and_raise(db_members_table_health_failure)
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:ok)
|
||||
expect(result.reason).to eq(:db_health_check_failed)
|
||||
expect(result.message).to eq('Rescheduling placeholder user records reassignment: database health')
|
||||
end
|
||||
|
||||
it 'logs a warning' do
|
||||
allow(service).to receive(:db_table_health_check!).with(Member).and_raise(db_members_table_health_failure)
|
||||
|
||||
expect(::Import::Framework::Logger).to receive(:warn).with(
|
||||
hash_including(
|
||||
message: "members table unhealthy. Rescheduling reassignment",
|
||||
source_user_id: source_user.id
|
||||
)
|
||||
)
|
||||
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls UserProjectAccessChangedService' do
|
||||
expect_next_instance_of(UserProjectAccessChangedService, reassign_to_user.id) do |service|
|
||||
expect(service).to receive(:execute)
|
||||
|
|
@ -226,8 +262,7 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category:
|
|||
|
||||
context 'when reassigned by user no longer exists' do
|
||||
before do
|
||||
source_user.reassigned_by_user.destroy!
|
||||
source_user.reload
|
||||
service.instance_variable_set(:@reassigned_by_user, nil)
|
||||
end
|
||||
|
||||
it 'can still create memberships' do
|
||||
|
|
@ -650,6 +685,180 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsService, feature_category:
|
|||
end
|
||||
end
|
||||
|
||||
context 'when database is healthy' do
|
||||
before do
|
||||
allow(service).to receive_messages(db_health_check!: nil, db_table_health_check!: nil, check_db_health?: true)
|
||||
end
|
||||
|
||||
it 'checks all tables and individual tables' do
|
||||
expect(service).to receive(:db_health_check!).at_least(:once)
|
||||
expect(service).to receive(:db_table_health_check!).at_least(:once)
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
context 'when :reassignment_throttling is disabled' do
|
||||
before do
|
||||
stub_feature_flags(reassignment_throttling: false)
|
||||
end
|
||||
|
||||
it 'does not check database health' do
|
||||
expect(service).not_to receive(:db_health_check!)
|
||||
expect(service).not_to receive(:db_table_health_check!)
|
||||
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when database is unhealthy' do
|
||||
let(:db_table_health_failure) { described_class::DatabaseHealthError.new("#{User.table_name} table unhealthy") }
|
||||
let(:db_health_failure) { described_class::DatabaseHealthError.new("Database unhealthy") }
|
||||
|
||||
it 'returns a reschedule response when checking global tables' do
|
||||
allow(service).to receive(:db_health_check!).and_raise(db_health_failure)
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:ok)
|
||||
expect(result.reason).to eq(:db_health_check_failed)
|
||||
expect(result.message).to eq('Rescheduling placeholder user records reassignment: database health')
|
||||
end
|
||||
|
||||
it 'logs a warning' do
|
||||
allow(service).to receive(:db_health_check!).and_raise(db_health_failure)
|
||||
|
||||
expect(::Import::Framework::Logger).to receive(:warn).with(
|
||||
hash_including(
|
||||
message: "Database unhealthy. Rescheduling reassignment",
|
||||
source_user_id: source_user.id
|
||||
)
|
||||
)
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
it 'returns a reschedule response when checking a single table' do
|
||||
allow(service).to receive(:db_table_health_check!).and_raise(db_table_health_failure)
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:ok)
|
||||
expect(result.reason).to eq(:db_health_check_failed)
|
||||
expect(result.message).to eq('Rescheduling placeholder user records reassignment: database health')
|
||||
end
|
||||
|
||||
it 'logs a warning when checking a single table' do
|
||||
allow(service).to receive(:db_table_health_check!).and_raise(db_table_health_failure)
|
||||
|
||||
expect(::Import::Framework::Logger).to receive(:warn).with(
|
||||
hash_including(
|
||||
message: "users table unhealthy. Rescheduling reassignment",
|
||||
source_user_id: source_user.id
|
||||
)
|
||||
)
|
||||
|
||||
service.execute
|
||||
end
|
||||
end
|
||||
|
||||
describe '#db_table_health_check!' do
|
||||
let(:health_status) { Gitlab::Database::HealthStatus }
|
||||
let(:table_health_indicator_class) { health_status::Indicators::AutovacuumActiveOnTable }
|
||||
let(:table_health_indicator) { instance_double(table_health_indicator_class) }
|
||||
let(:stop) { true }
|
||||
let(:stop_signal) do
|
||||
instance_double(
|
||||
"#{health_status}::Signals::Stop",
|
||||
log_info?: true,
|
||||
stop?: stop,
|
||||
indicator_class: table_health_indicator_class,
|
||||
short_name: 'Stop',
|
||||
reason: 'Test Exception'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(table_health_indicator_class).to receive(:new).with(anything).and_return(table_health_indicator)
|
||||
allow(table_health_indicator).to receive(:evaluate).and_return(stop_signal)
|
||||
end
|
||||
|
||||
context 'when the table is unhealthy' do
|
||||
it 'raises an error' do
|
||||
expect { service.send(:db_table_health_check!, User) }.to raise_error(described_class::DatabaseHealthError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the table is healthy' do
|
||||
let(:stop) { false }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(service.send(:db_table_health_check!, User)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#db_health_check!' do
|
||||
let(:health_status) { Gitlab::Database::HealthStatus }
|
||||
let(:health_status_indicator_class) { health_status::Indicators::WriteAheadLog }
|
||||
let(:health_status_indicator) { instance_double(health_status_indicator_class) }
|
||||
let(:stop) { false }
|
||||
let(:stop_signal) do
|
||||
instance_double(
|
||||
"#{health_status}::Signals::Stop",
|
||||
log_info?: true,
|
||||
stop?: stop,
|
||||
indicator_class: health_status_indicator_class,
|
||||
short_name: 'Stop',
|
||||
reason: 'Test Exception'
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(service).to receive(:check_db_health?).and_return(true)
|
||||
allow(health_status_indicator_class).to receive(:new).with(anything).and_return(health_status_indicator)
|
||||
allow(health_status_indicator).to receive(:evaluate).and_return(stop_signal)
|
||||
allow(Rails.cache).to receive(:fetch).and_yield
|
||||
end
|
||||
|
||||
context 'when caching health status' do
|
||||
after do
|
||||
travel_back
|
||||
end
|
||||
|
||||
it 'caches the result for 30 seconds' do
|
||||
expect(Rails.cache).to receive(:fetch).with(
|
||||
"reassign_placeholder_user_records_service_db_check",
|
||||
{ expires_in: 30.seconds }
|
||||
).thrice.and_yield
|
||||
|
||||
service.send(:db_health_check!)
|
||||
|
||||
travel 25.seconds
|
||||
service.send(:db_health_check!)
|
||||
|
||||
travel 6.seconds
|
||||
service.send(:db_health_check!)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the database is unhealthy' do
|
||||
let(:stop) { true }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { service.send(:db_health_check!) }.to raise_error(described_class::DatabaseHealthError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the database is healthy' do
|
||||
let(:stop) { false }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(service.send(:db_health_check!)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_placeholder_reference(source_user, object, user_column:, composite_key: nil)
|
||||
numeric_key = object.id if composite_key.nil?
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Packages::MarkPackageForDestructionService, feature_category: :package_registry do
|
||||
RSpec.describe Packages::MarkPackageForDestructionService, :aggregate_failures, feature_category: :package_registry do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be_with_reload(:package) { create(:npm_package) }
|
||||
|
||||
|
|
@ -29,6 +29,17 @@ RSpec.describe Packages::MarkPackageForDestructionService, feature_category: :pa
|
|||
expect(response).to be_success
|
||||
expect(response.message).to eq("Package was successfully marked as pending destruction")
|
||||
end
|
||||
|
||||
context 'when not npm package' do
|
||||
let_it_be_with_reload(:package) { create(:pypi_package) }
|
||||
|
||||
it 'returns a success ServiceResponse' do
|
||||
expect(package).to receive(:sync_maven_metadata).and_call_original
|
||||
expect(package).to receive(:mark_package_files_for_destruction).and_call_original
|
||||
expect(package).not_to receive(:sync_npm_metadata_cache)
|
||||
expect(service.execute).to be_success
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is not successful' do
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
|
|||
let_it_be(:package2) { create(:npm_package, version: '2.0.6', project: project, name: package_name) }
|
||||
let_it_be(:latest_package) { create(:npm_package, version: '2.0.11', project: project, name: package_name) }
|
||||
|
||||
let(:packages) { project.packages.npm.with_name(package_name) }
|
||||
let(:packages) { ::Packages::Npm::Package.for_projects(project).with_name(package_name) }
|
||||
let(:metadata) { described_class.new(package_name, packages).execute }
|
||||
|
||||
describe '#versions' do
|
||||
|
|
@ -74,7 +74,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
|
|||
end
|
||||
|
||||
context 'when generate dependencies' do
|
||||
let(:packages) { ::Packages::Package.where(id: package1.id) }
|
||||
let(:packages) { ::Packages::Npm::Package.where(id: package1.id) }
|
||||
|
||||
it 'loads grouped dependency links', :aggregate_failures do
|
||||
expect(::Packages::DependencyLink).to receive(:dependency_ids_grouped_by_type).and_call_original
|
||||
|
|
@ -166,7 +166,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
|
|||
let_it_be(:package_tag1) { create(:packages_tag, package: package1, name: 'latest') }
|
||||
let_it_be(:package_tag2) { create(:packages_tag, package: package2, name: 'latest') }
|
||||
|
||||
let(:packages) { ::Packages::Package.for_projects([project.id, project2.id]).with_name(package_name) }
|
||||
let(:packages) { ::Packages::Npm::Package.for_projects([project.id, project2.id]).with_name(package_name) }
|
||||
|
||||
it "returns the tag of the latest package's version" do
|
||||
expect(subject['latest']).to eq(package2.version)
|
||||
|
|
@ -193,7 +193,7 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
|
|||
end
|
||||
|
||||
it 'returns all tags' do
|
||||
expect(::Packages::Package).to receive(:preload_tags).and_call_original
|
||||
expect(::Packages::Npm::Package).to receive(:preload_tags).and_call_original
|
||||
|
||||
expect(subject.size).to eq(Packages::Tag.count)
|
||||
end
|
||||
|
|
@ -210,14 +210,14 @@ RSpec.describe ::Packages::Npm::GenerateMetadataService, feature_category: :pack
|
|||
end
|
||||
|
||||
def check_n_plus_one(only_dist_tags: false)
|
||||
pkgs = project.packages.npm.with_name(package_name).preload_files
|
||||
pkgs = ::Packages::Npm::Package.for_projects(project).with_name(package_name).preload_files
|
||||
control = ActiveRecord::QueryRecorder.new do
|
||||
described_class.new(package_name, pkgs).execute(only_dist_tags: only_dist_tags)
|
||||
end
|
||||
|
||||
yield
|
||||
|
||||
pkgs = project.packages.npm.with_name(package_name).preload_files
|
||||
pkgs = ::Packages::Npm::Package.for_projects(project).with_name(package_name).preload_files
|
||||
|
||||
expect do
|
||||
described_class.new(package_name, pkgs).execute(only_dist_tags: only_dist_tags)
|
||||
|
|
|
|||
|
|
@ -12,5 +12,9 @@ module Features
|
|||
find_field('new-access-token').value
|
||||
end
|
||||
end
|
||||
|
||||
def active_access_tokens_counter
|
||||
find_by_testid('active-token-count')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,6 +60,23 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsWorker, feature_category: :
|
|||
end
|
||||
end
|
||||
|
||||
context 'when database is unhealthy' do
|
||||
let(:service_response) { ServiceResponse.new(status: :success, reason: :db_health_check_failed) }
|
||||
let(:backoff_period) { described_class::BACKOFF_PERIOD }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Import::ReassignPlaceholderUserRecordsService) do |service|
|
||||
allow(service).to receive(:execute).and_return(service_response)
|
||||
end
|
||||
end
|
||||
|
||||
it 're-enqueues the job' do
|
||||
expect(described_class).to receive(:perform_in).with(backoff_period, import_source_user.id, {})
|
||||
|
||||
described_class.new.perform(import_source_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sidekiq_retries_exhausted' do
|
||||
it 'logs the failure and sets the source user status to failed', :aggregate_failures do
|
||||
exception = StandardError.new('Some error')
|
||||
|
|
|
|||
Loading…
Reference in New Issue